feat: PWA template - premium dark UI with glassmorphism, particles, and Docker deployment
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal 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;"]
|
||||||
289
app.js
289
app.js
@@ -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';
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// PWA Install Prompt Logic
|
// card glow follow
|
||||||
let deferredPrompt;
|
document.querySelectorAll('.feature-card').forEach((card) => {
|
||||||
const installBtn = document.getElementById('installBtn');
|
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 + '%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', (e) => {
|
// ─── Particles Canvas ───
|
||||||
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
const canvas = document.getElementById('particles-canvas');
|
||||||
e.preventDefault();
|
const ctx = canvas.getContext('2d');
|
||||||
// Stash the event so it can be triggered later.
|
let particles = [];
|
||||||
deferredPrompt = e;
|
const PARTICLE_COUNT = 60;
|
||||||
// Update UI to notify the user they can add to home screen
|
|
||||||
if(installBtn) {
|
function resizeCanvas() {
|
||||||
installBtn.hidden = false;
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
}
|
}
|
||||||
});
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
|
||||||
if(installBtn) {
|
class Particle {
|
||||||
installBtn.addEventListener('click', async () => {
|
constructor() { this.reset(); }
|
||||||
if (deferredPrompt !== null) {
|
reset() {
|
||||||
// Show the install prompt
|
this.x = Math.random() * canvas.width;
|
||||||
deferredPrompt.prompt();
|
this.y = Math.random() * canvas.height;
|
||||||
// Wait for the user to respond to the prompt
|
this.size = Math.random() * 2 + 0.5;
|
||||||
const { outcome } = await deferredPrompt.userChoice;
|
this.speedX = (Math.random() - 0.5) * 0.3;
|
||||||
if (outcome === 'accepted') {
|
this.speedY = (Math.random() - 0.5) * 0.3;
|
||||||
console.log('User accepted the install prompt');
|
this.opacity = Math.random() * 0.5 + 0.1;
|
||||||
} else {
|
this.hue = 260 + Math.random() * 80; // purple-pink range
|
||||||
console.log('User dismissed the install prompt');
|
|
||||||
}
|
|
||||||
// We've used the prompt, and can't use it again, throw it away
|
|
||||||
deferredPrompt = null;
|
|
||||||
installBtn.hidden = true;
|
|
||||||
}
|
}
|
||||||
});
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Interactive element animation
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
const pulseElement = document.getElementById('pulseElement');
|
particles.push(new Particle());
|
||||||
if(pulseElement) {
|
}
|
||||||
pulseElement.addEventListener('click', () => {
|
|
||||||
pulseElement.style.transform = 'scale(0.9)';
|
function drawLines() {
|
||||||
setTimeout(() => {
|
for (let i = 0; i < particles.length; i++) {
|
||||||
pulseElement.style.transform = 'scale(1)';
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
const randomColor = Math.floor(Math.random()*16777215).toString(16);
|
const dx = particles[i].x - particles[j].x;
|
||||||
pulseElement.style.background = `#${randomColor}`;
|
const dy = particles[i].y - particles[j].y;
|
||||||
}, 150);
|
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();
|
||||||
|
deferredPrompt = e;
|
||||||
|
installPrompt.hidden = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
installBtn.addEventListener('click', async () => {
|
||||||
|
if (!deferredPrompt) return;
|
||||||
|
deferredPrompt.prompt();
|
||||||
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
if (outcome === 'accepted') showToast('📲 Application installée !');
|
||||||
|
deferredPrompt = null;
|
||||||
|
installPrompt.hidden = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
dismissInstall.addEventListener('click', () => {
|
||||||
|
installPrompt.hidden = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
11
icon.svg
11
icon.svg
@@ -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
BIN
icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
BIN
icons/icon-512.png
Normal file
BIN
icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
361
index.html
361
index.html
@@ -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 -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<title>Amusement — Frontend Visual Showcase</title>
|
||||||
<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">
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
<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>" />
|
||||||
<!-- Styles -->
|
<link rel="apple-touch-icon" href="icons/icon-192.png" />
|
||||||
<link rel="stylesheet" href="./style.css" />
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<!-- PWA Manifest & Icons -->
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<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="icon" type="image/svg+xml" href="./icon.svg" />
|
|
||||||
<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>
|
<!-- ─── Floating particles background ─── -->
|
||||||
|
<canvas id="particles-canvas" aria-hidden="true"></canvas>
|
||||||
|
|
||||||
|
<!-- ─── Navigation ─── -->
|
||||||
|
<nav id="main-nav" class="glass-nav" role="navigation">
|
||||||
|
<div class="nav-brand">
|
||||||
|
<span class="brand-icon">🎨</span>
|
||||||
|
<span class="brand-text">Amusement</span>
|
||||||
|
</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>
|
||||||
|
<button id="nav-toggle" class="nav-toggle" aria-label="Menu">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<main class="app-container">
|
<main>
|
||||||
<header class="glass-header">
|
<!-- ─── Hero Section ─── -->
|
||||||
<div class="logo">
|
<section id="hero" class="section hero-section">
|
||||||
<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">
|
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="badge">Next-Gen Experience</div>
|
<div class="hero-badge">✨ Frontend Visual Showcase</div>
|
||||||
<h2 class="display-text">Stunning visuals,<br/> <span class="gradient-text">Zero Compromises.</span></h2>
|
<h1 class="hero-title">
|
||||||
<p class="subtitle">Experience a breathtaking Progressive Web App designed with modern aesthetics, glassmorphism, and seamless interactions.</p>
|
<span class="title-line">Expériences</span>
|
||||||
|
<span class="title-line gradient-text">Visuelles</span>
|
||||||
<div class="action-group">
|
<span class="title-line">Modernes</span>
|
||||||
<button class="btn btn-primary" id="installBtn" hidden>
|
</h1>
|
||||||
<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>
|
<p class="hero-description">
|
||||||
Install App
|
Une collection de composants UI premium avec glassmorphism,
|
||||||
</button>
|
animations fluides et design dark-mode raffiné.
|
||||||
<button class="btn btn-secondary">Explore Demo</button>
|
</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="hero-visual">
|
<div class="hero-visual">
|
||||||
<div class="glass-card main-card">
|
<div class="floating-card card-1 glass-card">
|
||||||
<div class="card-header">
|
<div class="mini-chart"></div>
|
||||||
<div class="dots">
|
<span>Performances</span>
|
||||||
<span></span><span></span><span></span>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div class="card-body">
|
<h3 class="card-title">Design System</h3>
|
||||||
<div class="skeleton-line full"></div>
|
<p class="card-text">Tokens de design cohérents, palette de couleurs harmonieuse et typographie soignée.</p>
|
||||||
<div class="skeleton-line medium"></div>
|
<div class="card-metrics">
|
||||||
<div class="stats-grid">
|
<div class="metric"><span class="metric-value">24</span><span class="metric-label">composants</span></div>
|
||||||
<div class="stat-box">
|
<div class="metric"><span class="metric-value">8</span><span class="metric-label">variantes</span></div>
|
||||||
<span class="stat-value">99%</span>
|
</div>
|
||||||
<span class="stat-label">Performance</span>
|
</article>
|
||||||
</div>
|
<article class="feature-card glass-card" id="card-motion">
|
||||||
<div class="stat-box">
|
<div class="card-glow"></div>
|
||||||
<span class="stat-value">PWA</span>
|
<div class="card-icon">
|
||||||
<span class="stat-label">Native Feel</span>
|
<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>
|
</div>
|
||||||
</div>
|
<h3 class="card-title">Micro-Animations</h3>
|
||||||
<div class="interactive-element" id="pulseElement">
|
<p class="card-text">Transitions fluides et animations GPU-accélérées pour une expérience vivante.</p>
|
||||||
Tap to Interact
|
<div class="card-metrics">
|
||||||
</div>
|
<div class="metric"><span class="metric-value">60</span><span class="metric-label">fps</span></div>
|
||||||
|
<div class="metric"><span class="metric-value"><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="stat-card glass-card">
|
||||||
|
<span class="stat-number" data-target="1284">0</span>
|
||||||
|
<span class="stat-label">Composants créés</span>
|
||||||
|
<div class="stat-bar"><div class="stat-fill" data-width="87"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card glass-card">
|
||||||
|
<span class="stat-number" data-target="99">0</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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ─── 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 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>
|
||||||
|
|||||||
@@ -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
33
nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
package.json
13
package.json
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
sw.js
69
sw.js
@@ -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');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user