mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 23:17:47 +01:00
1702 lines
62 KiB
HTML
1702 lines
62 KiB
HTML
<!-- PHPFlasher Interactive Studio - Enhanced Edition -->
|
|
<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 floating elements -->
|
|
<div class="floating-element absolute top-[15%] left-[10%] w-16 h-16 rounded-full bg-gradient-to-br from-blue-500/10 to-indigo-500/10 animate-float-slow"></div>
|
|
<div class="floating-element absolute top-[40%] left-[80%] w-20 h-20 rounded-full bg-gradient-to-br from-green-500/5 to-emerald-500/5 animate-float-medium"></div>
|
|
<div class="floating-element absolute top-[80%] left-[20%] w-12 h-12 rounded-full bg-gradient-to-br from-amber-500/5 to-orange-500/5 animate-float-fast"></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-6 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-xl bg-white overflow-hidden backdrop-blur-sm border border-gray-100 transition-all hover:shadow-2xl">
|
|
<!-- 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-20250313</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: 650px;">
|
|
<!-- Notification Type Selection -->
|
|
<div class="mb-6">
|
|
<label class="block mb-2 text-sm font-medium text-gray-700">Notification Type</label>
|
|
<div class="grid grid-cols-4 gap-2 w-full">
|
|
<button class="type-button flex flex-col items-center justify-center p-3 rounded-lg bg-green-50 border border-green-200 hover:bg-green-100 transition-all duration-300 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 flex-col items-center justify-center p-3 rounded-lg bg-red-50 border border-red-200 hover:bg-red-100 transition-all duration-300 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 flex-col items-center justify-center p-3 rounded-lg bg-blue-50 border border-blue-200 hover:bg-blue-100 transition-all duration-300 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 flex-col items-center justify-center p-3 rounded-lg bg-amber-50 border border-amber-200 hover:bg-amber-100 transition-all duration-300 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</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-all duration-300" 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 transition-all duration-300" 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 hover:shadow-md transition-shadow duration-300">
|
|
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-3 py-2 flex items-center justify-between">
|
|
<span class="font-medium text-gray-700">Configuration Options</span>
|
|
<span class="text-xs text-gray-500">Customize appearance</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 transition-all duration-300">
|
|
<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 transition-all duration-300">
|
|
<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 transition-all duration-300">
|
|
<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 transition-all duration-300">
|
|
<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 shadow-lg" 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.5 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 id="user-info" 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>
|
|
|
|
<!-- Code animation wrapper - Contains both the static code and the animation layer -->
|
|
<div id="code-display-wrapper" class="relative">
|
|
<!-- Laravel Code Panel -->
|
|
<div id="laravel-code-panel" class="p-4 bg-gray-50 overflow-y-auto code-panel active" style="height: 420px;">
|
|
<!-- Code will be inserted here by JavaScript -->
|
|
</div>
|
|
|
|
<!-- Symfony Code Panel -->
|
|
<div id="symfony-code-panel" class="p-4 bg-gray-50 overflow-y-auto hidden code-panel" style="height: 420px;">
|
|
<!-- Code will be inserted here by JavaScript -->
|
|
</div>
|
|
|
|
<!-- JavaScript Code Panel -->
|
|
<div id="js-code-panel" class="p-4 bg-gray-50 overflow-y-auto hidden code-panel" style="height: 420px;">
|
|
<!-- Code will be inserted here by JavaScript -->
|
|
</div>
|
|
|
|
<!-- Animation overlay - This stays invisible but receives animation commands -->
|
|
<div id="animation-layer" class="absolute top-0 left-0 w-full h-full pointer-events-none"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer with links -->
|
|
<div class="px-6 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="pulse-dot"></span>
|
|
<span class="text-xs text-gray-500">Last update: <span id="last-update" class="font-medium">2025-03-13</span></span>
|
|
</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>
|
|
<a href="#" id="theme-toggle" class="text-xs text-gray-600 hover:text-blue-600 transition-colors flex items-center">
|
|
<i class="fas fa-adjust mr-1.5 text-xs"></i>
|
|
<span>Theme</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Version badge -->
|
|
<div class="mt-4 flex justify-center">
|
|
<div class="bg-white/80 backdrop-blur-sm shadow-sm border border-gray-100 rounded-full px-3 py-1 text-xs flex items-center space-x-1.5">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
|
<span class="text-gray-600">PHPFlasher v2.5.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
/* Base styles */
|
|
:root {
|
|
--primary-color: #4F46E5;
|
|
--success-color: #10B981;
|
|
--error-color: #EF4444;
|
|
--info-color: #3B82F6;
|
|
--warning-color: #F59E0B;
|
|
--highlight-color: rgba(16, 185, 129, 0.1);
|
|
}
|
|
|
|
body {
|
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
/* Custom animations */
|
|
@keyframes shine {
|
|
to { left: 100%; }
|
|
}
|
|
|
|
@keyframes ping {
|
|
0% { transform: scale(1); opacity: 1; }
|
|
75%, 100% { transform: scale(2); opacity: 0; }
|
|
}
|
|
|
|
@keyframes float-slow {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-15px); }
|
|
}
|
|
|
|
@keyframes float-medium {
|
|
0%, 100% { transform: translateY(0) translateX(0); }
|
|
50% { transform: translateY(-10px) translateX(5px); }
|
|
}
|
|
|
|
@keyframes float-fast {
|
|
0%, 100% { transform: translateY(0) translateX(0); }
|
|
25% { transform: translateY(-5px) translateX(3px); }
|
|
75% { transform: translateY(5px) translateX(-3px); }
|
|
}
|
|
|
|
@keyframes typing {
|
|
from { width: 0 }
|
|
to { width: 100% }
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
@keyframes pulse-bg {
|
|
0%, 100% { background-color: rgba(79, 70, 229, 0.05); }
|
|
50% { background-color: rgba(79, 70, 229, 0.15); }
|
|
}
|
|
|
|
@keyframes highlight-line {
|
|
0% { background-color: rgba(16, 185, 129, 0.05); }
|
|
50% { background-color: rgba(16, 185, 129, 0.2); }
|
|
100% { background-color: rgba(16, 185, 129, 0.05); }
|
|
}
|
|
|
|
/* Floating elements */
|
|
.animate-float-slow { animation: float-slow 8s ease-in-out infinite; }
|
|
.animate-float-medium { animation: float-medium 6s ease-in-out infinite; }
|
|
.animate-float-fast { animation: float-fast 4s ease-in-out infinite; }
|
|
|
|
/* Pulse dot animation in footer */
|
|
.pulse-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
display: inline-block;
|
|
border-radius: 50%;
|
|
background-color: var(--primary-color);
|
|
position: relative;
|
|
}
|
|
|
|
.pulse-dot::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -2px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--primary-color);
|
|
animation: ping 1.5s cubic-bezier(0,0,0.2,1) infinite;
|
|
}
|
|
|
|
/* Code display styling */
|
|
.code-block {
|
|
display: block;
|
|
position: relative;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
|
|
.flash-code-block {
|
|
display: inline-block;
|
|
width: 100%;
|
|
background-color: var(--highlight-color);
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
box-shadow: 0 0 2px rgba(16, 185, 129, 0.3);
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.flash-typing {
|
|
display: inline-block;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
background-color: rgba(16, 185, 129, 0.1);
|
|
width: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
animation: typing 3.5s steps(40, end) forwards;
|
|
}
|
|
|
|
.cursor {
|
|
display: inline-block;
|
|
width: 3px;
|
|
height: 1.2em;
|
|
background-color: var(--primary-color);
|
|
margin-left: 2px;
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
/* Animation for code typing */
|
|
.animated-line {
|
|
position: relative;
|
|
padding-left: 4px;
|
|
border-left: 2px solid var(--success-color);
|
|
animation: highlight-line 2s ease-in-out;
|
|
}
|
|
|
|
.type-button {
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.type-button:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.type-button::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 8px;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
box-shadow: 0 0 0 2px var(--primary-color);
|
|
}
|
|
|
|
.type-button:focus::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Code panel enhancements */
|
|
.code-panel {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.code-panel.hidden {
|
|
opacity: 0;
|
|
}
|
|
|
|
.code-panel.active {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Theme toggle effects */
|
|
#theme-toggle:hover i {
|
|
animation: spin 1s ease;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Add responsive adjustments */
|
|
@media (max-width: 768px) {
|
|
.code-panel {
|
|
height: 350px !important;
|
|
}
|
|
}
|
|
|
|
/* Concise code blocks */
|
|
pre code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
}
|
|
|
|
/* Typing container styles */
|
|
.typing-container {
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
padding: 0.5rem;
|
|
border-radius: 4px;
|
|
overflow: visible;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
}
|
|
|
|
/* Make sure the cursor blinks properly */
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
.cursor {
|
|
animation: blink 1s infinite;
|
|
display: inline-block;
|
|
width: 2px;
|
|
height: 1.2em;
|
|
vertical-align: middle;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
/**
|
|
* PHPFlasher Interactive Studio - Enhanced Edition
|
|
*
|
|
* Features:
|
|
* - Real-time code generation with typing animation
|
|
* - Context-aware examples for different notification types
|
|
* - Advanced animations and transitions
|
|
* - Clean and concise code examples
|
|
*
|
|
* @version 3.0.0
|
|
* @author PHPFlasher Team
|
|
* @lastUpdated 2025-03-13
|
|
*/
|
|
|
|
// Create state with proxy to track changes
|
|
const createStateProxy = () => {
|
|
const stateTarget = {
|
|
type: "success",
|
|
title: "",
|
|
message: "Your product has been created successfully!",
|
|
timeout: "",
|
|
position: "",
|
|
direction: "",
|
|
rtl: false,
|
|
theme: "",
|
|
escapeHtml: true,
|
|
_previousState: {},
|
|
_animationInProgress: false
|
|
};
|
|
|
|
return new Proxy(stateTarget, {
|
|
set(target, property, value) {
|
|
// Store previous value
|
|
if (property !== '_previousState' && property !== '_animationInProgress') {
|
|
target._previousState[property] = target[property];
|
|
}
|
|
|
|
target[property] = value;
|
|
|
|
// Update code when state changes
|
|
if (document.readyState === "complete" &&
|
|
property !== '_previousState' &&
|
|
property !== '_animationInProgress') {
|
|
|
|
const onlyOptionsChanged = ['position', 'timeout', 'theme', 'direction', 'rtl', 'escapeHtml'].includes(property);
|
|
|
|
if (property === 'type') {
|
|
// Show loading state
|
|
updateStatus("Changing template...");
|
|
}
|
|
|
|
// Update the code with animation
|
|
updateCodeDisplay(true, onlyOptionsChanged);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Create global state object
|
|
const state = createStateProxy();
|
|
|
|
/**
|
|
* Concise template examples for each notification type and framework
|
|
* Shorter examples to ensure they fit well in the viewport without scrolling
|
|
*/
|
|
const contextTemplates = {
|
|
success: {
|
|
laravel: {
|
|
title: "Success",
|
|
message: "Your product has been created successfully!",
|
|
controller: "ProductController",
|
|
method: "store",
|
|
code: {
|
|
before: `// ProductController.php
|
|
namespace App\\Http\\Controllers;
|
|
|
|
use App\\Models\\Product;
|
|
use App\\Http\\Requests\\ProductRequest;
|
|
|
|
class ProductController extends Controller
|
|
{
|
|
public function store(ProductRequest $request)
|
|
{
|
|
// Create 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: "create",
|
|
code: {
|
|
before: `// ProductController.php
|
|
namespace App\\Controller;
|
|
|
|
use App\\Entity\\Product;
|
|
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
|
|
|
|
class ProductController extends AbstractController
|
|
{
|
|
#[Route('/product/new', name: 'app_product_create')]
|
|
public function create(Request $request): Response
|
|
{
|
|
// Process form & save product
|
|
$product = new Product();
|
|
// ... form handling and persist logic
|
|
|
|
`,
|
|
after: `
|
|
return $this->redirectToRoute('app_product_index');
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Success",
|
|
message: "Your product has been created successfully!",
|
|
file: "product-service.js",
|
|
method: "createProduct",
|
|
code: {
|
|
before: `// product-service.js
|
|
import flasher from '@flasher/flasher';
|
|
|
|
async function createProduct(data) {
|
|
try {
|
|
const response = await api.post('/products', data);
|
|
|
|
`,
|
|
after: `
|
|
return response.data;
|
|
} catch (error) {
|
|
flasher.error('Failed to create product');
|
|
throw error;
|
|
}
|
|
}`
|
|
}
|
|
}
|
|
},
|
|
error: {
|
|
laravel: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your request.",
|
|
controller: "PaymentController",
|
|
method: "process",
|
|
code: {
|
|
before: `// PaymentController.php
|
|
namespace App\\Http\\Controllers;
|
|
|
|
use App\\Services\\PaymentGateway;
|
|
|
|
class PaymentController extends Controller
|
|
{
|
|
public function process(Request $request)
|
|
{
|
|
try {
|
|
$result = $this->paymentGateway->charge($request->token);
|
|
} catch (\\Exception $e) {
|
|
`,
|
|
after: `
|
|
return back();
|
|
}
|
|
|
|
return redirect()->route('payment.success');
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your request.",
|
|
controller: "PaymentController",
|
|
method: "process",
|
|
code: {
|
|
before: `// PaymentController.php
|
|
namespace App\\Controller;
|
|
|
|
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
|
|
|
|
class PaymentController extends AbstractController
|
|
{
|
|
public function process(Request $request): Response
|
|
{
|
|
try {
|
|
$payment = $this->paymentService->process($request->get('token'));
|
|
} catch (\\Exception $e) {
|
|
`,
|
|
after: `
|
|
return $this->redirectToRoute('payment_form');
|
|
}
|
|
|
|
return $this->redirectToRoute('payment_success');
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Error",
|
|
message: "An error occurred while processing your request.",
|
|
file: "payment-service.js",
|
|
method: "processPayment",
|
|
code: {
|
|
before: `// payment-service.js
|
|
import flasher from '@flasher/flasher';
|
|
|
|
async function processPayment(paymentData) {
|
|
try {
|
|
const response = await api.post('/payments', paymentData);
|
|
if (!response.ok) {
|
|
`,
|
|
after: `
|
|
return false;
|
|
}
|
|
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
}`
|
|
}
|
|
}
|
|
},
|
|
info: {
|
|
laravel: {
|
|
title: "Information",
|
|
message: "Your order is being processed.",
|
|
controller: "OrderController",
|
|
method: "submit",
|
|
code: {
|
|
before: `// OrderController.php
|
|
namespace App\\Http\\Controllers;
|
|
|
|
use App\\Models\\Order;
|
|
|
|
class OrderController extends Controller
|
|
{
|
|
public function submit(Request $request)
|
|
{
|
|
// Create and save order
|
|
$order = Order::create($request->validated());
|
|
|
|
`,
|
|
after: `
|
|
// Send to processing queue
|
|
ProcessOrderJob::dispatch($order);
|
|
|
|
return redirect()->route('orders.show', $order);
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Information",
|
|
message: "Your order is being processed.",
|
|
controller: "OrderController",
|
|
method: "create",
|
|
code: {
|
|
before: `// OrderController.php
|
|
namespace App\\Controller;
|
|
|
|
use App\\Entity\\Order;
|
|
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
|
|
|
|
class OrderController extends AbstractController
|
|
{
|
|
public function create(Request $request): Response
|
|
{
|
|
// Create and save order
|
|
$order = new Order();
|
|
// ... set order properties
|
|
$this->entityManager->persist($order);
|
|
$this->entityManager->flush();
|
|
|
|
`,
|
|
after: `
|
|
// Dispatch order processing
|
|
$this->messageBus->dispatch(new ProcessOrder($order->getId()));
|
|
|
|
return $this->redirectToRoute('app_order_show', [
|
|
'id' => $order->getId()
|
|
]);
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Information",
|
|
message: "Your order is being processed.",
|
|
file: "order-service.js",
|
|
method: "submitOrder",
|
|
code: {
|
|
before: `// order-service.js
|
|
import flasher from '@flasher/flasher';
|
|
|
|
async function submitOrder(orderData) {
|
|
try {
|
|
const response = await api.post('/orders', orderData);
|
|
const order = response.data;
|
|
|
|
`,
|
|
after: `
|
|
// Track order
|
|
analytics.trackOrder(order.id);
|
|
|
|
return order;
|
|
} catch (error) {
|
|
flasher.error('Order submission failed');
|
|
throw error;
|
|
}
|
|
}`
|
|
}
|
|
}
|
|
},
|
|
warning: {
|
|
laravel: {
|
|
title: "Warning",
|
|
message: "Your account will expire in 3 days.",
|
|
controller: "AccountController",
|
|
method: "dashboard",
|
|
code: {
|
|
before: `// AccountController.php
|
|
namespace App\\Http\\Controllers;
|
|
|
|
class AccountController extends Controller
|
|
{
|
|
public function dashboard()
|
|
{
|
|
$user = auth()->user();
|
|
$subscription = $user->subscription;
|
|
|
|
if ($subscription && $subscription->daysUntilExpiration() <= 3) {
|
|
`,
|
|
after: `
|
|
}
|
|
|
|
return view('account.dashboard', compact('user'));
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
symfony: {
|
|
title: "Warning",
|
|
message: "Your account will expire in 3 days.",
|
|
controller: "AccountController",
|
|
method: "dashboard",
|
|
code: {
|
|
before: `// AccountController.php
|
|
namespace App\\Controller;
|
|
|
|
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
|
|
|
|
class AccountController extends AbstractController
|
|
{
|
|
public function dashboard(): Response
|
|
{
|
|
$user = $this->getUser();
|
|
$subscription = $user->getSubscription();
|
|
|
|
if ($subscription && $subscription->getDaysRemaining() <= 3) {
|
|
`,
|
|
after: `
|
|
}
|
|
|
|
return $this->render('account/dashboard.html.twig', [
|
|
'user' => $user
|
|
]);
|
|
}
|
|
}`
|
|
}
|
|
},
|
|
js: {
|
|
title: "Warning",
|
|
message: "Your account will expire in 3 days.",
|
|
file: "account-service.js",
|
|
method: "checkStatus",
|
|
code: {
|
|
before: `// account-service.js
|
|
import flasher from '@flasher/flasher';
|
|
|
|
async function checkStatus() {
|
|
const response = await api.get('/account/status');
|
|
const { subscription } = response.data;
|
|
|
|
if (subscription && subscription.daysRemaining <= 3) {
|
|
`,
|
|
after: `
|
|
}
|
|
|
|
return subscription;
|
|
}`
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Helper function to safely escape special characters in code
|
|
*/
|
|
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Updates the visual style of notification type buttons
|
|
*/
|
|
function initializeTypeButtons() {
|
|
document.querySelectorAll(".type-button").forEach(btn => {
|
|
const type = btn.dataset.type;
|
|
|
|
// Reset styles for all buttons
|
|
btn.classList.remove("border-2");
|
|
btn.classList.remove("border-green-500", "border-red-500", "border-blue-500", "border-amber-500");
|
|
|
|
// Style the active button
|
|
if (type === state.type) {
|
|
btn.classList.add("border-2");
|
|
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize form fields with values from state
|
|
*/
|
|
function initializeFormValues() {
|
|
const template = contextTemplates[state.type];
|
|
const currentTab = getCurrentTab();
|
|
|
|
// Set title - don't set if it's the same as notification type
|
|
if (!state.title && template) {
|
|
state.title = template[currentTab].title;
|
|
}
|
|
document.getElementById("title-input").value = state.title || "";
|
|
|
|
// Set message
|
|
document.getElementById("message-input").value = state.message || "";
|
|
|
|
// Set option fields
|
|
document.getElementById("position-select").value = state.position || "";
|
|
document.getElementById("timeout-input").value = state.timeout || "";
|
|
document.getElementById("theme-select").value = state.theme || "";
|
|
document.getElementById("direction-select").value = state.direction || "";
|
|
|
|
document.getElementById("rtl-check").checked = !!state.rtl;
|
|
document.getElementById("escape-html-check").checked = state.escapeHtml !== false;
|
|
}
|
|
|
|
/**
|
|
* Updates UI status indicator
|
|
*/
|
|
function updateStatus(message) {
|
|
const statusText = document.getElementById("status-text");
|
|
if (!statusText) return;
|
|
|
|
statusText.textContent = message;
|
|
|
|
// Change status colors
|
|
statusText.classList.remove("text-emerald-600");
|
|
statusText.classList.add("text-blue-600");
|
|
|
|
const dot = document.querySelector("#status-indicator div");
|
|
if (dot) {
|
|
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";
|
|
|
|
if (dot) {
|
|
dot.classList.remove("bg-blue-500");
|
|
dot.classList.add("bg-emerald-500");
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Get the current active tab (laravel/symfony/js)
|
|
*/
|
|
function getCurrentTab() {
|
|
const activeTab = document.querySelector(".code-tab-btn.text-blue-600");
|
|
return activeTab ? activeTab.getAttribute("data-tab") : "laravel";
|
|
}
|
|
|
|
/**
|
|
* Switch to a specific code tab
|
|
*/
|
|
function showCodeTab(tab) {
|
|
// Hide all panels
|
|
document.querySelectorAll(".code-panel").forEach(panel => {
|
|
panel.classList.add("hidden");
|
|
});
|
|
|
|
// Show 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 code display
|
|
updateCodeDisplay(true);
|
|
}
|
|
|
|
/**
|
|
* Generate flash code for the specified framework
|
|
* Handles multi-line formatting with proper indentation
|
|
*/
|
|
function generateFlashCode(framework, indent = '') {
|
|
// Determine the prefix based on framework
|
|
const prefix = framework === "js" ? "flasher" : "flash()";
|
|
|
|
// Sanitize message and title
|
|
const safeMessage = sanitizeForCode(state.message);
|
|
|
|
// Only use title if it's not empty and doesn't match the type name
|
|
// (prevents redundant "Success" title on success notifications)
|
|
const safeTitle = state.title && state.title.toLowerCase() !== state.type.toLowerCase()
|
|
? sanitizeForCode(state.title) : "";
|
|
|
|
// Lines of code with proper indentation
|
|
let lines = [];
|
|
|
|
if (framework === "js") {
|
|
// JavaScript options 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;
|
|
|
|
// Simple one-line if no options
|
|
if (Object.keys(options).length === 0) {
|
|
return `${indent}${prefix}.${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''});`;
|
|
}
|
|
|
|
// Multi-line with options
|
|
lines.push(`${indent}${prefix}`);
|
|
lines.push(`${indent} .${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''}, {`);
|
|
|
|
// Add options with proper indentation
|
|
Object.entries(options).forEach(([key, value], index, array) => {
|
|
const comma = index < array.length - 1 ? ',' : '';
|
|
if (typeof value === "string") {
|
|
lines.push(`${indent} ${key}: '${value}'${comma}`);
|
|
} else {
|
|
lines.push(`${indent} ${key}: ${value}${comma}`);
|
|
}
|
|
});
|
|
|
|
lines.push(`${indent} });`);
|
|
} else {
|
|
// PHP frameworks with method chaining
|
|
lines.push(`${indent}${prefix}`);
|
|
|
|
// Add options
|
|
if (state.position) {
|
|
lines.push(`${indent} ->option('position', '${state.position}')`);
|
|
}
|
|
if (state.timeout) {
|
|
lines.push(`${indent} ->option('timeout', ${state.timeout})`);
|
|
}
|
|
if (state.theme) {
|
|
lines.push(`${indent} ->option('theme', '${state.theme}')`);
|
|
}
|
|
if (state.direction) {
|
|
lines.push(`${indent} ->option('direction', '${state.direction}')`);
|
|
}
|
|
if (state.rtl) {
|
|
lines.push(`${indent} ->option('rtl', true)`);
|
|
}
|
|
if (state.escapeHtml === false) {
|
|
lines.push(`${indent} ->option('escapeHtml', false)`);
|
|
}
|
|
|
|
// Add the method call
|
|
lines.push(`${indent} ->${state.type}('${safeMessage}'${safeTitle ? `, '${safeTitle}'` : ''});`);
|
|
}
|
|
|
|
// Join lines with newlines
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Generate the complete code example
|
|
*/
|
|
function generateCompleteExample(framework) {
|
|
// Get framework-specific template for current notification type
|
|
const template = contextTemplates[state.type][framework];
|
|
const indent = framework === "js" ? ' ' : ' ';
|
|
|
|
// Generate the flash code with proper indentation
|
|
const flashCode = generateFlashCode(framework, indent);
|
|
|
|
// Combine the parts to create the complete code example
|
|
return `${template.code.before}\n${flashCode}\n${template.code.after}`;
|
|
}
|
|
|
|
/**
|
|
* Update code display with animation
|
|
*/
|
|
function updateCodeDisplay(animate = true) {
|
|
const currentTab = getCurrentTab();
|
|
const panel = document.getElementById(`${currentTab}-code-panel`);
|
|
|
|
if (!panel) return;
|
|
|
|
// Get template parts
|
|
const template = contextTemplates[state.type][currentTab];
|
|
const indent = currentTab === "js" ? ' ' : ' ';
|
|
|
|
// Generate the flash code
|
|
const flashCode = generateFlashCode(currentTab, indent);
|
|
|
|
if (animate) {
|
|
// Animated version
|
|
// First, display the "before" code with no animation
|
|
panel.innerHTML = `
|
|
<pre class="language-${currentTab === 'js' ? 'javascript' : 'php'}">
|
|
<code>${escapeHtml(template.code.before)}</code>
|
|
</pre>
|
|
`;
|
|
|
|
// Apply syntax highlighting to the static part
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(panel.querySelector('code'));
|
|
}
|
|
|
|
// Create a typing container that will show the animated code
|
|
const typingContainer = document.createElement('div');
|
|
typingContainer.className = 'typing-container mt-2 mb-2 pl-2 border-l-2 border-green-500 bg-green-50/20';
|
|
typingContainer.style.fontFamily = 'monospace';
|
|
typingContainer.style.position = 'relative';
|
|
typingContainer.style.minHeight = '50px';
|
|
|
|
// Insert the typing container
|
|
panel.querySelector('pre').appendChild(typingContainer);
|
|
|
|
// Create container for the "after" code
|
|
const afterContainer = document.createElement('pre');
|
|
afterContainer.className = `language-${currentTab === 'js' ? 'javascript' : 'php'}`;
|
|
afterContainer.innerHTML = `<code>${escapeHtml(template.code.after)}</code>`;
|
|
|
|
// Add the after code
|
|
panel.appendChild(afterContainer);
|
|
|
|
// Apply syntax highlighting to the after part
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(afterContainer.querySelector('code'));
|
|
}
|
|
|
|
// Now animate the typing of the flash code
|
|
typeCode(flashCode, typingContainer);
|
|
} else {
|
|
// Non-animated version - just show the complete code
|
|
const completeCode = template.code.before + flashCode + template.code.after;
|
|
|
|
panel.innerHTML = `
|
|
<pre class="language-${currentTab === 'js' ? 'javascript' : 'php'}">
|
|
<code>${escapeHtml(completeCode)}</code>
|
|
</pre>
|
|
`;
|
|
|
|
// Apply syntax highlighting
|
|
if (typeof Prism !== "undefined") {
|
|
Prism.highlightElement(panel.querySelector('code'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Human-like typing animation
|
|
*/
|
|
function typeCode(code, container) {
|
|
// Create cursor element
|
|
const cursor = document.createElement('span');
|
|
cursor.className = 'cursor';
|
|
cursor.innerHTML = '|';
|
|
cursor.style.display = 'inline-block';
|
|
cursor.style.width = '2px';
|
|
cursor.style.backgroundColor = '#4F46E5';
|
|
cursor.style.color = 'transparent';
|
|
cursor.style.marginLeft = '1px';
|
|
cursor.style.animation = 'blink 1s infinite';
|
|
|
|
container.appendChild(cursor);
|
|
|
|
// Clear existing content except cursor
|
|
let i = 0;
|
|
let currentLine = document.createElement('div');
|
|
currentLine.style.minHeight = '1.2em';
|
|
container.insertBefore(currentLine, cursor);
|
|
|
|
// Characters typed per minute (average human typing speed)
|
|
const charsPerMinute = 300;
|
|
const baseDelay = 60000 / charsPerMinute; // milliseconds per character
|
|
|
|
// Human-like typing with variable speed
|
|
function typeNextChar() {
|
|
if (i < code.length) {
|
|
const char = code.charAt(i);
|
|
|
|
// Handle newlines specially
|
|
if (char === '\n') {
|
|
currentLine = document.createElement('div');
|
|
currentLine.style.minHeight = '1.2em';
|
|
container.insertBefore(currentLine, cursor);
|
|
} else {
|
|
// Use syntax highlighting for individual characters
|
|
const charSpan = document.createElement('span');
|
|
|
|
// Different colors based on character type for code-like appearance
|
|
if ('(){}[];'.includes(char)) {
|
|
charSpan.style.color = '#a626a4'; // Purple for punctuation
|
|
} else if ('\'"`'.includes(char)) {
|
|
charSpan.style.color = '#50a14f'; // Green for strings
|
|
} else if (char === '-' && i > 0 && code.charAt(i-1) === '-' && code.charAt(i-2) === '>') {
|
|
charSpan.style.color = '#e45649'; // Red for operators
|
|
} else if (char === '>') {
|
|
charSpan.style.color = '#e45649'; // Red for operators
|
|
} else {
|
|
charSpan.style.color = '#383a42'; // Default text color
|
|
}
|
|
|
|
charSpan.textContent = char;
|
|
currentLine.appendChild(charSpan);
|
|
}
|
|
|
|
i++;
|
|
|
|
// Variable typing speed for realism
|
|
let delay = baseDelay;
|
|
|
|
// Slow down for punctuation
|
|
if (['.', ',', ';', ')', '}', '>', '!'].includes(char)) {
|
|
delay *= 2.5;
|
|
}
|
|
// Even slower after line breaks
|
|
else if (char === '\n') {
|
|
delay *= 4;
|
|
}
|
|
// Add random variation (± 30%)
|
|
else {
|
|
delay *= (0.7 + Math.random() * 0.6);
|
|
}
|
|
|
|
// Random "thinking" pauses (5% chance)
|
|
if (Math.random() < 0.05) {
|
|
delay += Math.random() * 500;
|
|
}
|
|
|
|
setTimeout(typeNextChar, delay);
|
|
}
|
|
}
|
|
|
|
// Start typing with slight initial delay
|
|
setTimeout(typeNextChar, 300);
|
|
}
|
|
|
|
/**
|
|
* Create and play a typing animation for the flash code section
|
|
*/
|
|
function animateFlashCode(framework) {
|
|
const template = contextTemplates[state.type][framework];
|
|
const panel = document.getElementById(`${framework}-code-panel`);
|
|
if (!panel) return;
|
|
|
|
// Find the pre/code elements
|
|
const codeElement = panel.querySelector('code');
|
|
if (!codeElement) return;
|
|
|
|
// Find the flash code - we'll highlight this section
|
|
const flashCode = generateFlashCode(framework, framework === 'js' ? ' ' : ' ');
|
|
const fullText = codeElement.textContent;
|
|
|
|
// Split the code into sections
|
|
const beforePart = template.code.before;
|
|
const afterPart = template.code.after;
|
|
|
|
// Get the position of the flash code
|
|
const startIndex = fullText.indexOf(flashCode);
|
|
if (startIndex === -1) return;
|
|
|
|
// Highlight the flash code part
|
|
const flashHtml = document.createElement('span');
|
|
flashHtml.className = 'animated-line';
|
|
flashHtml.textContent = flashCode;
|
|
|
|
// Use ranges to precisely replace just that section of text
|
|
const range = document.createRange();
|
|
const selection = window.getSelection();
|
|
|
|
// Clear any existing selection
|
|
selection.removeAllRanges();
|
|
|
|
// Try to set up the range for the flashCode text
|
|
try {
|
|
const textNodes = getTextNodesIn(codeElement);
|
|
let currentPos = 0;
|
|
let startNode, startOffset, endNode, endOffset;
|
|
|
|
// Find the nodes containing our flash code
|
|
for (let node of textNodes) {
|
|
const nodeTextLength = node.textContent.length;
|
|
|
|
// Check if this node contains the start of our target
|
|
if (currentPos <= startIndex && currentPos + nodeTextLength > startIndex) {
|
|
startNode = node;
|
|
startOffset = startIndex - currentPos;
|
|
}
|
|
|
|
// Check if this node contains the end of our target
|
|
if (currentPos <= startIndex + flashCode.length &&
|
|
currentPos + nodeTextLength >= startIndex + flashCode.length) {
|
|
endNode = node;
|
|
endOffset = startIndex + flashCode.length - currentPos;
|
|
break;
|
|
}
|
|
|
|
currentPos += nodeTextLength;
|
|
}
|
|
|
|
if (startNode && endNode) {
|
|
// Set the range and highlight it
|
|
range.setStart(startNode, startOffset);
|
|
range.setEnd(endNode, endOffset);
|
|
|
|
// Create our highlighted element
|
|
const highlightSpan = document.createElement('span');
|
|
highlightSpan.className = 'animated-line';
|
|
highlightSpan.style.animation = 'highlight-line 2s ease-in-out';
|
|
|
|
// Replace the range with our highlight
|
|
range.surroundContents(highlightSpan);
|
|
|
|
// Animate typing within the highlighted section
|
|
simulateTyping(highlightSpan, flashCode);
|
|
}
|
|
} catch (e) {
|
|
console.warn("Animation error:", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all text nodes within an element
|
|
*/
|
|
function getTextNodesIn(element) {
|
|
const textNodes = [];
|
|
const walk = document.createTreeWalker(
|
|
element,
|
|
NodeFilter.SHOW_TEXT,
|
|
null,
|
|
false
|
|
);
|
|
|
|
let node;
|
|
while (node = walk.nextNode()) {
|
|
textNodes.push(node);
|
|
}
|
|
|
|
return textNodes;
|
|
}
|
|
|
|
/**
|
|
* Simulate typing effect within an element
|
|
*/
|
|
function simulateTyping(element, text) {
|
|
if (!element) return;
|
|
|
|
// Create cursor element
|
|
const cursor = document.createElement('span');
|
|
cursor.className = 'cursor';
|
|
element.appendChild(cursor);
|
|
|
|
// Save original content
|
|
const originalContent = element.innerHTML;
|
|
|
|
// Clear content except cursor
|
|
element.innerHTML = '';
|
|
element.appendChild(cursor);
|
|
|
|
let i = 0;
|
|
const typingSpeed = 35; // milliseconds per character
|
|
|
|
// Type characters one by one
|
|
function typeCharacter() {
|
|
if (i < text.length) {
|
|
const char = document.createTextNode(text.charAt(i));
|
|
element.insertBefore(char, cursor);
|
|
i++;
|
|
|
|
// Variable speed for natural feel
|
|
let delay = typingSpeed;
|
|
const currentChar = text.charAt(i - 1);
|
|
|
|
// Pause longer at punctuation
|
|
if ([';', '.', ':', ',', ')', '}'].includes(currentChar)) {
|
|
delay = typingSpeed * 3;
|
|
} else if (currentChar === '\n') {
|
|
delay = typingSpeed * 5; // Longer at line breaks
|
|
} else if (Math.random() < 0.1) {
|
|
delay = typingSpeed * (1 + Math.random()); // Random pauses
|
|
}
|
|
|
|
setTimeout(typeCharacter, delay);
|
|
} else {
|
|
// Restore full content with original formatting but keep highlighting
|
|
setTimeout(() => {
|
|
cursor.remove();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// Start typing with initial delay
|
|
setTimeout(typeCharacter, 300);
|
|
}
|
|
|
|
/**
|
|
* Updates the message and title based on the notification type
|
|
*/
|
|
function updateContentForType(type) {
|
|
const currentTab = getCurrentTab();
|
|
const template = contextTemplates[type][currentTab];
|
|
|
|
if (!template) return;
|
|
|
|
// Update message input
|
|
document.getElementById("message-input").value = template.message;
|
|
state.message = template.message;
|
|
|
|
// Update title input - but check if it's the same as type
|
|
if (template.title.toLowerCase() !== type.toLowerCase()) {
|
|
document.getElementById("title-input").value = template.title;
|
|
state.title = template.title;
|
|
} else {
|
|
document.getElementById("title-input").value = "";
|
|
state.title = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show a notification with current settings
|
|
*/
|
|
function showNotification() {
|
|
// Build a clean options object
|
|
const options = {};
|
|
|
|
// Add all configured 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 = state.rtl;
|
|
if (state.escapeHtml !== undefined) options.escapeHtml = state.escapeHtml;
|
|
|
|
try {
|
|
// Check if flasher is available
|
|
if (typeof flasher !== "undefined") {
|
|
if (state.theme) {
|
|
// Use themed approach
|
|
const notificationOptions = {
|
|
type: state.type,
|
|
message: state.message,
|
|
title: state.title || undefined,
|
|
...options
|
|
};
|
|
delete notificationOptions.theme; // Remove theme from options
|
|
flasher.use(state.theme).flash(notificationOptions);
|
|
} else {
|
|
// Use standard approach
|
|
flasher[state.type](state.message, state.title || null, options);
|
|
}
|
|
|
|
// Update status
|
|
updateStatus("Notification shown ✓");
|
|
|
|
} else {
|
|
// Fallback to alert
|
|
let optionsText = Object.entries(options)
|
|
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
.join('\n');
|
|
|
|
alert(`Notification would display with:\n\n` +
|
|
`Type: ${state.type}\n` +
|
|
`Message: ${state.message}\n` +
|
|
(state.title ? `Title: ${state.title}\n` : "") +
|
|
(optionsText ? `\nOptions:\n${optionsText}` : ""));
|
|
}
|
|
} catch (e) {
|
|
console.error("Error showing notification:", e);
|
|
updateStatus("Error: " + e.message);
|
|
alert("Failed to show notification: " + e.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update current time in the UI
|
|
*/
|
|
function updateCurrentTime() {
|
|
const userInfo = document.getElementById("user-info");
|
|
if (userInfo) {
|
|
const now = new Date();
|
|
const formattedDate = now.toISOString().slice(0, 19).replace('T', ' ');
|
|
userInfo.innerHTML = `User: <span class="font-medium">yoeunes</span> • Active: ${formattedDate}`;
|
|
}
|
|
|
|
// Update last update time
|
|
const lastUpdate = document.getElementById("last-update");
|
|
if (lastUpdate) {
|
|
lastUpdate.textContent = new Date().toISOString().split('T')[0];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the application when DOM is loaded
|
|
*/
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
// Set up initial values
|
|
initializeFormValues();
|
|
initializeTypeButtons();
|
|
updateCurrentTime();
|
|
|
|
// Set up type button event listeners
|
|
document.querySelectorAll(".type-button").forEach(btn => {
|
|
btn.addEventListener("click", () => {
|
|
const type = btn.dataset.type;
|
|
state.type = type;
|
|
|
|
// Update content based on type
|
|
updateContentForType(type);
|
|
|
|
// Update button styling
|
|
initializeTypeButtons();
|
|
|
|
// Update status
|
|
updateStatus(`Type: ${state.type}`);
|
|
});
|
|
});
|
|
|
|
// Input event listeners
|
|
document.getElementById("title-input").addEventListener("input", (e) => {
|
|
state.title = e.target.value.trim();
|
|
});
|
|
|
|
document.getElementById("message-input").addEventListener("input", (e) => {
|
|
state.message = e.target.value;
|
|
});
|
|
|
|
// Select event listeners
|
|
document.getElementById("position-select").addEventListener("change", (e) => {
|
|
state.position = e.target.value;
|
|
});
|
|
|
|
document.getElementById("timeout-input").addEventListener("change", (e) => {
|
|
state.timeout = e.target.value ? parseInt(e.target.value) : "";
|
|
});
|
|
|
|
document.getElementById("theme-select").addEventListener("change", (e) => {
|
|
state.theme = e.target.value;
|
|
});
|
|
|
|
document.getElementById("direction-select").addEventListener("change", (e) => {
|
|
state.direction = e.target.value;
|
|
});
|
|
|
|
// Checkbox event listeners
|
|
document.getElementById("rtl-check").addEventListener("change", (e) => {
|
|
state.rtl = e.target.checked;
|
|
});
|
|
|
|
document.getElementById("escape-html-check").addEventListener("change", (e) => {
|
|
state.escapeHtml = e.target.checked;
|
|
});
|
|
|
|
// Code tab buttons
|
|
document.querySelectorAll(".code-tab-btn").forEach(button => {
|
|
button.addEventListener("click", () => {
|
|
const tab = button.getAttribute("data-tab");
|
|
showCodeTab(tab);
|
|
});
|
|
});
|
|
|
|
// Launch notification button
|
|
const notificationBtn = document.getElementById("show-notification-btn");
|
|
notificationBtn.addEventListener("click", () => {
|
|
// Add press effect
|
|
notificationBtn.classList.add("scale-95");
|
|
setTimeout(() => notificationBtn.classList.remove("scale-95"), 150);
|
|
|
|
updateStatus("Launching notification...");
|
|
showNotification();
|
|
});
|
|
|
|
// Theme toggle
|
|
const themeToggle = document.getElementById("theme-toggle");
|
|
if (themeToggle) {
|
|
themeToggle.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
document.documentElement.classList.toggle("dark");
|
|
|
|
// Update icon
|
|
const icon = themeToggle.querySelector("i");
|
|
if (icon) {
|
|
if (document.documentElement.classList.contains("dark")) {
|
|
icon.classList.remove("fa-adjust");
|
|
icon.classList.add("fa-sun");
|
|
themeToggle.querySelector("span").textContent = "Light";
|
|
} else {
|
|
icon.classList.remove("fa-sun");
|
|
icon.classList.add("fa-adjust");
|
|
themeToggle.querySelector("span").textContent = "Theme";
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update time periodically
|
|
setInterval(updateCurrentTime, 60000); // Update every minute
|
|
|
|
// Add console signature
|
|
console.info(
|
|
"%c✨ PHPFlasher Interactive Studio v3.0.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;");
|
|
|
|
// Initialize the default tab
|
|
showCodeTab("laravel");
|
|
});
|
|
</script>
|