mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 23:17:47 +01:00
1491 lines
63 KiB
HTML
1491 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 script powers the interactive notification demo that allows users to:
|
|
* - Select notification types (success, error, info, warning)
|
|
* - Customize notification content and options
|
|
* - See real-time code examples for Laravel, Symfony, and JavaScript
|
|
* - View typing animation effects for code generation
|
|
*
|
|
* @author PHPFlasher Team
|
|
* @version 2.0.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
|
|
};
|
|
|
|
return new Proxy(stateTarget, {
|
|
set(target, property, value) {
|
|
target[property] = value;
|
|
|
|
// When state changes, update the code display
|
|
if (document.readyState === "complete") {
|
|
updateCodeDisplay(true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Create state object with reactive properties
|
|
const state = createStateProxy();
|
|
|
|
/**
|
|
* Context-specific templates for each notification type and framework
|
|
*
|
|
* Each template contains:
|
|
* - title: Default title for the notification
|
|
* - message: Default message for the notification
|
|
* - controller: The controller name and context
|
|
* - method: The method name that would trigger this notification
|
|
* - code: Framework-specific code surrounding the notification
|
|
*/
|
|
const contextTemplates = {
|
|
success: {
|
|
laravel: {
|
|
title: "Success",
|
|
message: "Your product has been created successfully!",
|
|
controller: "ProductController",
|
|
method: "store",
|
|
code: {
|
|
before: `// Create new product\n$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: `$em->persist($product);\n$em->flush();`,
|
|
after: `return $this->redirectToRoute('app_product_index');`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Success",
|
|
message: "Your product has been created successfully!",
|
|
method: "createProduct",
|
|
code: {
|
|
before: `const result = await response.json();\n\nif (!response.ok) {\n throw new Error(result.message || 'Failed to create product');\n}`,
|
|
after: `return result;`
|
|
}
|
|
}
|
|
},
|
|
error: {
|
|
laravel: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your request.",
|
|
controller: "PaymentController",
|
|
method: "processPayment",
|
|
code: {
|
|
before: `try {\n $payment = $this->paymentGateway->charge($request->amount, $request->token);\n} catch (\\Exception $e) {`,
|
|
after: ` return back()->withInput();\n}`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your payment.",
|
|
controller: "PaymentController",
|
|
method: "process",
|
|
code: {
|
|
before: `try {\n $payment = $this->paymentService->processPayment($request->get('token'), $amount);\n} catch (\\Exception $e) {`,
|
|
after: ` return $this->redirectToRoute('app_checkout');\n}`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your payment.",
|
|
method: "processPayment",
|
|
code: {
|
|
before: `try {\n const paymentResponse = await stripeClient.createPayment({\n amount: amount,\n currency: 'usd',\n payment_method: paymentMethodId\n });\n\n if (paymentResponse.error) {`,
|
|
after: ` return { success: false };\n }\n} catch (error) {\n console.error('Payment error:', error);\n flasher.error('Payment failed: ' + error.message);\n return { success: false };\n}`
|
|
}
|
|
}
|
|
},
|
|
info: {
|
|
laravel: {
|
|
title: "Information",
|
|
message: "Your order has been received and is being processed.",
|
|
controller: "OrderController",
|
|
method: "create",
|
|
code: {
|
|
before: `// Create order\n$order = Order::create([\n 'user_id' => auth()->id(),\n 'total' => $cart->total(),\n 'status' => 'pending'\n]);`,
|
|
after: `// Dispatch background job\nProcessOrderJob::dispatch($order);\n\nreturn redirect()->route('orders.show', $order);`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Information",
|
|
message: "Your order has been received and is being processed.",
|
|
controller: "OrderController",
|
|
method: "create",
|
|
code: {
|
|
before: `$order = new Order();\n$order->setUser($this->getUser());\n$order->setTotal($cart->getTotal());\n$order->setStatus('pending');\n\n$em->persist($order);\n$em->flush();`,
|
|
after: `// Dispatch message to queue\n$this->messageBus->dispatch(new ProcessOrderMessage($order->getId()));\n\nreturn $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: `// Submit order to API\nconst orderResponse = await fetch('/api/orders', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(orderData)\n});\n\nconst order = await orderResponse.json();`,
|
|
after: `// Track analytics\nanalytics.track('Order Submitted', {\n orderId: order.id,\n amount: order.total\n});\n\n// Navigate to order page\nnavigator.push(\`/orders/\${order.id}\`);`
|
|
}
|
|
}
|
|
},
|
|
warning: {
|
|
laravel: {
|
|
title: "Warning",
|
|
message: "Your subscription will expire in 3 days.",
|
|
controller: "SubscriptionController",
|
|
method: "checkStatus",
|
|
code: {
|
|
before: `$subscription = $user->subscription();\n$daysLeft = now()->diffInDays($subscription->expires_at, false);\n\nif ($daysLeft <= 3 && $daysLeft > 0) {`,
|
|
after: `}\n\nreturn view('account.subscription', compact('subscription'));`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Warning",
|
|
message: "Your subscription will expire in 3 days.",
|
|
controller: "SubscriptionController",
|
|
method: "checkStatus",
|
|
code: {
|
|
before: `$subscription = $subscriptionRepository->findByUser($this->getUser());\n$daysLeft = $subscription->getExpiresAt()->diff(new \\DateTime())->days;\n\nif ($daysLeft <= 3 && $daysLeft > 0) {`,
|
|
after: `}\n\nreturn $this->render('account/subscription.html.twig', [\n 'subscription' => $subscription\n]);`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Warning",
|
|
message: "Your subscription will expire in 3 days.",
|
|
method: "checkSubscriptionStatus",
|
|
code: {
|
|
before: `const { subscription } = await api.getSubscriptionStatus();\nconst daysLeft = moment(subscription.expiresAt).diff(moment(), 'days');\n\nif (daysLeft <= 3 && daysLeft > 0) {`,
|
|
after: `}\n\n// Update UI\nthis.setState({\n subscriptionStatus: subscription.status,\n daysLeft: daysLeft\n});`
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function to safely escape special characters in code strings
|
|
* Ensures that quotes and backslashes are properly displayed in code examples
|
|
*/
|
|
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
|
|
* Prevents XSS and ensures proper rendering of code
|
|
*/
|
|
function escapeHtml(text) {
|
|
if (typeof text !== "string") return "";
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Updates the styling of notification type buttons
|
|
* Highlights the currently selected type
|
|
*/
|
|
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
|
|
* Sets all inputs, selects, and checkboxes to match the state object
|
|
*/
|
|
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 if it matches a template message
|
|
const currentMessage = document.getElementById("message-input").value;
|
|
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
|
|
* Temporarily changes color and then reverts back to "Ready" state
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Generates a properly formatted flash method chain for the specified framework
|
|
* Handles proper multi-line formatting and indentation
|
|
*/
|
|
function generateFlashCode(framework, indent = '') {
|
|
const prefix = framework === "js" ? "flasher" : "flash()";
|
|
const safeMessage = sanitizeForCode(state.message);
|
|
const safeTitle = state.title ? sanitizeForCode(state.title) : "";
|
|
|
|
// Start with the base prefix
|
|
let code = prefix;
|
|
let hasOptions = false;
|
|
|
|
// Indentation rules
|
|
const baseIndent = framework === "js" ? indent + ' ' : indent + ' ';
|
|
|
|
// Create properly formatted multi-line methods
|
|
let formattedCode = [];
|
|
formattedCode.push(prefix);
|
|
|
|
// Add options if present
|
|
if (state.position || state.timeout || state.theme || state.direction ||
|
|
state.rtl || state.escapeHtml === false) {
|
|
hasOptions = true;
|
|
}
|
|
|
|
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;
|
|
|
|
// For JS, all goes on one line if no options
|
|
if (Object.keys(options).length === 0) {
|
|
let jsCode = `${prefix}.${state.type}('${safeMessage}'`;
|
|
if (safeTitle) jsCode += `, '${safeTitle}'`;
|
|
jsCode += ");";
|
|
return jsCode;
|
|
}
|
|
|
|
// Add the type and arguments
|
|
formattedCode.push(`${baseIndent}.${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''}, {`);
|
|
|
|
// Add each option on its own line with proper indentation
|
|
const optionsLines = [];
|
|
Object.entries(options).forEach(([key, value]) => {
|
|
if (typeof value === "string") {
|
|
optionsLines.push(`${baseIndent} ${key}: '${value}'`);
|
|
} else {
|
|
optionsLines.push(`${baseIndent} ${key}: ${value}`);
|
|
}
|
|
});
|
|
|
|
formattedCode = formattedCode.concat(optionsLines);
|
|
formattedCode.push(`${baseIndent}});`);
|
|
} else {
|
|
// PHP frameworks with method chaining - each option on a new line
|
|
if (state.position) {
|
|
formattedCode.push(`${baseIndent}->option('position', '${state.position}')`);
|
|
}
|
|
if (state.timeout) {
|
|
formattedCode.push(`${baseIndent}->option('timeout', ${state.timeout})`);
|
|
}
|
|
if (state.theme) {
|
|
formattedCode.push(`${baseIndent}->option('theme', '${state.theme}')`);
|
|
}
|
|
if (state.direction) {
|
|
formattedCode.push(`${baseIndent}->option('direction', '${state.direction}')`);
|
|
}
|
|
if (state.rtl) {
|
|
formattedCode.push(`${baseIndent}->option('rtl', true)`);
|
|
}
|
|
if (state.escapeHtml === false) {
|
|
formattedCode.push(`${baseIndent}->option('escapeHtml', false)`);
|
|
}
|
|
|
|
// Add the final method call
|
|
const finalLine = `${baseIndent}->${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''});`;
|
|
formattedCode.push(finalLine);
|
|
}
|
|
|
|
// Join with line breaks for multi-line formatting
|
|
return formattedCode.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Generates complete, contextual code examples for each framework
|
|
* Uses templates that change based on the notification type
|
|
*/
|
|
function generateCodeBlock(framework) {
|
|
// Get the template for the current notification type and framework
|
|
const template = contextTemplates[state.type][framework];
|
|
const indent = framework === "js" ? ' ' : ' ';
|
|
const flashCode = generateFlashCode(framework, indent);
|
|
|
|
// Template for each framework
|
|
let codeTemplate = "";
|
|
|
|
if (framework === "laravel") {
|
|
codeTemplate = `// app/Http/Controllers/${template.controller}.php
|
|
|
|
namespace App\\Http\\Controllers;
|
|
|
|
use App\\Models\\Product;
|
|
use App\\Http\\Requests\\StoreProductRequest;
|
|
use Illuminate\\Http\\Request;
|
|
|
|
class ${template.controller} extends Controller
|
|
{
|
|
public function ${template.method}(Request $request)
|
|
{
|
|
${template.code.before}
|
|
|
|
${flashCode}
|
|
|
|
${template.code.after}
|
|
}
|
|
}`;
|
|
} else if (framework === "symfony") {
|
|
codeTemplate = `// src/Controller/${template.controller}.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 ${template.controller} extends AbstractController
|
|
{
|
|
#[Route('/${template.method}', name: 'app_${template.method}', methods: ['GET', 'POST'])]
|
|
public function ${template.method}(Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
${template.code.before}
|
|
|
|
${flashCode}
|
|
|
|
${template.code.after}
|
|
}
|
|
}`;
|
|
} else { // JavaScript
|
|
codeTemplate = `// services/${template.method}.js
|
|
|
|
import flasher from '@flasher/flasher';
|
|
|
|
async function ${template.method}(data) {
|
|
try {
|
|
${template.code.before}
|
|
|
|
${flashCode}
|
|
|
|
${template.code.after}
|
|
} catch (error) {
|
|
flasher.error('Error: ' + error.message);
|
|
throw error;
|
|
}
|
|
}`;
|
|
}
|
|
|
|
return codeTemplate;
|
|
}
|
|
|
|
/**
|
|
* Apply typing animation to flash code
|
|
* Simulates real-time human typing for flash code blocks
|
|
*/
|
|
function animateFlashCode(codeBlock, flashCode, fullCode) {
|
|
// Create container for the pre and code elements
|
|
const container = document.createElement('div');
|
|
|
|
// Create temporary pre and code elements
|
|
const pre = document.createElement('pre');
|
|
const code = document.createElement('code');
|
|
|
|
// Set appropriate classes
|
|
pre.className = 'language-' + (codeBlock.id.includes('js') ? 'javascript' : 'php');
|
|
code.className = 'code-block';
|
|
|
|
// Append them to the container
|
|
pre.appendChild(code);
|
|
container.appendChild(pre);
|
|
|
|
// Get the placeholder for the flash code in the full code
|
|
const flashRegex = /(\s*)flash\(\)[\s\S]*?;|(\s*)flasher[\s\S]*?;/m;
|
|
|
|
// Split the code at the flash part
|
|
const parts = fullCode.split(flashRegex);
|
|
|
|
// Find non-empty parts (before and after flash code)
|
|
const beforeFlash = parts.filter((part, i) => i < parts.length - 1 && part && !part.match(/^\s*$/)).join('');
|
|
const afterFlash = parts.filter((part, i) => i > 0 && part && !part.match(/^\s*$/)).pop();
|
|
|
|
// Set initial content to be everything before the flash code
|
|
code.textContent = beforeFlash;
|
|
codeBlock.innerHTML = '';
|
|
codeBlock.appendChild(container);
|
|
|
|
// Highlight the initial code
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(code);
|
|
}
|
|
|
|
// Prepare for typing animation
|
|
let i = 0;
|
|
const typingSpeed = 30; // milliseconds per character
|
|
let currentText = beforeFlash;
|
|
|
|
// Start typing animation
|
|
const typeWriter = () => {
|
|
if (i < flashCode.length) {
|
|
// Add one character at a time
|
|
currentText += flashCode.charAt(i);
|
|
code.textContent = currentText;
|
|
|
|
// Re-highlight the code
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(code);
|
|
}
|
|
|
|
i++;
|
|
setTimeout(typeWriter, typingSpeed);
|
|
} else {
|
|
// Typing complete, add the rest of the code
|
|
currentText += afterFlash;
|
|
code.textContent = currentText;
|
|
|
|
// Final highlighting
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(code);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Start the typing animation after a small delay
|
|
setTimeout(typeWriter, 100);
|
|
}
|
|
|
|
/**
|
|
* Update code display in all panels
|
|
* Optionally applies typing animation effect
|
|
*/
|
|
function updateCodeDisplay(animate = false) {
|
|
const currentTab = getCurrentTab();
|
|
|
|
// Generate complete code blocks
|
|
const laravelCode = generateCodeBlock("laravel");
|
|
const symfonyCode = generateCodeBlock("symfony");
|
|
const jsCode = generateCodeBlock("js");
|
|
|
|
// Get panels
|
|
const laravelPanel = document.getElementById("laravel-code-panel");
|
|
const symfonyPanel = document.getElementById("symfony-code-panel");
|
|
const jsPanel = document.getElementById("js-code-panel");
|
|
|
|
// Extract just the flash code for animation
|
|
const getFlashCodePart = (framework) => {
|
|
const fullCode = generateCodeBlock(framework);
|
|
const flashRegex = /(\s*)flash\(\)[\s\S]*?;|(\s*)flasher[\s\S]*?;/m;
|
|
const match = fullCode.match(flashRegex);
|
|
return match ? match[0] : "";
|
|
};
|
|
|
|
// Update each panel with animation if requested
|
|
if (animate) {
|
|
// For Laravel panel
|
|
if (laravelPanel && currentTab === 'laravel') {
|
|
const flashCode = getFlashCodePart("laravel");
|
|
animateFlashCode(laravelPanel, flashCode, laravelCode);
|
|
} else if (laravelPanel) {
|
|
laravelPanel.innerHTML = `<pre class="language-php"><code class="code-block">${escapeHtml(laravelCode)}</code></pre>`;
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(laravelPanel.querySelector('code'));
|
|
}
|
|
}
|
|
|
|
// For Symfony panel
|
|
if (symfonyPanel && currentTab === 'symfony') {
|
|
const flashCode = getFlashCodePart("symfony");
|
|
animateFlashCode(symfonyPanel, flashCode, symfonyCode);
|
|
} else if (symfonyPanel) {
|
|
symfonyPanel.innerHTML = `<pre class="language-php"><code class="code-block">${escapeHtml(symfonyCode)}</code></pre>`;
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(symfonyPanel.querySelector('code'));
|
|
}
|
|
}
|
|
|
|
// For JavaScript panel
|
|
if (jsPanel && currentTab === 'js') {
|
|
const flashCode = getFlashCodePart("js");
|
|
animateFlashCode(jsPanel, flashCode, jsCode);
|
|
} else if (jsPanel) {
|
|
jsPanel.innerHTML = `<pre class="language-javascript"><code class="code-block">${escapeHtml(jsCode)}</code></pre>`;
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(jsPanel.querySelector('code'));
|
|
}
|
|
}
|
|
} else {
|
|
// Simple update without animation
|
|
if (laravelPanel) {
|
|
laravelPanel.innerHTML = `<pre class="language-php"><code class="code-block">${escapeHtml(laravelCode)}</code></pre>`;
|
|
}
|
|
|
|
if (symfonyPanel) {
|
|
symfonyPanel.innerHTML = `<pre class="language-php"><code class="code-block">${escapeHtml(symfonyCode)}</code></pre>`;
|
|
}
|
|
|
|
if (jsPanel) {
|
|
jsPanel.innerHTML = `<pre class="language-javascript"><code class="code-block">${escapeHtml(jsCode)}</code></pre>`;
|
|
}
|
|
|
|
// Re-initialize syntax highlighting
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows the notification using the Flasher library
|
|
* Falls back to a browser alert if Flasher isn't available
|
|
*/
|
|
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` : "") +
|
|
(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
|
|
* Uses real-world examples from the contextTemplates
|
|
*/
|
|
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 the state
|
|
* Can be called from the browser console
|
|
*/
|
|
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);
|
|
}
|
|
|
|
// Add this to window for easy access from console
|
|
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();
|
|
});
|
|
|
|
document.getElementById("message-input").addEventListener("input", (e) => {
|
|
state.message = e.target.value;
|
|
updateCodeDisplay();
|
|
});
|
|
|
|
// Select event listeners
|
|
document.getElementById("position-select").addEventListener("change", (e) => {
|
|
state.position = e.target.value;
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
document.getElementById("timeout-input").addEventListener("change", (e) => {
|
|
state.timeout = e.target.value ? parseInt(e.target.value) : "";
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
document.getElementById("theme-select").addEventListener("change", (e) => {
|
|
state.theme = e.target.value;
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
document.getElementById("direction-select").addEventListener("change", (e) => {
|
|
state.direction = e.target.value;
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
// Checkbox event listeners
|
|
document.getElementById("rtl-check").addEventListener("change", (e) => {
|
|
state.rtl = e.target.checked;
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
document.getElementById("escape-html-check").addEventListener("change", (e) => {
|
|
state.escapeHtml = e.target.checked;
|
|
updateCodeDisplay(true);
|
|
});
|
|
|
|
// Initialize code tabs with enhanced animation
|
|
document.querySelectorAll(".code-tab-btn").forEach(button => {
|
|
button.addEventListener("click", () => {
|
|
const tab = button.getAttribute("data-tab");
|
|
showCodeTab(tab);
|
|
|
|
// When changing tabs, update content based on the current state type
|
|
// This ensures each tab shows appropriate context for the notification 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 to show a special notification
|
|
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(",")) {
|
|
// Show special Easter egg notification
|
|
if (typeof flasher !== "undefined") {
|
|
flasher.use("neon").success("You found the Konami code! 🎮", "Secret Unlocked");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add special message to console
|
|
console.info("%c✨ PHPFlasher Interactive Studio v2.0 ✨", "color: #4F46E5; font-size: 14px; font-weight: bold;");
|
|
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
|
|
setTimeout(() => {
|
|
updateCodeDisplay(true);
|
|
}, 500);
|
|
});
|
|
|
|
/**
|
|
* Special feature: Code animator
|
|
* Add subtle animations to the code display to make it more engaging
|
|
*/
|
|
class CodeAnimator {
|
|
static highlight(codeElement) {
|
|
if (!codeElement) return;
|
|
|
|
// Add a subtle flash effect to the element
|
|
codeElement.style.transition = "background-color 0.5s ease";
|
|
codeElement.style.backgroundColor = "rgba(79, 70, 229, 0.1)";
|
|
|
|
setTimeout(() => {
|
|
codeElement.style.backgroundColor = "transparent";
|
|
}, 1000);
|
|
}
|
|
|
|
static pulse(element) {
|
|
if (!element) return;
|
|
|
|
element.classList.add("animate-pulse");
|
|
setTimeout(() => {
|
|
element.classList.remove("animate-pulse");
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// Initialize tooltips and other UI enhancements if available
|
|
if (typeof tippy !== "undefined") {
|
|
tippy("[data-tippy-content]");
|
|
}
|
|
</script>
|