feat: PWA template - premium dark UI with glassmorphism, particles, and Docker deployment

This commit is contained in:
Kerboul
2026-03-09 08:00:59 -07:00
parent 14031f73a3
commit bff5fdc3c0
12 changed files with 1453 additions and 481 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.git
.gitignore
README.md
*.md

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM nginx:alpine
# Remove default config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy application files
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
COPY app.js /usr/share/nginx/html/
COPY sw.js /usr/share/nginx/html/
COPY manifest.json /usr/share/nginx/html/
COPY icons/ /usr/share/nginx/html/icons/
EXPOSE 2080
CMD ["nginx", "-g", "daemon off;"]

277
app.js
View File

@@ -1,59 +1,246 @@
// Register Service Worker for PWA /* ════════════════════════════════════════════════════
if ('serviceWorker' in navigator) { AMUSEMENT — Application Logic
window.addEventListener('load', () => { Particles, counters, cursor glow, service worker
navigator.serviceWorker.register('./sw.js') ════════════════════════════════════════════════════ */
.then((registration) => {
console.log('SW registered: ', registration); (function () {
}) 'use strict';
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError); // ─── Cursor Glow ───
const glow = document.getElementById('cursor-glow');
let mouseX = -1000, mouseY = -1000;
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
glow.style.left = mouseX + 'px';
glow.style.top = mouseY + 'px';
});
// card glow follow
document.querySelectorAll('.feature-card').forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
card.style.setProperty('--mouse-x', x + '%');
card.style.setProperty('--mouse-y', y + '%');
}); });
}); });
}
// PWA Install Prompt Logic // ─── Particles Canvas ───
let deferredPrompt; const canvas = document.getElementById('particles-canvas');
const installBtn = document.getElementById('installBtn'); const ctx = canvas.getContext('2d');
let particles = [];
const PARTICLE_COUNT = 60;
window.addEventListener('beforeinstallprompt', (e) => { function resizeCanvas() {
// Prevent Chrome 67 and earlier from automatically showing the prompt canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
class Particle {
constructor() { this.reset(); }
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 0.5;
this.speedX = (Math.random() - 0.5) * 0.3;
this.speedY = (Math.random() - 0.5) * 0.3;
this.opacity = Math.random() * 0.5 + 0.1;
this.hue = 260 + Math.random() * 80; // purple-pink range
}
update() {
this.x += this.speedX;
this.y += this.speedY;
if (this.x < 0 || this.x > canvas.width) this.speedX *= -1;
if (this.y < 0 || this.y > canvas.height) this.speedY *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${this.hue}, 70%, 65%, ${this.opacity})`;
ctx.fill();
}
}
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push(new Particle());
}
function drawLines() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `hsla(270, 60%, 60%, ${0.06 * (1 - dist / 150)})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
}
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p) => { p.update(); p.draw(); });
drawLines();
requestAnimationFrame(animateParticles);
}
animateParticles();
// ─── Navigation ───
const nav = document.getElementById('main-nav');
const navToggle = document.getElementById('nav-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const navLinks = document.querySelectorAll('.nav-link, .mobile-link');
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 40);
updateActiveNavLink();
});
navToggle.addEventListener('click', () => {
navToggle.classList.toggle('open');
mobileMenu.classList.toggle('show');
});
navLinks.forEach((link) => {
link.addEventListener('click', () => {
navToggle.classList.remove('open');
mobileMenu.classList.remove('show');
});
});
function updateActiveNavLink() {
const sections = document.querySelectorAll('.section');
let current = '';
sections.forEach((s) => {
const top = s.offsetTop - 120;
if (window.scrollY >= top) current = s.getAttribute('id');
});
document.querySelectorAll('.nav-link').forEach((l) => {
l.classList.toggle('active', l.dataset.section === current);
});
}
// ─── Scroll Reveal ───
const revealElements = document.querySelectorAll(
'.feature-card, .stat-card, .gallery-item, .contact-container, .section-header'
);
revealElements.forEach((el) => el.classList.add('reveal'));
const revealObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: '0px 0px -40px 0px' }
);
revealElements.forEach((el) => revealObserver.observe(el));
// ─── Stat Counters ───
const statNumbers = document.querySelectorAll('.stat-number');
const statFills = document.querySelectorAll('.stat-fill');
let statAnimated = false;
function animateCounters() {
if (statAnimated) return;
statAnimated = true;
statNumbers.forEach((el) => {
const target = parseInt(el.dataset.target, 10);
const duration = 2000;
const start = performance.now();
function tick(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
el.textContent = Math.round(target * eased).toLocaleString('fr-FR');
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
statFills.forEach((el) => {
const w = el.dataset.width;
setTimeout(() => { el.style.width = w + '%'; }, 200);
});
}
const statsSection = document.getElementById('stats');
const statsObserver = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
animateCounters();
statsObserver.unobserve(statsSection);
}
},
{ threshold: 0.3 }
);
statsObserver.observe(statsSection);
// ─── Contact Form ───
const contactForm = document.getElementById('contact-form');
contactForm.addEventListener('submit', (e) => {
e.preventDefault();
showToast('✅ Message envoyé avec succès !');
contactForm.reset();
});
// ─── Toast ───
const toastEl = document.getElementById('toast');
let toastTimeout;
function showToast(msg) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => toastEl.classList.remove('show'), 3000);
}
// ─── PWA Install Prompt ───
let deferredPrompt;
const installPrompt = document.getElementById('install-prompt');
const installBtn = document.getElementById('install-btn');
const dismissInstall = document.getElementById('dismiss-install');
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e; deferredPrompt = e;
// Update UI to notify the user they can add to home screen installPrompt.hidden = false;
if(installBtn) { });
installBtn.hidden = false;
}
});
if(installBtn) {
installBtn.addEventListener('click', async () => { installBtn.addEventListener('click', async () => {
if (deferredPrompt !== null) { if (!deferredPrompt) return;
// Show the install prompt
deferredPrompt.prompt(); deferredPrompt.prompt();
// Wait for the user to respond to the prompt
const { outcome } = await deferredPrompt.userChoice; const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') { if (outcome === 'accepted') showToast('📲 Application installée !');
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
// We've used the prompt, and can't use it again, throw it away
deferredPrompt = null; deferredPrompt = null;
installBtn.hidden = true; installPrompt.hidden = true;
}
}); });
}
// Interactive element animation dismissInstall.addEventListener('click', () => {
const pulseElement = document.getElementById('pulseElement'); installPrompt.hidden = true;
if(pulseElement) {
pulseElement.addEventListener('click', () => {
pulseElement.style.transform = 'scale(0.9)';
setTimeout(() => {
pulseElement.style.transform = 'scale(1)';
const randomColor = Math.floor(Math.random()*16777215).toString(16);
pulseElement.style.background = `#${randomColor}`;
}, 150);
}); });
}
// ─── Service Worker Registration ───
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then((reg) => console.log('SW registered:', reg.scope))
.catch((err) => console.warn('SW registration failed:', err));
});
}
})();

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="50%" style="stop-color:#a855f7;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ec4899;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="512" height="512" rx="100" fill="url(#grad)" />
<path d="M256 120 L380 340 L132 340 Z" fill="white" />
</svg>

Before

Width:  |  Height:  |  Size: 533 B

BIN
icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

BIN
icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

View File

@@ -1,89 +1,312 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f172a" /> <meta name="description" content="Amusement — Showcase de visuels frontend modernes, animations et effets premium." />
<title>Amusement - Stylish PWA Demo</title> <meta name="theme-color" content="#0a0a1a" />
<meta name="description" content="A generic frontend demo showcasing beautiful, modern web design and PWA capabilities." /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- Fonts --> <title>Amusement — Frontend Visual Showcase</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Outfit:wght@400;600;800&display=swap" rel="stylesheet">
<!-- Styles --> <link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="./style.css" /> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>" />
<link rel="apple-touch-icon" href="icons/icon-192.png" />
<!-- PWA Manifest & Icons --> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="manifest" href="./manifest.json" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="icon" type="image/svg+xml" href="./icon.svg" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="apple-touch-icon" href="./icon.svg" />
<link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>
<div class="background-effects"> <!-- ─── Cursor glow effect ─── -->
<div class="blob blob-1"></div> <div id="cursor-glow" aria-hidden="true"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<main class="app-container"> <!-- ─── Floating particles background ─── -->
<header class="glass-header"> <canvas id="particles-canvas" aria-hidden="true"></canvas>
<div class="logo">
<span class="logo-icon"></span>
<h1>Amusement</h1>
</div>
<nav>
<button class="nav-btn active">Home</button>
<button class="nav-btn">Features</button>
<button class="nav-btn primary">Get Started</button>
</nav>
</header>
<section class="hero-section"> <!-- ─── Navigation ─── -->
<div class="hero-content"> <nav id="main-nav" class="glass-nav" role="navigation">
<div class="badge">Next-Gen Experience</div> <div class="nav-brand">
<h2 class="display-text">Stunning visuals,<br/> <span class="gradient-text">Zero Compromises.</span></h2> <span class="brand-icon">🎨</span>
<p class="subtitle">Experience a breathtaking Progressive Web App designed with modern aesthetics, glassmorphism, and seamless interactions.</p> <span class="brand-text">Amusement</span>
<div class="action-group">
<button class="btn btn-primary" id="installBtn" hidden>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Install App
</button>
<button class="btn btn-secondary">Explore Demo</button>
</div> </div>
<div class="nav-links">
<a href="#hero" class="nav-link active" data-section="hero">Accueil</a>
<a href="#cards" class="nav-link" data-section="cards">Cartes</a>
<a href="#stats" class="nav-link" data-section="stats">Stats</a>
<a href="#gallery" class="nav-link" data-section="gallery">Galerie</a>
<a href="#contact" class="nav-link" data-section="contact">Contact</a>
</div> </div>
<button id="nav-toggle" class="nav-toggle" aria-label="Menu">
<div class="hero-visual">
<div class="glass-card main-card">
<div class="card-header">
<div class="dots">
<span></span><span></span><span></span> <span></span><span></span><span></span>
</button>
</nav>
<!-- ─── Mobile menu ─── -->
<div id="mobile-menu" class="mobile-menu glass-panel">
<a href="#hero" class="mobile-link" data-section="hero">Accueil</a>
<a href="#cards" class="mobile-link" data-section="cards">Cartes</a>
<a href="#stats" class="mobile-link" data-section="stats">Stats</a>
<a href="#gallery" class="mobile-link" data-section="gallery">Galerie</a>
<a href="#contact" class="mobile-link" data-section="contact">Contact</a>
</div>
<main>
<!-- ─── Hero Section ─── -->
<section id="hero" class="section hero-section">
<div class="hero-content">
<div class="hero-badge">✨ Frontend Visual Showcase</div>
<h1 class="hero-title">
<span class="title-line">Expériences</span>
<span class="title-line gradient-text">Visuelles</span>
<span class="title-line">Modernes</span>
</h1>
<p class="hero-description">
Une collection de composants UI premium avec glassmorphism,
animations fluides et design dark-mode raffiné.
</p>
<div class="hero-actions">
<a href="#cards" class="btn btn-primary">
<span>Explorer</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<a href="#contact" class="btn btn-ghost">En savoir plus</a>
</div> </div>
</div> </div>
<div class="card-body"> <div class="hero-visual">
<div class="skeleton-line full"></div> <div class="floating-card card-1 glass-card">
<div class="skeleton-line medium"></div> <div class="mini-chart"></div>
<span>Performances</span>
</div>
<div class="floating-card card-2 glass-card">
<div class="mini-bars"></div>
<span>Analytics</span>
</div>
<div class="floating-card card-3 glass-card">
<div class="mini-ring"></div>
<span>Engagement</span>
</div>
</div>
</section>
<!-- ─── Feature Cards Section ─── -->
<section id="cards" class="section cards-section">
<div class="section-header">
<span class="section-tag">Composants</span>
<h2 class="section-title">Cartes <span class="gradient-text">Interactives</span></h2>
<p class="section-subtitle">Survolez pour découvrir les effets de profondeur et de lumière</p>
</div>
<div class="cards-grid">
<article class="feature-card glass-card" id="card-design">
<div class="card-glow"></div>
<div class="card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<h3 class="card-title">Design System</h3>
<p class="card-text">Tokens de design cohérents, palette de couleurs harmonieuse et typographie soignée.</p>
<div class="card-metrics">
<div class="metric"><span class="metric-value">24</span><span class="metric-label">composants</span></div>
<div class="metric"><span class="metric-value">8</span><span class="metric-label">variantes</span></div>
</div>
</article>
<article class="feature-card glass-card" id="card-motion">
<div class="card-glow"></div>
<div class="card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<h3 class="card-title">Micro-Animations</h3>
<p class="card-text">Transitions fluides et animations GPU-accélérées pour une expérience vivante.</p>
<div class="card-metrics">
<div class="metric"><span class="metric-value">60</span><span class="metric-label">fps</span></div>
<div class="metric"><span class="metric-value">&lt;16</span><span class="metric-label">ms</span></div>
</div>
</article>
<article class="feature-card glass-card" id="card-glass">
<div class="card-glow"></div>
<div class="card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
</div>
<h3 class="card-title">Glassmorphism</h3>
<p class="card-text">Effets de verre dépoli avec transparence, blur et bordures lumineuses.</p>
<div class="card-metrics">
<div class="metric"><span class="metric-value">CSS</span><span class="metric-label">natif</span></div>
<div class="metric"><span class="metric-value">GPU</span><span class="metric-label">accel.</span></div>
</div>
</article>
<article class="feature-card glass-card" id="card-responsive">
<div class="card-glow"></div>
<div class="card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</div>
<h3 class="card-title">Responsive</h3>
<p class="card-text">Layouts fluides qui s'adaptent à tous les écrans, mobile-first et performants.</p>
<div class="card-metrics">
<div class="metric"><span class="metric-value">100</span><span class="metric-label">score</span></div>
<div class="metric"><span class="metric-value">PWA</span><span class="metric-label">ready</span></div>
</div>
</article>
</div>
</section>
<!-- ─── Stats Section ─── -->
<section id="stats" class="section stats-section">
<div class="section-header">
<span class="section-tag">Métriques</span>
<h2 class="section-title">Statistiques <span class="gradient-text">en Direct</span></h2>
<p class="section-subtitle">Compteurs animés avec intersection observer</p>
</div>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-box"> <div class="stat-card glass-card">
<span class="stat-value">99%</span> <span class="stat-number" data-target="1284">0</span>
<span class="stat-label">Performance</span> <span class="stat-label">Composants créés</span>
<div class="stat-bar"><div class="stat-fill" data-width="87"></div></div>
</div> </div>
<div class="stat-box"> <div class="stat-card glass-card">
<span class="stat-value">PWA</span> <span class="stat-number" data-target="99">0</span>
<span class="stat-label">Native Feel</span> <span class="stat-label">Performance Score</span>
<div class="stat-bar"><div class="stat-fill" data-width="99"></div></div>
</div>
<div class="stat-card glass-card">
<span class="stat-number" data-target="42">0</span>
<span class="stat-label">Animations fluides</span>
<div class="stat-bar"><div class="stat-fill" data-width="72"></div></div>
</div>
<div class="stat-card glass-card">
<span class="stat-number" data-target="365">0</span>
<span class="stat-label">Jours d'uptime</span>
<div class="stat-bar"><div class="stat-fill" data-width="100"></div></div>
</div> </div>
</div> </div>
<div class="interactive-element" id="pulseElement"> </section>
Tap to Interact
<!-- ─── Gallery Section ─── -->
<section id="gallery" class="section gallery-section">
<div class="section-header">
<span class="section-tag">Visuels</span>
<h2 class="section-title">Galerie <span class="gradient-text">Immersive</span></h2>
<p class="section-subtitle">Grille adaptative avec effets de survol et transitions</p>
</div> </div>
<div class="gallery-grid">
<div class="gallery-item glass-card" style="--hue: 260;">
<div class="gallery-visual">
<div class="abstract-shape shape-a"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Gradient Mesh</span>
</div>
</div>
<div class="gallery-item gallery-wide glass-card" style="--hue: 320;">
<div class="gallery-visual">
<div class="abstract-shape shape-b"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Aurora Effect</span>
</div>
</div>
<div class="gallery-item glass-card" style="--hue: 180;">
<div class="gallery-visual">
<div class="abstract-shape shape-c"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Fluid Blob</span>
</div>
</div>
<div class="gallery-item glass-card" style="--hue: 30;">
<div class="gallery-visual">
<div class="abstract-shape shape-d"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Neon Glow</span>
</div>
</div>
<div class="gallery-item gallery-wide glass-card" style="--hue: 140;">
<div class="gallery-visual">
<div class="abstract-shape shape-e"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Particle Wave</span>
</div>
</div>
<div class="gallery-item glass-card" style="--hue: 210;">
<div class="gallery-visual">
<div class="abstract-shape shape-f"></div>
</div>
<div class="gallery-overlay">
<span class="gallery-tag">Glass Prism</span>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- ─── Contact / CTA Section ─── -->
<section id="contact" class="section contact-section">
<div class="contact-container glass-card">
<div class="contact-info">
<span class="section-tag">Contact</span>
<h2 class="section-title">Restons en <span class="gradient-text">Contact</span></h2>
<p class="contact-text">Envoyez un message pour en savoir plus sur ces composants et techniques frontend.</p>
<div class="contact-details">
<div class="contact-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<span>hello@amusement.dev</span>
</div>
<div class="contact-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
<span>Paris, France</span>
</div>
</div>
</div>
<form id="contact-form" class="contact-form" onsubmit="return false;">
<div class="form-group">
<label for="name">Nom</label>
<input type="text" id="name" class="form-input" placeholder="Votre nom" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" class="form-input" placeholder="votre@email.com" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" class="form-input" rows="4" placeholder="Votre message..." required></textarea>
</div>
<button type="submit" class="btn btn-primary btn-full" id="submit-btn">
<span>Envoyer</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>
</button>
</form>
</div>
</section>
</main> </main>
<script src="./app.js" type="module"></script> <!-- ─── Footer ─── -->
<footer class="site-footer glass-nav">
<div class="footer-content">
<div class="footer-brand">
<span class="brand-icon">🎨</span>
<span>Amusement</span>
</div>
<p class="footer-copy">© 2026 Amusement PWA — Frontend Visual Showcase</p>
<div class="footer-links">
<a href="#hero">Accueil</a>
<a href="#cards">Cartes</a>
<a href="#gallery">Galerie</a>
</div>
</div>
</footer>
<!-- ─── Toast notification ─── -->
<div id="toast" class="toast" role="alert" aria-live="polite"></div>
<!-- ─── Install PWA prompt ─── -->
<div id="install-prompt" class="install-prompt glass-card" hidden>
<span>📲 Installer l'application</span>
<button id="install-btn" class="btn btn-primary btn-sm">Installer</button>
<button id="dismiss-install" class="btn-dismiss" aria-label="Fermer"></button>
</div>
<script src="app.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,16 +1,23 @@
{ {
"name": "Amusement Dashboard", "name": "Amusement — Visual Showcase",
"short_name": "Amusement", "short_name": "Amusement",
"description": "A stunning visual demo and Progressive Web App template", "description": "Showcase de visuels frontend modernes avec glassmorphism, animations et design premium.",
"start_url": "/index.html", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0f172a", "background_color": "#0a0a1a",
"theme_color": "#0f172a", "theme_color": "#0a0a1a",
"orientation": "any",
"icons": [ "icons": [
{ {
"src": "./icon.svg", "src": "icons/icon-192.png",
"sizes": "192x192 512x512", "sizes": "192x192",
"type": "image/svg+xml", "type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
} }
] ]

33
nginx.conf Normal file
View File

@@ -0,0 +1,33 @@
server {
listen 2080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache static assets
location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Service worker - no cache
location = /sw.js {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,13 +0,0 @@
{
"name": "amusement-pwa-template",
"version": "1.0.0",
"description": "Stylish PWA Demo",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

1112
style.css

File diff suppressed because it is too large Load Diff

67
sw.js
View File

@@ -1,48 +1,57 @@
const CACHE_NAME = 'amusement-pwa-cache-v1'; /* ════════════════════════════════════════════════════
const urlsToCache = [ AMUSEMENT — Service Worker
Cache-first strategy for offline PWA support
════════════════════════════════════════════════════ */
const CACHE_NAME = 'amusement-v1';
const ASSETS = [
'./', './',
'./index.html', './index.html',
'./style.css', './style.css',
'./app.js', './app.js',
'./manifest.json', './manifest.json',
'./icon.svg',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Outfit:wght@400;600;800&display=swap'
]; ];
self.addEventListener('install', event => { // Install: pre-cache core assets
self.addEventListener('install', (event) => {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
.then(cache => {
return cache.addAll(urlsToCache);
})
); );
self.skipWaiting();
}); });
self.addEventListener('fetch', event => { // Activate: clean old caches
event.respondWith( self.addEventListener('activate', (event) => {
caches.match(event.request) event.waitUntil(
.then(response => { caches.keys().then((keys) =>
// Cache hit - return response Promise.all(
if (response) { keys
return response; .filter((k) => k !== CACHE_NAME)
} .map((k) => caches.delete(k))
return fetch(event.request); )
}
) )
); );
self.clients.claim();
}); });
self.addEventListener('activate', event => { // Fetch: cache-first, fallback to network
const cacheWhitelist = [CACHE_NAME]; self.addEventListener('fetch', (event) => {
event.waitUntil( event.respondWith(
caches.keys().then(cacheNames => { caches.match(event.request).then((cached) => {
return Promise.all( if (cached) return cached;
cacheNames.map(cacheName => { return fetch(event.request).then((response) => {
if (cacheWhitelist.indexOf(cacheName) === -1) { if (!response || response.status !== 200 || response.type !== 'basic') {
return caches.delete(cacheName); return response;
}
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
return response;
});
}).catch(() => {
// If both cache and network fail, return offline fallback
if (event.request.mode === 'navigate') {
return caches.match('./index.html');
} }
}) })
); );
})
);
}); });