Files
php-flasher/docs/_includes/flasher-studio.html
T
Younes ENNAJI 5d7145ff65 Wip
2025-03-13 06:05:44 +00:00

1686 lines
63 KiB
HTML

<!-- Elegant PHPFlasher Interactive Demo -->
<section class="min-h-screen relative overflow-hidden bg-gradient-to-br from-gray-50 to-gray-100 py-8">
<!-- Dynamic background elements -->
<div class="absolute inset-0 z-0">
<div
class="absolute top-0 right-0 w-1/3 h-1/3 bg-gradient-to-br from-blue-500/5 to-indigo-500/5 rounded-full blur-3xl"></div>
<div
class="absolute bottom-0 left-0 w-1/3 h-1/3 bg-gradient-to-tr from-emerald-500/5 to-blue-500/5 rounded-full blur-3xl"></div>
<!-- Animated particles -->
<div id="particles-js" class="absolute inset-0 opacity-60"></div>
<!-- Animated lines -->
<svg class="absolute inset-0 w-full h-full z-0 opacity-10" viewBox="0 0 100 100" preserveAspectRatio="none">
<line x1="0" y1="0" x2="100" y2="100" stroke="currentColor" stroke-width="0.2"
class="text-blue-500 animate-[pulse_8s_cubic-bezier(0.4,0,0.6,1)_infinite]"></line>
<line x1="100" y1="0" x2="0" y2="100" stroke="currentColor" stroke-width="0.2"
class="text-indigo-500 animate-[pulse_6s_cubic-bezier(0.4,0,0.6,1)_infinite]"></line>
</svg>
</div>
<!-- Main content -->
<div class="container relative z-10 mx-auto px-4 max-w-7xl">
<!-- Elegant header -->
<header class="mb-8 text-center relative">
<div
class="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-1 bg-gradient-to-r from-blue-500/0 via-blue-500 to-blue-500/0"></div>
<h1 class="mt-6 text-3xl font-light tracking-tight">
PHPFlasher <span
class="font-semibold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600">Interactive</span>
Studio
</h1>
<p class="mt-2 text-gray-500 max-w-2xl mx-auto">Design your perfect notification, customize options, and see
it in action</p>
</header>
<!-- Main interactive area -->
<div class="rounded-xl shadow-lg bg-white overflow-hidden backdrop-blur-sm border border-gray-100">
<!-- Toolbar -->
<div
class="bg-gradient-to-r from-gray-50 to-gray-100 border-b border-gray-200 px-6 py-2 flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div class="text-sm text-gray-500 font-medium">PHPFlasher Studio • Build ID: FL-20250311</div>
<div id="status-indicator" class="flex items-center space-x-2">
<span id="status-text" class="text-xs text-emerald-600">Ready</span>
<div
class="w-2 h-2 bg-emerald-500 rounded-full animate-[ping_2.5s_cubic-bezier(0,0,0.2,1)_infinite]"></div>
</div>
</div>
<!-- Main content area with tabs -->
<div class="flex flex-col lg:flex-row">
<!-- Left panel: Options -->
<div class="w-full lg:w-2/5 border-r border-gray-100">
<!-- Options panel -->
<div class="p-4 overflow-y-auto" style="height: calc(100vh - 13rem); max-height: 700px;">
<!-- Notification Type Selection - Changed to flex row layout -->
<div class="mb-6">
<label class="block mb-2 text-sm font-medium text-gray-700">Notification Type</label>
<div class="flex flex-row space-x-2 w-full">
<button
class="type-button flex-1 flex flex-col items-center justify-center p-3 rounded-lg bg-green-50 border border-green-200 hover:bg-green-100 transition-colors active:scale-95 transform border-2 border-green-500"
data-type="success">
<span
class="flex items-center justify-center w-8 h-8 rounded-md bg-green-100 text-green-600 mb-2">
<i class="fas fa-check text-lg"></i>
</span>
<span class="text-xs font-medium text-gray-700">Success</span>
</button>
<button
class="type-button flex-1 flex flex-col items-center justify-center p-3 rounded-lg bg-red-50 border border-red-200 hover:bg-red-100 transition-colors active:scale-95 transform"
data-type="error">
<span
class="flex items-center justify-center w-8 h-8 rounded-md bg-red-100 text-red-600 mb-2">
<i class="fas fa-times text-lg"></i>
</span>
<span class="text-xs font-medium text-gray-700">Error</span>
</button>
<button
class="type-button flex-1 flex flex-col items-center justify-center p-3 rounded-lg bg-blue-50 border border-blue-200 hover:bg-blue-100 transition-colors active:scale-95 transform"
data-type="info">
<span
class="flex items-center justify-center w-8 h-8 rounded-md bg-blue-100 text-blue-600 mb-2">
<i class="fas fa-info-circle text-lg"></i>
</span>
<span class="text-xs font-medium text-gray-700">Info</span>
</button>
<button
class="type-button flex-1 flex flex-col items-center justify-center p-3 rounded-lg bg-amber-50 border border-amber-200 hover:bg-amber-100 transition-colors active:scale-95 transform"
data-type="warning">
<span
class="flex items-center justify-center w-8 h-8 rounded-md bg-amber-100 text-amber-600 mb-2">
<i class="fas fa-exclamation-triangle text-lg"></i>
</span>
<span class="text-xs font-medium text-gray-700">Warning</span>
</button>
</div>
</div>
<!-- Content Section -->
<div class="mb-6 space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<label for="title-input" class="block text-sm font-medium text-gray-700">Title
(optional)</label>
<span class="text-xs text-gray-400">Optional</span>
</div>
<input type="text" id="title-input"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-shadow"
placeholder="Enter notification title" value="">
</div>
<div>
<label for="message-input"
class="block mb-2 text-sm font-medium text-gray-700">Message</label>
<input id="message-input"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your notification message"
value="Your product has been created successfully!">
</div>
</div>
<!-- Configuration Options -->
<div class="mb-6 border border-gray-200 rounded-md overflow-hidden shadow-sm">
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-3 py-2 flex items-center">
<span class="font-medium text-gray-700">Configuration Options</span>
</div>
<div class="px-3 py-3 border-t border-gray-200">
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label for="position-select"
class="block mb-1 text-sm font-medium text-gray-700">Position</label>
<select id="position-select"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Default</option>
<option value="top-right">top-right</option>
<option value="top-left">top-left</option>
<option value="bottom-right">bottom-right</option>
<option value="bottom-left">bottom-left</option>
<option value="center-top">center-top</option>
<option value="center-bottom">center-bottom</option>
</select>
</div>
<div>
<label for="timeout-input" class="block mb-1 text-sm font-medium text-gray-700">Timeout
(ms)</label>
<select id="timeout-input"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Default</option>
<option value="10000">10 seconds</option>
<option value="5000">5 seconds</option>
<option value="3000">3 seconds</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label for="theme-select" class="block mb-1 text-sm font-medium text-gray-700">Theme</label>
<select id="theme-select"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Default</option>
<option value="amazon">amazon</option>
<option value="amber">amber</option>
<option value="aurora">aurora</option>
<option value="crystal">crystal</option>
<option value="emerald">emerald</option>
<option value="facebook">facebook</option>
<option value="flasher">flasher</option>
<option value="google">google</option>
<option value="ios">ios</option>
<option value="jade">jade</option>
<option value="material">material</option>
<option value="minimal">minimal</option>
<option value="neon">neon</option>
<option value="onyx">onyx</option>
<option value="ruby">ruby</option>
<option value="sapphire">sapphire</option>
<option value="slack">slack</option>
</select>
</div>
<div>
<label for="direction-select"
class="block mb-1 text-sm font-medium text-gray-700">Direction</label>
<select id="direction-select"
class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Default</option>
<option value="top">top</option>
<option value="bottom">bottom</option>
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-3">
<div class="flex items-center">
<input type="checkbox" id="rtl-check"
class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
<label for="rtl-check" class="ml-2 text-sm text-gray-700">RTL Layout</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="escape-html-check"
class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500" checked>
<label for="escape-html-check" class="ml-2 text-sm text-gray-700">Escape
HTML</label>
</div>
</div>
</div>
</div>
<!-- Launch button -->
<button id="show-notification-btn" class="w-full group relative overflow-hidden rounded-md"
data-controller="notification-demo">
<div
class="absolute inset-0 bg-gradient-to-r from-blue-600 to-indigo-600 group-hover:from-blue-700 group-hover:to-indigo-700 transition-all duration-300"></div>
<div
class="absolute inset-0 opacity-10 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22%23ffffff%22%20fill-opacity%3D%221%22%20fill-rule%3D%22evenodd%22%3E%3Ccircle%20cx%3D%223%22%20cy%3D%223%22%20r%3D%221%22%2F%3E%3Ccircle%20cx%3D%2213%22%20cy%3D%2213%22%20r%3D%221%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E')]"></div>
<div class="relative px-5 py-3 flex items-center justify-center text-white font-medium">
<span class="flex items-center space-x-2">
<span>Launch Notification</span>
<i class="fas fa-arrow-right transform group-hover:translate-x-1 transition-transform ml-2"></i>
</span>
</div>
<div
class="absolute top-0 -inset-full h-full w-1/2 block transform -skew-x-12 bg-gradient-to-r from-transparent to-white opacity-20 group-hover:animate-[shine_1s_forwards]"></div>
</button>
</div>
</div>
<!-- Right panel: Code Preview (wider) -->
<div class="w-full lg:w-3/5">
<div class="border-b border-gray-200 px-6 py-2 flex items-center justify-between bg-gray-50">
<div class="text-sm font-medium text-gray-700">Generated Code</div>
<div class="text-xs text-gray-500">User: <span class="font-medium">yoeunes</span></div>
</div>
<!-- Code tabs with FontAwesome icons -->
<div class="flex border-b border-gray-200 bg-gray-50 overflow-x-auto">
<button id="laravel-tab"
class="code-tab-btn px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-500 flex items-center space-x-1.5"
data-tab="laravel">
<i class="fab fa-laravel text-red-500"></i>
<span>Laravel</span>
</button>
<button id="symfony-tab"
class="code-tab-btn px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center space-x-1.5"
data-tab="symfony">
<i class="fab fa-symfony text-black"></i>
<span>Symfony</span>
</button>
<button id="js-tab"
class="code-tab-btn px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center space-x-1.5"
data-tab="js">
<i class="fab fa-js text-yellow-400"></i>
<span>JavaScript</span>
</button>
</div>
<!-- Laravel Code Panel -->
<div id="laravel-code-panel" class="p-4 bg-gray-50 overflow-y-auto code-panel active"
style="height: calc(100vh - 15rem); max-height: 700px;">
<pre class="language-php"><code class="code-block">
// app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Http\Requests\StoreProductRequest;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function store(StoreProductRequest $request)
{
// Create new product
$product = Product::create($request->validated());
<span class="flash-code-block">flash()->success('Your product has been created successfully!');</span>
return redirect()->route('products.index');
}
}</code></pre>
</div>
<!-- Symfony Code Panel -->
<div id="symfony-code-panel" class="p-4 bg-gray-50 overflow-y-auto hidden code-panel"
style="height: calc(100vh - 15rem); max-height: 700px;">
<pre class="language-php"><code class="code-block">
// src/Controller/ProductController.php
namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products/new', name: 'app_product_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $em): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($product);
$em->flush();
<span class="flash-code-block">flash()->success('Your product has been created successfully!');</span>
return $this->redirectToRoute('app_product_index');
}
return $this->render('product/new.html.twig', [
'product' => $product,
'form' => $form,
]);
}
}</code></pre>
</div>
<!-- JavaScript Code Panel -->
<div id="js-code-panel" class="p-4 bg-gray-50 overflow-y-auto hidden code-panel"
style="height: calc(100vh - 15rem); max-height: 700px;">
<pre class="language-javascript"><code class="code-block">
// product-service.js
import flasher from '@flasher/flasher';
async function createProduct(productData) {
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(productData)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Failed to create product');
}
<span class="flash-code-block">flasher.success('Your product has been created successfully!');</span>
return result;
} catch (error) {
flasher.error('Error: ' + error.message);
throw error;
}
}</code></pre>
</div>
</div>
</div>
<!-- Additional interactive elements for demos -->
<div class="px-6 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
<div class="text-sm text-gray-500"></div>
<div class="flex space-x-4">
<a href="https://phpflasher.com/docs"
class="text-xs text-gray-600 hover:text-blue-600 transition-colors flex items-center">
<i class="fas fa-book mr-1.5 text-xs"></i>
Documentation
</a>
<a href="https://github.com/php-flasher/php-flasher"
class="text-xs text-gray-600 hover:text-blue-600 transition-colors flex items-center">
<i class="fab fa-github mr-1.5 text-xs"></i>
GitHub
</a>
</div>
</div>
</div>
</div>
</section>
<style>
/* Custom styles for highlighting code */
pre {
position: relative;
}
.code-block {
display: block;
position: relative;
font-size: 0.875rem;
}
/* Highlight for the flash method lines */
.flash-code-block {
display: inline-block;
width: 100%;
background-color: rgba(16, 185, 129, 0.1);
border-radius: 4px;
font-weight: 600;
box-shadow: 0 0 2px rgba(16, 185, 129, 0.3);
padding: 2px 0;
}
/* Animation keyframes */
@keyframes shine {
to {
left: 100%;
}
}
@keyframes progress {
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
}
@keyframes ping {
0% {
transform: scale(1);
opacity: 1;
}
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
/* Additional styles for the notification types buttons */
.type-button {
transition: all 0.2s ease;
}
.type-button:hover {
transform: translateY(-2px);
}
/* Additional animations for the PHPFlasher Interactive Studio */
/* Typing animation */
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
/* Code highlight pulse */
@keyframes codePulse {
0% { background-color: rgba(79, 70, 229, 0.0); }
50% { background-color: rgba(79, 70, 229, 0.1); }
100% { background-color: rgba(79, 70, 229, 0.0); }
}
/* Shining effect for buttons */
@keyframes shine {
0% { left: -100%; }
100% { left: 100%; }
}
/* Cursor blink effect */
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Line highlight effect */
.highlight-line {
background-color: rgba(16, 185, 129, 0.1);
display: block;
padding: 0 4px;
border-radius: 2px;
transition: background-color 0.3s ease;
}
.highlight-line:hover {
background-color: rgba(16, 185, 129, 0.2);
}
/* Typing container styles */
.typing-container {
position: relative;
white-space: nowrap;
overflow: hidden;
}
.typing-text {
animation: typing 3s steps(40, end);
}
.typing-cursor {
display: inline-block;
margin-left: 2px;
width: 2px;
height: 1em;
background: currentColor;
animation: cursorBlink 1s infinite;
}
/* Enhanced code block */
.code-block {
position: relative;
transition: all 0.2s ease-in-out;
}
.code-block:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Particle animations */
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.floating-particle {
position: absolute;
animation: float 3s ease-in-out infinite;
opacity: 0.5;
pointer-events: none;
}
</style>
<script>
/**
* PHPFlasher Interactive Studio - Main Script
*
* This enhanced script brings a real IDE-like experience to the PHPFlasher demo:
* - Shows real code templates with the flash code being typed in real-time
* - Only animates the relevant flash notification code, keeping surroundings static
* - Contextual examples based on notification type
* - Human-like typing animation with realistic speed
*
* @author PHPFlasher Team
* @version 2.1.0
* @lastUpdated 2025-03-13
*/
// Create a proxy to monitor state changes for reactive UI updates
const createStateProxy = () => {
const stateTarget = {
type: "success",
title: "",
message: "Your product has been created successfully!",
timeout: "",
position: "",
direction: "",
rtl: false,
theme: "",
escapeHtml: true,
// Track what's changed to only animate that part
_previousState: {}
};
return new Proxy(stateTarget, {
set(target, property, value) {
// Store previous value before updating
if (property !== '_previousState') {
target._previousState[property] = target[property];
}
target[property] = value;
// When state changes, update the code display
if (document.readyState === "complete" && property !== '_previousState') {
const onlyOptionsChanged =
['position', 'timeout', 'theme', 'direction', 'rtl', 'escapeHtml'].includes(property);
updateCodeDisplay(true, onlyOptionsChanged);
}
return true;
}
});
};
// Create state object with reactive properties
const state = createStateProxy();
/**
* Context-specific templates for each notification type and framework
*/
const contextTemplates = {
success: {
laravel: {
title: "Success",
message: "Your product has been created successfully!",
controller: "ProductController",
method: "store",
code: {
before: `// app/Http/Controllers/ProductController.php
namespace App\\Http\\Controllers;
use App\\Models\\Product;
use App\\Http\\Requests\\StoreProductRequest;
use Illuminate\\Http\\Request;
class ProductController extends Controller
{
public function store(StoreProductRequest $request)
{
// Create new product
$product = Product::create($request->validated());
`,
after: `
return redirect()->route('products.index');
}
}`
}
},
symfony: {
title: "Success",
message: "Your product has been created successfully!",
controller: "ProductController",
method: "new",
code: {
before: `// src/Controller/ProductController.php
namespace App\\Controller;
use App\\Entity\\Product;
use App\\Form\\ProductType;
use Doctrine\\ORM\\EntityManagerInterface;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\Request;
use Symfony\\Component\\HttpFoundation\\Response;
use Symfony\\Component\\Routing\\Attribute\\Route;
class ProductController extends AbstractController
{
#[Route('/products/new', name: 'app_product_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $em): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($product);
$em->flush();
`,
after: `
return $this->redirectToRoute('app_product_index');
}
return $this->render('product/new.html.twig', [
'product' => $product,
'form' => $form,
]);
}
}`
}
},
js: {
title: "Success",
message: "Your product has been created successfully!",
method: "createProduct",
code: {
before: `// product-service.js
import flasher from '@flasher/flasher';
async function createProduct(productData) {
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(productData)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Failed to create product');
}
`,
after: `
return result;
} catch (error) {
flasher.error('Error: ' + error.message);
throw error;
}
}`
}
}
},
error: {
laravel: {
title: "Error",
message: "An error occurred while processing your request.",
controller: "PaymentController",
method: "processPayment",
code: {
before: `// app/Http/Controllers/PaymentController.php
namespace App\\Http\\Controllers;
use App\\Services\\PaymentGateway;
use Illuminate\\Http\\Request;
class PaymentController extends Controller
{
protected $paymentGateway;
public function __construct(PaymentGateway $paymentGateway)
{
$this->paymentGateway = $paymentGateway;
}
public function processPayment(Request $request)
{
try {
$payment = $this->paymentGateway->charge($request->amount, $request->token);
} catch (\\Exception $e) {
`,
after: `
return back()->withInput();
}
return redirect()->route('order.confirmation');
}
}`
}
},
symfony: {
title: "Error",
message: "An error occurred while processing your payment.",
controller: "PaymentController",
method: "process",
code: {
before: `// src/Controller/PaymentController.php
namespace App\\Controller;
use App\\Service\\PaymentService;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\Request;
use Symfony\\Component\\HttpFoundation\\Response;
use Symfony\\Component\\Routing\\Attribute\\Route;
class PaymentController extends AbstractController
{
#[Route('/payment/process', name: 'app_payment_process', methods: ['POST'])]
public function process(Request $request, PaymentService $paymentService): Response
{
$amount = $request->get('amount');
try {
$payment = $paymentService->processPayment($request->get('token'), $amount);
} catch (\\Exception $e) {
`,
after: `
return $this->redirectToRoute('app_checkout');
}
return $this->redirectToRoute('app_confirmation');
}
}`
}
},
js: {
title: "Error",
message: "An error occurred while processing your payment.",
method: "processPayment",
code: {
before: `// payment-service.js
import flasher from '@flasher/flasher';
import { stripeClient } from './stripe-client';
async function processPayment({ paymentMethodId, amount }) {
try {
const paymentResponse = await stripeClient.createPayment({
amount: amount,
currency: 'usd',
payment_method: paymentMethodId
});
if (paymentResponse.error) {
`,
after: `
return { success: false };
}
return { success: true, payment: paymentResponse };
} catch (error) {
console.error('Payment error:', error);
flasher.error('Payment failed: ' + error.message);
return { success: false };
}
}`
}
}
},
info: {
laravel: {
title: "Information",
message: "Your order has been received and is being processed.",
controller: "OrderController",
method: "create",
code: {
before: `// app/Http/Controllers/OrderController.php
namespace App\\Http\\Controllers;
use App\\Models\\Order;
use App\\Jobs\\ProcessOrderJob;
use Illuminate\\Http\\Request;
class OrderController extends Controller
{
public function create(Request $request)
{
$cart = app(CartService::class);
// Create order
$order = Order::create([
'user_id' => auth()->id(),
'total' => $cart->total(),
'status' => 'pending'
]);
`,
after: `
// Dispatch background job
ProcessOrderJob::dispatch($order);
return redirect()->route('orders.show', $order);
}
}`
}
},
symfony: {
title: "Information",
message: "Your order has been received and is being processed.",
controller: "OrderController",
method: "create",
code: {
before: `// src/Controller/OrderController.php
namespace App\\Controller;
use App\\Entity\\Order;
use App\\Message\\ProcessOrderMessage;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\Response;
use Symfony\\Component\\Messenger\\MessageBusInterface;
use Symfony\\Component\\Routing\\Attribute\\Route;
class OrderController extends AbstractController
{
#[Route('/order/new', name: 'app_order_create', methods: ['POST'])]
public function create(CartService $cart, EntityManagerInterface $em, MessageBusInterface $messageBus): Response
{
$order = new Order();
$order->setUser($this->getUser());
$order->setTotal($cart->getTotal());
$order->setStatus('pending');
$em->persist($order);
$em->flush();
`,
after: `
// Dispatch message to queue
$messageBus->dispatch(new ProcessOrderMessage($order->getId()));
return $this->redirectToRoute('app_order_show', ['id' => $order->getId()]);
}
}`
}
},
js: {
title: "Information",
message: "Your order has been received and is being processed.",
method: "submitOrder",
code: {
before: `// order-service.js
import flasher from '@flasher/flasher';
import { analytics } from './analytics';
async function submitOrder(orderData) {
try {
// Submit order to API
const orderResponse = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
});
const order = await orderResponse.json();
`,
after: `
// Track analytics
analytics.track('Order Submitted', {
orderId: order.id,
amount: order.total
});
// Navigate to order page
navigator.push(\`/orders/\${order.id}\`);
return order;
} catch (error) {
flasher.error('Error: ' + error.message);
throw error;
}
}`
}
}
},
warning: {
laravel: {
title: "Warning",
message: "Your subscription will expire in 3 days.",
controller: "SubscriptionController",
method: "checkStatus",
code: {
before: `// app/Http/Controllers/SubscriptionController.php
namespace App\\Http\\Controllers;
use Carbon\\Carbon;
use Illuminate\\Http\\Request;
class SubscriptionController extends Controller
{
public function checkStatus(Request $request)
{
$user = auth()->user();
$subscription = $user->subscription();
$daysLeft = now()->diffInDays($subscription->expires_at, false);
if ($daysLeft <= 3 && $daysLeft > 0) {
`,
after: `
}
return view('account.subscription', compact('subscription'));
}
}`
}
},
symfony: {
title: "Warning",
message: "Your subscription will expire in 3 days.",
controller: "SubscriptionController",
method: "checkStatus",
code: {
before: `// src/Controller/SubscriptionController.php
namespace App\\Controller;
use App\\Repository\\SubscriptionRepository;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\Response;
use Symfony\\Component\\Routing\\Attribute\\Route;
class SubscriptionController extends AbstractController
{
#[Route('/account/subscription', name: 'app_subscription_status')]
public function checkStatus(SubscriptionRepository $subscriptionRepository): Response
{
$subscription = $subscriptionRepository->findByUser($this->getUser());
$daysLeft = $subscription->getExpiresAt()->diff(new \\DateTime())->days;
if ($daysLeft <= 3 && $daysLeft > 0) {
`,
after: `
}
return $this->render('account/subscription.html.twig', [
'subscription' => $subscription
]);
}
}`
}
},
js: {
title: "Warning",
message: "Your subscription will expire in 3 days.",
method: "checkSubscriptionStatus",
code: {
before: `// subscription-service.js
import flasher from '@flasher/flasher';
import moment from 'moment';
class SubscriptionService {
async checkSubscriptionStatus() {
const { subscription } = await api.getSubscriptionStatus();
const daysLeft = moment(subscription.expiresAt).diff(moment(), 'days');
if (daysLeft <= 3 && daysLeft > 0) {
`,
after: `
}
// Update UI
this.setState({
subscriptionStatus: subscription.status,
daysLeft: daysLeft
});
return subscription;
}
}`
}
}
}
};
/**
* Function to safely escape special characters in code strings
*/
function sanitizeForCode(str) {
if (typeof str !== "string") return str;
return str
.replace(/\\/g, "\\\\") // Escape backslashes
.replace(/'/g, "\\'") // Escape single quotes
.replace(/"/g, "\\\"") // Escape double quotes
.replace(/\n/g, "\\n") // Handle newlines
.replace(/\r/g, "\\r") // Handle carriage returns
.replace(/\t/g, "\\t"); // Handle tabs
}
/**
* Function to safely escape HTML for display in code blocks
*/
function escapeHtml(text) {
if (typeof text !== "string") return "";
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Updates the styling of notification type buttons
*/
function initializeTypeButtons() {
document.querySelectorAll(".type-button").forEach(btn => {
const type = btn.dataset.type;
// Clear existing styling
btn.classList.remove("border-2");
btn.classList.remove("border-green-500", "border-red-500", "border-blue-500", "border-amber-500");
// Apply styling for the current type
if (type === state.type) {
btn.classList.add("border-2");
// Add appropriate border color based on type
switch (type) {
case "success":
btn.classList.add("border-green-500");
break;
case "error":
btn.classList.add("border-red-500");
break;
case "info":
btn.classList.add("border-blue-500");
break;
case "warning":
btn.classList.add("border-amber-500");
break;
}
}
});
}
/**
* Initializes form fields with current state values
*/
function initializeFormValues() {
// Set values for all form elements based on state
const template = contextTemplates[state.type];
// Set title based on current notification type's template if empty
if (!state.title && template) {
state.title = template[getCurrentTab()].title;
}
document.getElementById("title-input").value = state.title || "";
// Set message based on type's template
document.getElementById("message-input").value = state.message || "";
// Set all the option fields
if (state.position) {
document.getElementById("position-select").value = state.position;
}
if (state.timeout) {
document.getElementById("timeout-input").value = state.timeout;
}
if (state.theme) {
document.getElementById("theme-select").value = state.theme;
}
if (state.direction) {
document.getElementById("direction-select").value = state.direction;
}
document.getElementById("rtl-check").checked = !!state.rtl;
document.getElementById("escape-html-check").checked = state.escapeHtml !== false; // Default to true
}
/**
* Updates the status indicator in the UI
*/
function updateStatus(message) {
const statusText = document.getElementById("status-text");
statusText.textContent = message;
// Temporarily change color
statusText.classList.remove("text-emerald-600");
statusText.classList.add("text-blue-600");
// Add animation to the dot
const dot = document.querySelector("#status-indicator div");
dot.classList.remove("bg-emerald-500");
dot.classList.add("bg-blue-500");
// Reset after 2 seconds
setTimeout(() => {
statusText.classList.remove("text-blue-600");
statusText.classList.add("text-emerald-600");
statusText.textContent = "Ready";
dot.classList.remove("bg-blue-500");
dot.classList.add("bg-emerald-500");
}, 2000);
}
/**
* Gets the current active tab (laravel, symfony, or js)
*/
function getCurrentTab() {
const activeTab = document.querySelector(".code-tab-btn.text-blue-600");
return activeTab ? activeTab.getAttribute("data-tab") : "laravel";
}
/**
* Switches to the specified code tab and updates the display
*/
function showCodeTab(tab) {
// Hide all panels
document.querySelectorAll(".code-panel").forEach(panel => {
panel.classList.add("hidden");
});
// Show the selected panel
const activePanel = document.getElementById(`${tab}-code-panel`);
if (activePanel) {
activePanel.classList.remove("hidden");
}
// Update tab styling
document.querySelectorAll(".code-tab-btn").forEach(btn => {
btn.classList.remove("text-blue-600", "border-b-2", "border-blue-500");
btn.classList.add("text-gray-600");
});
// Highlight active tab
const activeTab = document.querySelector(`[data-tab="${tab}"]`);
if (activeTab) {
activeTab.classList.remove("text-gray-600");
activeTab.classList.add("text-blue-600", "border-b-2", "border-blue-500");
}
// Update the code based on current state
updateCodeDisplay(true, false);
}
/**
* Generates a properly formatted flash method chain for the specified framework
*/
function generateFlashCode(framework, indent = '') {
const prefix = framework === "js" ? "flasher" : "flash()";
const safeMessage = sanitizeForCode(state.message);
const safeTitle = state.title && state.title !== state.type ? sanitizeForCode(state.title) : "";
// Create properly formatted multi-line methods
let formattedCode = [];
// First line - always the prefix
formattedCode.push(`${prefix}`);
// Add options if present
if (framework === "js") {
// JavaScript format with options as object
const options = {};
if (state.position) options.position = state.position;
if (state.timeout) options.timeout = parseInt(state.timeout);
if (state.theme) options.theme = state.theme;
if (state.direction) options.direction = state.direction;
if (state.rtl) options.rtl = true;
if (state.escapeHtml === false) options.escapeHtml = false;
// If no options, keep it simple
if (Object.keys(options).length === 0) {
formattedCode = [`${indent}${prefix}.${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''});`];
} else {
// Add the type and arguments with options
formattedCode.push(`${indent} .${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''}, {`);
// Add each option on its own line with proper indentation
Object.entries(options).forEach(([key, value], index, array) => {
const comma = index < array.length - 1 ? ',' : '';
if (typeof value === "string") {
formattedCode.push(`${indent} ${key}: '${value}'${comma}`);
} else {
formattedCode.push(`${indent} ${key}: ${value}${comma}`);
}
});
formattedCode.push(`${indent} });`);
}
} else {
// PHP frameworks with method chaining - each option on a new line
if (state.position) {
formattedCode.push(`${indent} ->option('position', '${state.position}')`);
}
if (state.timeout) {
formattedCode.push(`${indent} ->option('timeout', ${state.timeout})`);
}
if (state.theme) {
formattedCode.push(`${indent} ->option('theme', '${state.theme}')`);
}
if (state.direction) {
formattedCode.push(`${indent} ->option('direction', '${state.direction}')`);
}
if (state.rtl) {
formattedCode.push(`${indent} ->option('rtl', true)`);
}
if (state.escapeHtml === false) {
formattedCode.push(`${indent} ->option('escapeHtml', false)`);
}
// Add the final method call
formattedCode.push(`${indent} ->${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''});\n`);
}
// Join with line breaks for multi-line formatting
return formattedCode.join('\n');
}
/**
* Apply typing animation to only the flash code
*/
function animateFlashCode(codeBlock, framework) {
// Get template for the current type and framework
const template = contextTemplates[state.type][framework];
const indent = framework === "js" ? '' : ' ';
// Generate the flash code to be typed
const flashCode = generateFlashCode(framework, indent);
// Create parent container with specific visibility
const container = document.createElement('div');
container.style.position = 'relative';
// Create pre and code elements with proper classes
const pre = document.createElement('pre');
const code = document.createElement('code');
pre.className = `language-${framework === 'js' ? 'javascript' : 'php'}`;
code.className = 'code-block';
// Add the "before" part of the code
code.innerHTML = escapeHtml(template.code.before);
// Create a highly visible container for the typing animation
const flashContainer = document.createElement('div');
flashContainer.className = 'flash-code-typing';
flashContainer.style.position = 'relative';
flashContainer.style.display = 'block';
flashContainer.style.backgroundColor = 'rgba(16, 185, 129, 0.1)';
flashContainer.style.borderRadius = '4px';
flashContainer.style.padding = '4px';
flashContainer.style.marginLeft = '0';
flashContainer.style.marginRight = '0';
flashContainer.style.marginTop = '10px';
flashContainer.style.marginBottom = '10px';
flashContainer.style.boxShadow = '0 0 3px rgba(16, 185, 129, 0.5)';
flashContainer.style.borderLeft = '3px solid rgba(16, 185, 129, 0.5)';
// Create a visible cursor
const cursor = document.createElement('span');
cursor.className = 'typing-cursor';
cursor.textContent = '|';
cursor.style.display = 'inline-block';
cursor.style.animation = 'cursorBlink 1s infinite';
cursor.style.color = '#4F46E5';
cursor.style.fontWeight = 'bold';
cursor.style.marginLeft = '1px';
cursor.style.fontSize = '1.1em';
// Add to DOM
flashContainer.appendChild(cursor);
pre.appendChild(code);
container.appendChild(pre);
// Clear existing content and add our new structure
codeBlock.innerHTML = '';
codeBlock.appendChild(container);
// Add the flash container at the right position
code.appendChild(flashContainer);
// Add the "after" part of the code
const afterSpan = document.createElement('span');
afterSpan.innerHTML = escapeHtml(template.code.after);
code.appendChild(afterSpan);
// Apply syntax highlighting to the static parts
if (typeof Prism !== "undefined") {
Prism.highlightElement(code);
}
// More obvious typing effect with slightly slower speed
let i = 0;
let currentText = '';
const typingSpeed = 35; // milliseconds per character
// Function to type with human-like pacing
const typeWithHumanPace = () => {
if (i < flashCode.length) {
// Add one character at a time
const char = flashCode.charAt(i);
currentText += char;
// Create content span with proper escaping
const contentSpan = document.createElement('span');
contentSpan.innerHTML = escapeHtml(currentText);
contentSpan.style.color = '#000'; // Ensure text is visible
// Update the flash container
flashContainer.innerHTML = '';
flashContainer.appendChild(contentSpan);
flashContainer.appendChild(cursor);
// Progress the animation
i++;
// Variable typing speed for realistic effect
let nextDelay = typingSpeed;
if ([';', '.', ',', ':', ')', '}'].includes(char)) {
nextDelay = typingSpeed * 3; // Pause at punctuation
} else if (char === '\n') {
nextDelay = typingSpeed * 5; // Longer pause at line breaks
} else if (Math.random() < 0.1) {
nextDelay = typingSpeed * (1 + Math.random()); // Random pauses
}
// Continue typing
setTimeout(typeWithHumanPace, nextDelay);
} else {
// When done, add a subtle highlight effect
flashContainer.style.transition = 'background-color 1s ease';
flashContainer.style.backgroundColor = 'rgba(16, 185, 129, 0.15)';
}
};
// Start typing after a visible delay
setTimeout(() => {
// Pulse animation to draw attention
flashContainer.style.animation = 'codePulse 1s';
// Start typing
setTimeout(typeWithHumanPace, 300);
}, 300);
}
/**
* Updates code display with option to animate only the flash code
*/
function updateCodeDisplay(animate = false, onlyOptionsChanged = false) {
const currentTab = getCurrentTab();
// Get the panel for the current tab
const panelId = `${currentTab}-code-panel`;
const panel = document.getElementById(panelId);
if (!panel) return;
// Get the template and generate the flash code
const template = contextTemplates[state.type][currentTab];
const indent = currentTab === "js" ? '' : ' ';
const flashCode = generateFlashCode(currentTab, indent);
// Create the full code by concatenating the parts
const fullCode =
template.code.before +
flashCode +
template.code.after;
// Set the HTML directly with the complete code
// This allows Prism to do its highlighting on the complete code
panel.innerHTML = `<pre class="language-${currentTab === 'js' ? 'javascript' : 'php'}"><code class="code-block">${escapeHtml(fullCode)}</code></pre>`;
// Apply syntax highlighting
if (typeof Prism !== "undefined") {
Prism.highlightElement(panel.querySelector('code'));
}
}
/**
* Shows the notification using the Flasher library
*/
function showNotification() {
// Create a clean options object with only the necessary properties
const options = {};
// Only add properties that have values
if (state.position) options.position = state.position;
if (state.timeout) options.timeout = parseInt(state.timeout);
if (state.theme) options.theme = state.theme;
if (state.direction) options.direction = state.direction;
if (state.rtl) options.rtl = state.rtl;
if (state.escapeHtml !== undefined) options.escapeHtml = state.escapeHtml;
try {
// If the flasher library is available, use it
if (typeof flasher !== "undefined") {
if (state.theme) {
// When using a specific theme
const notificationOptions = {
type: state.type,
message: state.message,
title: state.title || undefined,
...options
};
delete notificationOptions.theme; // Remove theme from options as we're using it directly
flasher.use(state.theme).flash(notificationOptions);
} else {
// Standard approach
flasher[state.type](state.message, state.title || null, options);
}
// Show success status
updateStatus("Notification shown successfully!");
// Add a subtle ripple effect to the button
const btn = document.getElementById("show-notification-btn");
btn.classList.add("animate-pulse");
setTimeout(() => btn.classList.remove("animate-pulse"), 1000);
} else {
// Fallback to alert if flasher isn't available
let optionsText = "";
for (const [key, value] of Object.entries(options)) {
if (!["type", "message", "title"].includes(key)) {
optionsText += `\n${key}: ${JSON.stringify(value)}`;
}
}
alert("Notification would show with these settings:\n\n" +
`Type: ${state.type}\n` +
`Message: ${state.message}\n` +
(state.title ? `Title: ${state.title}\n` : "")
`Type: ${state.type}\n` +
`Message: ${state.message}\n` +
(state.title ? `Title: ${state.title}\n` : "") +
(optionsText ? "\nOptions:" + optionsText : "")
);
}
} catch (e) {
console.error("Error showing notification:", e);
updateStatus("Error: " + e.message);
alert("Failed to show notification: " + e.message);
}
}
/**
* Updates the message and title inputs based on the selected notification type
*/
function updateContentForType(type) {
const currentTab = getCurrentTab();
const template = contextTemplates[type][currentTab];
// Update message and title inputs
if (template) {
document.getElementById("message-input").value = template.message;
state.message = template.message;
// Only update title if we have one in the template
if (template.title) {
document.getElementById("title-input").value = template.title;
state.title = template.title;
}
}
}
/**
* Debug helper function for troubleshooting
*/
function debugState() {
console.log("Current state:", { ...state });
console.log("Form values:");
console.log("- Title:", document.getElementById("title-input").value);
console.log("- Message:", document.getElementById("message-input").value);
console.log("- Position:", document.getElementById("position-select").value);
console.log("- Timeout:", document.getElementById("timeout-input").value);
console.log("- Theme:", document.getElementById("theme-select").value);
console.log("- Direction:", document.getElementById("direction-select").value);
console.log("- RTL:", document.getElementById("rtl-check").checked);
console.log("- Escape HTML:", document.getElementById("escape-html-check").checked);
console.log("- Current tab:", getCurrentTab());
}
// Add to window for console access
window.debugPhpFlasher = debugState;
/**
* Initialize the application when the DOM is fully loaded
*/
document.addEventListener("DOMContentLoaded", function() {
// Initialize form with current state values
initializeFormValues();
// Initialize type buttons
initializeTypeButtons();
// Set up type button event listeners
document.querySelectorAll(".type-button").forEach(btn => {
btn.addEventListener("click", () => {
const type = btn.dataset.type;
state.type = type;
// Update message and title based on contextual templates
updateContentForType(type);
// Update button styling
initializeTypeButtons();
// Update code display with animation
updateCodeDisplay(true);
// Update status
updateStatus(`Type changed to: ${state.type}`);
});
});
// Input event listeners
document.getElementById("title-input").addEventListener("input", (e) => {
state.title = e.target.value.trim();
updateCodeDisplay(true);
});
document.getElementById("message-input").addEventListener("input", (e) => {
state.message = e.target.value;
updateCodeDisplay(true);
});
// Select event listeners with animation
document.getElementById("position-select").addEventListener("change", (e) => {
state.position = e.target.value;
updateCodeDisplay(true, true);
});
document.getElementById("timeout-input").addEventListener("change", (e) => {
state.timeout = e.target.value ? parseInt(e.target.value) : "";
updateCodeDisplay(true, true);
});
document.getElementById("theme-select").addEventListener("change", (e) => {
state.theme = e.target.value;
updateCodeDisplay(true, true);
});
document.getElementById("direction-select").addEventListener("change", (e) => {
state.direction = e.target.value;
updateCodeDisplay(true, true);
});
// Checkbox event listeners
document.getElementById("rtl-check").addEventListener("change", (e) => {
state.rtl = e.target.checked;
updateCodeDisplay(true, true);
});
document.getElementById("escape-html-check").addEventListener("change", (e) => {
state.escapeHtml = e.target.checked;
updateCodeDisplay(true, true);
});
// Initialize code tabs with animation
document.querySelectorAll(".code-tab-btn").forEach(button => {
button.addEventListener("click", () => {
const tab = button.getAttribute("data-tab");
showCodeTab(tab);
// Update content based on the current state type
updateContentForType(state.type);
});
});
// Show notification button with enhanced feedback
const notificationBtn = document.getElementById("show-notification-btn");
notificationBtn.addEventListener("click", () => {
// Add a subtle press effect
notificationBtn.classList.add("scale-95");
setTimeout(() => notificationBtn.classList.remove("scale-95"), 150);
updateStatus("Launching notification...");
showNotification();
});
// Add hover shine effect to button
notificationBtn.addEventListener("mouseenter", () => {
const shineElement = notificationBtn.querySelector("div:last-child");
if (shineElement) {
shineElement.style.animation = "none";
setTimeout(() => {
shineElement.style.animation = "shine 1s forwards";
}, 10);
}
});
// Easter egg: Konami code
let konamiCode = [];
const konamiSequence = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown",
"ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
document.addEventListener("keydown", function(e) {
konamiCode.push(e.key);
konamiCode = konamiCode.slice(-konamiSequence.length);
if (konamiCode.join(",") === konamiSequence.join(",")) {
if (typeof flasher !== "undefined") {
flasher.use("neon").success("You found the Konami code! 🎮", "Secret Unlocked");
}
}
});
// Add current user info - nice personal touch
const userInfo = document.querySelector(".text-xs.text-gray-500");
if (userInfo) {
const today = new Date();
const formattedDate = today.toISOString().slice(0, 19).replace('T', ' ');
userInfo.innerHTML = `User: <span class="font-medium">yoeunes</span> • Last active: ${formattedDate}`;
}
// Add special message to console
console.info("%c✨ PHPFlasher Interactive Studio v2.1.0 ✨",
"color: #4F46E5; font-size: 14px; font-weight: bold; text-shadow: 1px 1px 2px rgba(0,0,0,0.1);");
console.info("%cTry the Konami code for a surprise!", "color: #10B981; font-style: italic;");
// Show the initial tab
showCodeTab("laravel");
// Initial code display update with animation after a small delay
setTimeout(() => {
updateCodeDisplay(true, false);
}, 300);
});
</script>