Initialize Next.js project with essential configurations and components
- Added .gitignore to exclude unnecessary files and directories. - Created next.config.js for Next.js configuration. - Set up package.json and package-lock.json with dependencies including Next.js, React, and TypeScript. - Implemented Tailwind CSS for styling with a dedicated tailwind.config.ts. - Developed core application structure including layout, pages, and components for header, footer, and various sections (Hero, Services, Pricing, etc.). - Integrated contact form functionality using nodemailer for email handling. - Established global styles in globals.css and added animations with Framer Motion. - Included README.md for project documentation and setup instructions.
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Runlock.re - Site Web Next.js
|
||||||
|
|
||||||
|
Site web moderne pour Runlock.re, expert Vaultwarden à la Réunion.
|
||||||
|
|
||||||
|
## 🚀 Technologies
|
||||||
|
|
||||||
|
- **Next.js 14** - Framework React
|
||||||
|
- **TypeScript** - Typage statique
|
||||||
|
- **Tailwind CSS** - Styling
|
||||||
|
- **Framer Motion** - Animations
|
||||||
|
- **Lucide React** - Icônes
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installer les dépendances
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Lancer le serveur de développement
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build pour la production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Lancer en production
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Fonctionnalités
|
||||||
|
|
||||||
|
- ✨ Animations fluides avec Framer Motion
|
||||||
|
- 🎯 Design moderne orienté sécurité
|
||||||
|
- 📱 Responsive design
|
||||||
|
- ⚡ Performance optimisée
|
||||||
|
- 🔒 Thème sombre sécurisé
|
||||||
|
|
||||||
|
## 📄 Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── app/
|
||||||
|
│ ├── layout.tsx # Layout principal
|
||||||
|
│ ├── page.tsx # Page d'accueil
|
||||||
|
│ └── globals.css # Styles globaux
|
||||||
|
├── components/
|
||||||
|
│ ├── Header.tsx # Navigation
|
||||||
|
│ ├── Footer.tsx # Pied de page
|
||||||
|
│ └── sections/ # Sections de la page
|
||||||
|
│ ├── Hero.tsx
|
||||||
|
│ ├── Stats.tsx
|
||||||
|
│ ├── Services.tsx
|
||||||
|
│ ├── Encryption.tsx
|
||||||
|
│ ├── Timeline.tsx
|
||||||
|
│ ├── Pricing.tsx
|
||||||
|
│ ├── FAQ.tsx
|
||||||
|
│ └── Contact.tsx
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Déploiement
|
||||||
|
|
||||||
|
Le site peut être déployé sur :
|
||||||
|
- Vercel (recommandé pour Next.js)
|
||||||
|
- Netlify
|
||||||
|
- Tout hébergeur supportant Node.js
|
||||||
|
|
||||||
169
app/api/contact/route.ts
Normal file
169
app/api/contact/route.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, email, company, service, message } = body
|
||||||
|
|
||||||
|
// Validation des champs requis
|
||||||
|
if (!name || !email || !service || !message) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Tous les champs requis doivent être remplis.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation de l'email
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Adresse email invalide.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration du transporteur SMTP OVH
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST || 'ssl0.ovh.net',
|
||||||
|
port: parseInt(process.env.SMTP_PORT || '465'),
|
||||||
|
secure: true, // true pour le port 465, false pour les autres ports
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER, // L'email d'envoi OVH
|
||||||
|
pass: process.env.SMTP_PASSWORD, // Le mot de passe de l'email OVH
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration de l'email
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||||
|
to: 'contact@runlock.re',
|
||||||
|
replyTo: email,
|
||||||
|
subject: `[Runlock.re] Nouvelle demande de contact - ${service}`,
|
||||||
|
html: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #00ff88;
|
||||||
|
border-bottom: 2px solid #00ff88;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.info-row {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-left: 4px solid #00ff88;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.message-box {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Nouvelle demande de contact</h1>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Nom :</span>
|
||||||
|
<span>${name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Email :</span>
|
||||||
|
<span><a href="mailto:${email}">${email}</a></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${company ? `
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Entreprise :</span>
|
||||||
|
<span>${company}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Service :</span>
|
||||||
|
<span>${service}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-box">
|
||||||
|
<strong>Message :</strong><br><br>
|
||||||
|
${message.replace(/\n/g, '<br>')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Cet email a été envoyé depuis le formulaire de contact de runlock.re</p>
|
||||||
|
<p>Vous pouvez répondre directement à cet email pour contacter ${name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
text: `
|
||||||
|
Nouvelle demande de contact - Runlock.re
|
||||||
|
|
||||||
|
Nom: ${name}
|
||||||
|
Email: ${email}
|
||||||
|
${company ? `Entreprise: ${company}\n` : ''}
|
||||||
|
Service: ${service}
|
||||||
|
|
||||||
|
Message:
|
||||||
|
${message}
|
||||||
|
|
||||||
|
---
|
||||||
|
Cet email a été envoyé depuis le formulaire de contact de runlock.re
|
||||||
|
Vous pouvez répondre directement à cet email pour contacter ${name}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoi de l'email
|
||||||
|
await transporter.sendMail(mailOptions)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Votre message a été envoyé avec succès !' },
|
||||||
|
{ status: 200 }
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de l\'envoi de l\'email:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Une erreur est survenue lors de l\'envoi de votre message. Veuillez réessayer.' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
114
app/globals.css
Normal file
114
app/globals.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-security-darker text-gray-100 antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.gradient-text {
|
||||||
|
@apply bg-gradient-to-r from-security-accent via-green-400 to-emerald-400 bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-bg {
|
||||||
|
@apply bg-gradient-to-br from-security-darker via-security-dark to-security-darker;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-glow {
|
||||||
|
box-shadow: 0 0 30px rgba(0, 255, 136, 0.3), 0 0 60px rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
@apply transition-all duration-300 hover:scale-105 hover:shadow-2xl hover:shadow-security-accent/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-padding {
|
||||||
|
@apply py-24 px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-effect {
|
||||||
|
@apply bg-security-card/80 backdrop-blur-lg border border-security-border/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-border {
|
||||||
|
@apply border-2 border-security-accent/40 shadow-lg shadow-security-accent/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-dark {
|
||||||
|
@apply bg-security-card border border-security-border rounded-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations de fond */
|
||||||
|
.animated-grid {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(26, 26, 36, 0.3) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(26, 26, 36, 0.3) 1px, transparent 1px);
|
||||||
|
background-size: 4rem 4rem;
|
||||||
|
animation: gridMove 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-blob {
|
||||||
|
animation: float 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-glow {
|
||||||
|
animation: pulseGlow 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-shift {
|
||||||
|
animation: gradientShift 8s ease infinite;
|
||||||
|
background-size: 200% 200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gridMove {
|
||||||
|
0% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 4rem 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px) translateX(0px);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translateY(-30px) translateX(20px);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translateY(20px) translateX(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulseGlow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
32
app/layout.tsx
Normal file
32
app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
import Footer from '@/components/Footer'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Runlock.re - Expert Vaultwarden à la Réunion | Sécurisez vos mots de passe',
|
||||||
|
description: 'Expert Vaultwarden à la Réunion - Hébergement sécurisé, installation NAS et Mini PC. Solutions de gestion de mots de passe pour entreprises 974.',
|
||||||
|
keywords: 'Vaultwarden, Réunion, gestionnaire de mots de passe, sécurité, NAS, Bitwarden',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="fr">
|
||||||
|
<body className={inter.className}>
|
||||||
|
<Header />
|
||||||
|
<main className="min-h-screen">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
22
app/page.tsx
Normal file
22
app/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Hero from '@/components/sections/Hero'
|
||||||
|
import Stats from '@/components/sections/Stats'
|
||||||
|
import Services from '@/components/sections/Services'
|
||||||
|
import Encryption from '@/components/sections/Encryption'
|
||||||
|
import Pricing from '@/components/sections/Pricing'
|
||||||
|
import FAQ from '@/components/sections/FAQ'
|
||||||
|
import Contact from '@/components/sections/Contact'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<Stats />
|
||||||
|
<Services />
|
||||||
|
<Encryption />
|
||||||
|
<Pricing />
|
||||||
|
<FAQ />
|
||||||
|
<Contact />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
75
components/BackgroundAnimations.tsx
Normal file
75
components/BackgroundAnimations.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
export default function BackgroundAnimations({ variant = 'default' }: { variant?: 'default' | 'grid' | 'particles' | 'gradient' }) {
|
||||||
|
if (variant === 'grid') {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="animated-grid absolute inset-0 opacity-30" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant === 'particles') {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="animated-grid absolute inset-0 opacity-20" />
|
||||||
|
{/* Particules flottantes */}
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="floating-blob absolute rounded-full"
|
||||||
|
style={{
|
||||||
|
width: `${20 + i * 10}px`,
|
||||||
|
height: `${20 + i * 10}px`,
|
||||||
|
background: `radial-gradient(circle, rgba(0, 255, 136, ${0.1 + i * 0.05}) 0%, transparent 70%)`,
|
||||||
|
left: `${10 + i * 15}%`,
|
||||||
|
top: `${10 + i * 12}%`,
|
||||||
|
animationDelay: `${i * 0.5}s`,
|
||||||
|
animationDuration: `${15 + i * 3}s`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant === 'gradient') {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="animated-grid absolute inset-0 opacity-25" />
|
||||||
|
<div
|
||||||
|
className="absolute top-1/4 right-1/4 w-96 h-96 bg-security-accent/5 rounded-full blur-3xl pulse-glow"
|
||||||
|
style={{ animationDelay: '0s' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-1/4 left-1/4 w-96 h-96 bg-green-500/5 rounded-full blur-3xl pulse-glow"
|
||||||
|
style={{ animationDelay: '2s' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-1/2 left-1/2 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl pulse-glow"
|
||||||
|
style={{ animationDelay: '4s', transform: 'translate(-50%, -50%)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default variant
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="animated-grid absolute inset-0 opacity-20" />
|
||||||
|
<div
|
||||||
|
className="floating-blob absolute top-1/4 right-1/4 w-96 h-96 bg-security-accent/5 rounded-full blur-3xl"
|
||||||
|
style={{ animationDelay: '0s' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="floating-blob absolute bottom-1/4 left-1/4 w-96 h-96 bg-green-500/5 rounded-full blur-3xl"
|
||||||
|
style={{ animationDelay: '1s' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="floating-blob absolute top-1/2 right-1/3 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl"
|
||||||
|
style={{ animationDelay: '2s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
130
components/Footer.tsx
Normal file
130
components/Footer.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Shield, Mail, Phone, MapPin } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="bg-security-dark border-t border-security-border">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
|
||||||
|
{/* Brand */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<Shield className="w-8 h-8 text-security-accent" />
|
||||||
|
<span className="text-2xl font-bold text-white">
|
||||||
|
RUNLOCK<span className="text-security-accent">.re</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm leading-relaxed">
|
||||||
|
Solutions de gestion de mots de passe sécurisées pour les entreprises.
|
||||||
|
Protégez vos données avec Vaultwarden.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold mb-4">Navigation</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{['Accueil', 'Services', 'Tarifs', 'FAQ', 'Contact'].map((item) => (
|
||||||
|
<li key={item}>
|
||||||
|
<a
|
||||||
|
href={`#${item.toLowerCase()}`}
|
||||||
|
className="text-gray-400 hover:text-security-accent transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold mb-4">Services</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{['Hébergement Cloud', 'Installation NAS', 'Installation Mini PC', 'Support technique'].map((service) => (
|
||||||
|
<li key={service}>
|
||||||
|
<a
|
||||||
|
href="#services"
|
||||||
|
className="text-gray-400 hover:text-security-accent transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold mb-4">Contact</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li className="flex items-center space-x-2 text-gray-400 text-sm">
|
||||||
|
<Mail className="w-4 h-4 text-security-accent" />
|
||||||
|
<a href="mailto:contact@runlock.re" className="hover:text-security-accent transition-colors">
|
||||||
|
contact@runlock.re
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2 text-gray-400 text-sm">
|
||||||
|
<Phone className="w-4 h-4 text-security-accent" />
|
||||||
|
<a href="tel:0693511558" className="hover:text-security-accent transition-colors">
|
||||||
|
0693 51 15 58
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center space-x-2 text-gray-400 text-sm">
|
||||||
|
<MapPin className="w-4 h-4 text-security-accent" />
|
||||||
|
<span>La Réunion</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-security-border pt-8 mt-8">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
© {currentYear} Runlock.re - Tous droits réservés
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-6 text-sm">
|
||||||
|
<a href="#" className="text-gray-400 hover:text-security-accent transition-colors">
|
||||||
|
Mentions légales
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-400 hover:text-security-accent transition-colors">
|
||||||
|
Politique de confidentialité
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-gray-400 hover:text-security-accent transition-colors">
|
||||||
|
RGPD
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-xs mt-4 text-center">
|
||||||
|
Service édité par: httpdesign.fr
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
124
components/Header.tsx
Normal file
124
components/Header.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Menu, X, Lock, Shield } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false)
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 20)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: '#accueil', label: 'Accueil' },
|
||||||
|
{ href: '#services', label: 'Services' },
|
||||||
|
{ href: '#tarifs', label: 'Tarifs' },
|
||||||
|
{ href: '#faq', label: 'FAQ' },
|
||||||
|
{ href: '#contact', label: 'Contact' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.header
|
||||||
|
initial={{ y: -100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||||
|
isScrolled
|
||||||
|
? 'bg-security-dark/95 backdrop-blur-md shadow-lg shadow-black/50 border-b border-security-border'
|
||||||
|
: 'bg-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<nav className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between h-20">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/" className="flex items-center space-x-3 group">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 bg-security-accent/20 rounded-xl blur-lg group-hover:blur-xl transition-all duration-300" />
|
||||||
|
<Shield className="relative w-10 h-10 text-security-accent group-hover:scale-110 transition-transform duration-300" />
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-bold text-white group-hover:text-security-accent transition-colors">
|
||||||
|
RUNLOCK<span className="gradient-text">.re</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-8">
|
||||||
|
{navItems.map((item, index) => (
|
||||||
|
<motion.a
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="text-gray-300 hover:text-security-accent font-medium transition-colors duration-300 relative group"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-security-accent group-hover:w-full transition-all duration-300" />
|
||||||
|
</motion.a>
|
||||||
|
))}
|
||||||
|
<motion.a
|
||||||
|
href="#contact"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="px-6 py-2.5 bg-gradient-to-r from-security-accent to-green-500 text-black rounded-xl font-semibold hover:shadow-lg hover:shadow-security-accent/50 transition-all duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
Demander un devis
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
className="md:hidden text-gray-300 hover:text-security-accent transition-colors"
|
||||||
|
aria-label="Menu"
|
||||||
|
>
|
||||||
|
{isMobileMenuOpen ? <X size={28} /> : <Menu size={28} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="md:hidden bg-security-dark/98 backdrop-blur-md border-t border-security-border shadow-lg"
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-6 space-y-4">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="block text-gray-300 hover:text-security-accent font-medium transition-colors py-2"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<a
|
||||||
|
href="#contact"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="block w-full text-center px-6 py-3 bg-gradient-to-r from-security-accent to-green-500 text-black rounded-xl font-semibold mt-4"
|
||||||
|
>
|
||||||
|
Demander un devis
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
312
components/sections/Contact.tsx
Normal file
312
components/sections/Contact.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Mail, Phone, MapPin, Clock, Send, Shield, MessageSquare } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
const contactInfo = [
|
||||||
|
{
|
||||||
|
icon: Mail,
|
||||||
|
label: 'Email',
|
||||||
|
value: 'contact@runlock.re',
|
||||||
|
href: 'mailto:contact@runlock.re',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Phone,
|
||||||
|
label: 'Téléphone',
|
||||||
|
value: '0693 51 15 58',
|
||||||
|
href: 'tel:0693511558',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MapPin,
|
||||||
|
label: 'Adresse',
|
||||||
|
value: 'La Réunion',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Clock,
|
||||||
|
label: 'Horaires',
|
||||||
|
value: 'Lundi - Vendredi: 9h00 - 18h00\nWeekend: Fermé',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
'Hébergement Cloud',
|
||||||
|
'Installation NAS',
|
||||||
|
'Installation Mini PC',
|
||||||
|
'Autre',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Contact() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
company: '',
|
||||||
|
service: '',
|
||||||
|
message: '',
|
||||||
|
})
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<{ type: 'success' | 'error' | null; message: string }>({
|
||||||
|
type: null,
|
||||||
|
message: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsSubmitting(true)
|
||||||
|
setSubmitStatus({ type: null, message: '' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setSubmitStatus({
|
||||||
|
type: 'success',
|
||||||
|
message: data.message || 'Votre message a été envoyé avec succès !',
|
||||||
|
})
|
||||||
|
// Réinitialiser le formulaire
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
company: '',
|
||||||
|
service: '',
|
||||||
|
message: '',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSubmitStatus({
|
||||||
|
type: 'error',
|
||||||
|
message: data.error || 'Une erreur est survenue. Veuillez réessayer.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de l\'envoi du formulaire:', error)
|
||||||
|
setSubmitStatus({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Une erreur est survenue lors de l\'envoi de votre message. Veuillez réessayer.',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contact" className="section-padding bg-security-darker relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="gradient" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Centered Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-security-accent/10 border border-security-accent/30 rounded-2xl mb-6">
|
||||||
|
<MessageSquare className="w-10 h-10 text-security-accent" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-4 text-white">
|
||||||
|
<span className="gradient-text">Contactez-nous</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||||
|
Nous sommes là pour répondre à vos questions
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="grid lg:grid-cols-3 gap-8">
|
||||||
|
{/* Left - Contact Info Cards */}
|
||||||
|
<div className="lg:col-span-1 space-y-4">
|
||||||
|
{contactInfo.map((info, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="card-dark p-5 rounded-xl card-hover"
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="w-12 h-12 bg-security-accent/10 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
|
<info.icon className="w-6 h-6 text-security-accent" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-500 mb-1">{info.label}</div>
|
||||||
|
{info.href ? (
|
||||||
|
<a
|
||||||
|
href={info.href}
|
||||||
|
className="text-white hover:text-security-accent transition-colors font-medium text-sm"
|
||||||
|
>
|
||||||
|
{info.value}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="text-white whitespace-pre-line font-medium text-sm">{info.value}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Security Badge */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
className="card-dark p-5 rounded-xl border border-security-accent/20"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
|
<Shield className="w-5 h-5 text-security-accent" />
|
||||||
|
<span className="text-white font-semibold text-sm">Protection anti-spam</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-xs">
|
||||||
|
Vos données sont sécurisées et ne seront jamais partagées.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right - Contact Form */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 30 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="lg:col-span-2 card-dark p-8 rounded-3xl"
|
||||||
|
>
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-6">Envoyez-nous un message</h3>
|
||||||
|
|
||||||
|
{/* Message de statut */}
|
||||||
|
{submitStatus.type && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`p-4 rounded-xl mb-6 ${
|
||||||
|
submitStatus.type === 'success'
|
||||||
|
? 'bg-green-500/10 border border-green-500/30 text-green-400'
|
||||||
|
: 'bg-red-500/10 border border-red-500/30 text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{submitStatus.type === 'success' ? (
|
||||||
|
<Shield className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<MessageSquare className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">{submitStatus.message}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div className="grid md:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-400 mb-2">
|
||||||
|
Nom complet
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="w-full px-4 py-3 bg-security-dark border border-security-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-security-accent transition-colors"
|
||||||
|
placeholder="Votre nom"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-400 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
className="w-full px-4 py-3 bg-security-dark border border-security-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-security-accent transition-colors"
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="company" className="block text-sm font-medium text-gray-400 mb-2">
|
||||||
|
Entreprise
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company"
|
||||||
|
value={formData.company}
|
||||||
|
onChange={(e) => setFormData({ ...formData, company: e.target.value })}
|
||||||
|
className="w-full px-4 py-3 bg-security-dark border border-security-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-security-accent transition-colors"
|
||||||
|
placeholder="Nom de votre entreprise"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="service" className="block text-sm font-medium text-gray-400 mb-2">
|
||||||
|
Service souhaité
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="service"
|
||||||
|
value={formData.service}
|
||||||
|
onChange={(e) => setFormData({ ...formData, service: e.target.value })}
|
||||||
|
className="w-full px-4 py-3 bg-security-dark border border-security-border rounded-xl text-white focus:outline-none focus:border-security-accent transition-colors"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Sélectionnez un service</option>
|
||||||
|
{services.map((service) => (
|
||||||
|
<option key={service} value={service}>
|
||||||
|
{service}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="message" className="block text-sm font-medium text-gray-400 mb-2">
|
||||||
|
Message
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
value={formData.message}
|
||||||
|
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||||
|
rows={5}
|
||||||
|
className="w-full px-4 py-3 bg-security-dark border border-security-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-security-accent transition-colors resize-none"
|
||||||
|
placeholder="Votre message..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
whileHover={!isSubmitting ? { scale: 1.02 } : {}}
|
||||||
|
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
|
||||||
|
className={`w-full py-4 px-6 bg-gradient-to-r from-security-accent to-green-500 text-black rounded-xl font-bold shadow-lg shadow-security-accent/50 hover:shadow-xl transition-all duration-300 flex items-center justify-center space-x-2 ${
|
||||||
|
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{isSubmitting ? 'Envoi en cours...' : 'Envoyer le message'}</span>
|
||||||
|
{!isSubmitting && <Send className="w-5 h-5" />}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
97
components/sections/Encryption.tsx
Normal file
97
components/sections/Encryption.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useInView } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { Lock, Key, Shield, TrendingUp } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
const algorithms = [
|
||||||
|
{
|
||||||
|
name: 'AES-256-GCM',
|
||||||
|
bits: '256',
|
||||||
|
details: 'Rounds 14 • 3.7 GB/s',
|
||||||
|
icon: Lock,
|
||||||
|
gradient: 'from-blue-500 to-cyan-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RSA-4096',
|
||||||
|
bits: '4096',
|
||||||
|
details: 'Primes 2048 bits • Sécurité Ultra',
|
||||||
|
icon: Key,
|
||||||
|
gradient: 'from-purple-500 to-pink-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ECDSA P-384',
|
||||||
|
bits: '384',
|
||||||
|
details: 'Courbe P-384 • Vérification Instantanée',
|
||||||
|
icon: Shield,
|
||||||
|
gradient: 'from-security-accent to-green-500',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Encryption() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section-padding bg-security-dark relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="particles" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Split Layout - Left Text, Right Visual */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||||
|
{/* Left Side - Text Content */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="inline-block px-4 py-2 bg-security-accent/10 border border-security-accent/30 rounded-full mb-6">
|
||||||
|
<span className="text-security-accent text-sm font-semibold">Chiffrement</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-6 text-white">
|
||||||
|
<span className="gradient-text">Avancé</span> pour vos données
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 mb-8 leading-relaxed">
|
||||||
|
Algorithmes cryptographiques de pointe pour protéger vos données avec une sécurité maximale.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-4 text-gray-300">
|
||||||
|
<TrendingUp className="w-6 h-6 text-security-accent" />
|
||||||
|
<span className="text-sm">Performance optimale garantie</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Right Side - Algorithm Cards */}
|
||||||
|
<div ref={ref} className="space-y-6">
|
||||||
|
{algorithms.map((algo, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, x: 50 }}
|
||||||
|
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||||
|
className="card-dark p-6 rounded-2xl card-hover"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`w-16 h-16 bg-gradient-to-br ${algo.gradient} rounded-xl flex items-center justify-center shadow-lg`}>
|
||||||
|
<algo.icon className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-white mb-1">{algo.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{algo.details}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-3xl font-black gradient-text">{algo.bits}</div>
|
||||||
|
<div className="text-xs text-gray-500">bits</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
113
components/sections/FAQ.tsx
Normal file
113
components/sections/FAQ.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ChevronDown, HelpCircle, MessageCircle } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
question: "Qu'est-ce que Vaultwarden ?",
|
||||||
|
answer: "Une implémentation légère et performante du serveur Bitwarden open-source. Permet de gérer vos mots de passe de manière sécurisée avec un chiffrement de bout en bout.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Est-ce que mes données restent chez moi ?',
|
||||||
|
answer: "Oui, avec nos solutions NAS ou Mini PC, vos données restent 100% chez vous. Pour l'hébergement cloud, vos données sont stockées sur nos serveurs sécurisés en France avec chiffrement de bout en bout.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Que se passe-t-il si mon serveur tombe en panne ?',
|
||||||
|
answer: "Notre hébergement cloud garantit 99.9% d'uptime. Pour les installations locales, nous proposons des contrats de support. Les apps Bitwarden fonctionnent aussi en mode offline.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Quelle est la différence entre Bitwarden et Vaultwarden ?',
|
||||||
|
answer: "Vaultwarden est plus léger et consomme moins de ressources, écrit en Rust. Parfaitement compatible avec Bitwarden, avec toutes les fonctionnalités premium incluses.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Proposez-vous une migration depuis d'autres gestionnaires ?",
|
||||||
|
answer: "Oui, nous accompagnons la migration depuis LastPass, 1Password, Dashlane, KeePass et autres. Outils et assistance fournis pour un transfert sécurisé.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Quels sont les prérequis pour l\'installation NAS ?',
|
||||||
|
answer: "NAS compatible (Synology, QNAP, etc.) avec Docker installé, minimum 2 Go de RAM et quelques Go d'espace disque. Nom de domaine et accès réseau requis.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function FAQ() {
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="faq" className="section-padding bg-security-dark relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="default" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Split Layout */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-16 items-start">
|
||||||
|
{/* Left - Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="sticky top-24"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-security-accent/10 border border-security-accent/30 rounded-2xl mb-6">
|
||||||
|
<HelpCircle className="w-10 h-10 text-security-accent" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-6 text-white">
|
||||||
|
Questions <span className="gradient-text">Fréquentes</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 leading-relaxed mb-8">
|
||||||
|
Tout ce que vous devez savoir sur nos solutions Vaultwarden
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-3 text-gray-400">
|
||||||
|
<MessageCircle className="w-5 h-5 text-security-accent" />
|
||||||
|
<span className="text-sm">Besoin d'aide ? Contactez-nous</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Right - FAQ Items */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{faqs.map((faq, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, x: 50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="card-dark rounded-2xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
||||||
|
className="w-full px-6 py-5 flex items-center justify-between text-left hover:bg-security-card/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold text-white pr-8">{faq.question}</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: openIndex === index ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5 text-security-accent" />
|
||||||
|
</motion.div>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{openIndex === index && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-6 pb-5 text-gray-400 leading-relaxed border-t border-security-border">
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
components/sections/Hero.tsx
Normal file
112
components/sections/Hero.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Lock, Shield, ArrowRight, Sparkles, Zap, Globe } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
return (
|
||||||
|
<section id="accueil" className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 gradient-bg">
|
||||||
|
{/* Animated Grid Background */}
|
||||||
|
<BackgroundAnimations variant="gradient" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Top Badge */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="flex justify-center mb-8"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center space-x-2 px-6 py-3 bg-security-card border border-security-border rounded-full">
|
||||||
|
<Zap className="w-4 h-4 text-security-accent" />
|
||||||
|
<span className="text-sm font-semibold text-gray-300">Expert Vaultwarden à la Réunion</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Main Content - Centered Layout */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
|
className="text-6xl md:text-8xl font-black mb-8 leading-tight"
|
||||||
|
>
|
||||||
|
<span className="block text-white mb-4">Sécurisez vos</span>
|
||||||
|
<span className="gradient-text block text-7xl md:text-9xl">mots de passe</span>
|
||||||
|
<span className="block text-4xl md:text-5xl mt-4 text-gray-400 font-light">
|
||||||
|
d'entreprise
|
||||||
|
</span>
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.4 }}
|
||||||
|
className="text-xl md:text-2xl text-gray-400 mb-12 max-w-3xl mx-auto leading-relaxed"
|
||||||
|
>
|
||||||
|
Hébergement sécurisé, installation NAS et Mini PC. Solutions de gestion de mots de passe
|
||||||
|
pour entreprises <span className="text-security-accent font-bold">974</span>.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* CTA Buttons - Horizontal */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.6 }}
|
||||||
|
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
|
||||||
|
>
|
||||||
|
<motion.a
|
||||||
|
href="#contact"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="group px-10 py-5 bg-gradient-to-r from-security-accent to-green-500 text-black rounded-2xl font-bold text-lg shadow-2xl shadow-security-accent/50 flex items-center space-x-3 hover:shadow-security-accent/70 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<span>Voir nos offres</span>
|
||||||
|
<ArrowRight className="w-6 h-6 group-hover:translate-x-2 transition-transform" />
|
||||||
|
</motion.a>
|
||||||
|
<motion.a
|
||||||
|
href="#services"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="px-10 py-5 border-2 border-security-accent text-security-accent rounded-2xl font-bold text-lg hover:bg-security-accent/10 transition-all duration-300"
|
||||||
|
>
|
||||||
|
En savoir plus
|
||||||
|
</motion.a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid - 3 Columns */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.8 }}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto"
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{ icon: Shield, label: 'AES-256', value: 'Chiffrement', color: 'text-security-accent' },
|
||||||
|
{ icon: Lock, label: 'Zero-Knowledge', value: 'Architecture', color: 'text-green-400' },
|
||||||
|
{ icon: Globe, label: 'RGPD', value: 'Conformité 100%', color: 'text-emerald-400' },
|
||||||
|
].map((stat, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 1 + index * 0.2 }}
|
||||||
|
className="p-6 card-dark security-glow card-hover"
|
||||||
|
>
|
||||||
|
<stat.icon className={`w-10 h-10 ${stat.color} mb-4 mx-auto`} />
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{stat.label}</div>
|
||||||
|
<div className="text-sm text-gray-400">{stat.value}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Wave */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-security-dark to-transparent" />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
164
components/sections/Pricing.tsx
Normal file
164
components/sections/Pricing.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useInView } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { Cloud, Server, HardDrive, CheckCircle, ArrowRight, Star } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
icon: Cloud,
|
||||||
|
name: 'Cloud Hébergé',
|
||||||
|
subtitle: 'Hébergement sécurisé chez nous',
|
||||||
|
price: '20',
|
||||||
|
period: '/ mois par utilisateur',
|
||||||
|
badge: null,
|
||||||
|
features: [
|
||||||
|
'5 à 100 utilisateurs',
|
||||||
|
'Stockage 10 à 100 Go',
|
||||||
|
'Sauvegardes quotidiennes',
|
||||||
|
'SSL/TLS inclus',
|
||||||
|
'Support email',
|
||||||
|
'Mises à jour automatiques',
|
||||||
|
],
|
||||||
|
cta: 'Demander un devis',
|
||||||
|
gradient: 'from-blue-500 to-cyan-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: HardDrive,
|
||||||
|
name: 'Installation Mini PC',
|
||||||
|
subtitle: 'Solution clé en main',
|
||||||
|
price: '500',
|
||||||
|
period: ' installation',
|
||||||
|
badge: 'Recommandé',
|
||||||
|
features: [
|
||||||
|
'Matériel inclus',
|
||||||
|
'Installation complète',
|
||||||
|
'Configuration réseau',
|
||||||
|
'Formation sur site',
|
||||||
|
'Support 60 jours inclus',
|
||||||
|
'Garantie matériel 1 an',
|
||||||
|
],
|
||||||
|
cta: 'Demander un devis',
|
||||||
|
gradient: 'from-security-accent to-green-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Server,
|
||||||
|
name: 'Installation NAS',
|
||||||
|
subtitle: 'Sur votre infrastructure',
|
||||||
|
price: '350',
|
||||||
|
period: ' installation',
|
||||||
|
badge: null,
|
||||||
|
features: [
|
||||||
|
'Configuration complète',
|
||||||
|
'Formation personnalisée',
|
||||||
|
'Documentation détaillée',
|
||||||
|
'Certificat SSL',
|
||||||
|
'Support 30 jours inclus',
|
||||||
|
'Support optionnel dispo',
|
||||||
|
],
|
||||||
|
cta: 'Demander un devis',
|
||||||
|
gradient: 'from-purple-500 to-pink-500',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Pricing() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="tarifs" className="section-padding bg-security-darker relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="default" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Section Header - Centered */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<div className="inline-block px-4 py-2 bg-security-accent/10 border border-security-accent/30 rounded-full mb-6">
|
||||||
|
<span className="text-security-accent text-sm font-semibold">Nos Offres</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-6 text-white">
|
||||||
|
Nos <span className="gradient-text">Tarifs</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 leading-relaxed max-w-2xl mx-auto">
|
||||||
|
Des offres transparentes pour tous les budgets, de l'hébergement cloud aux installations sur site
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Pricing Cards - Centered Grid Layout */}
|
||||||
|
<div ref={ref} className="flex justify-center">
|
||||||
|
<div className="grid md:grid-cols-3 gap-8 max-w-7xl">
|
||||||
|
{plans.map((plan, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||||
|
className={`relative ${plan.badge ? 'neon-border' : 'card-dark'} p-10 rounded-3xl card-hover flex flex-col`}
|
||||||
|
>
|
||||||
|
{plan.badge && (
|
||||||
|
<div className="absolute -top-3 left-8 z-10">
|
||||||
|
<span className="px-4 py-1 bg-gradient-to-r from-security-accent to-green-500 text-black text-sm font-bold rounded-full shadow-lg flex items-center space-x-1">
|
||||||
|
<Star className="w-3 h-3" />
|
||||||
|
<span>{plan.badge}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Icon and Price */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className={`w-24 h-24 bg-gradient-to-br ${plan.gradient} rounded-2xl flex items-center justify-center mb-6 shadow-2xl mx-auto`}>
|
||||||
|
<plan.icon className="w-12 h-12 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">À partir de</div>
|
||||||
|
<div className="flex items-baseline justify-center">
|
||||||
|
<span className="text-5xl font-black gradient-text">{plan.price}€</span>
|
||||||
|
<span className="text-gray-400 ml-2 text-base">{plan.period}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title and Description */}
|
||||||
|
<div className="text-center mb-8 flex-grow">
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-3">{plan.name}</h3>
|
||||||
|
<p className="text-gray-400 text-base mb-8">{plan.subtitle}</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<ul className="space-y-4 text-left">
|
||||||
|
{plan.features.map((feature, idx) => (
|
||||||
|
<li key={idx} className="flex items-center space-x-3 text-gray-300">
|
||||||
|
<CheckCircle className="w-5 h-5 text-security-accent flex-shrink-0" />
|
||||||
|
<span className="text-base">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<motion.a
|
||||||
|
href="#contact"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className={`w-full py-4 px-6 rounded-xl font-bold text-center transition-all duration-300 flex items-center justify-center space-x-2 ${
|
||||||
|
plan.badge
|
||||||
|
? 'bg-gradient-to-r from-security-accent to-green-500 text-black shadow-lg shadow-security-accent/50 hover:shadow-xl'
|
||||||
|
: 'bg-security-card border-2 border-security-border text-white hover:border-security-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{plan.cta}</span>
|
||||||
|
<ArrowRight className="w-5 h-5" />
|
||||||
|
</motion.a>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
118
components/sections/Services.tsx
Normal file
118
components/sections/Services.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useInView } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { Cloud, Server, HardDrive, CheckCircle, ArrowRight } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
icon: Cloud,
|
||||||
|
title: 'Hébergement Cloud Vaultwarden Réunion',
|
||||||
|
description: 'Hébergement sécurisé de votre Vaultwarden à la Réunion avec sauvegardes automatiques quotidiennes.',
|
||||||
|
features: ['Sauvegardes automatiques', 'SSL/TLS inclus', 'Mise à jour gérée', 'Support technique'],
|
||||||
|
gradient: 'from-blue-500 to-cyan-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: HardDrive,
|
||||||
|
title: 'Installation Vaultwarden Mini PC Réunion',
|
||||||
|
description: 'Solution compacte clé en main pour entreprises de la Réunion sans NAS.',
|
||||||
|
features: ['Matériel fourni', 'Installation complète', 'Configuration réseau', 'Support 60 jours'],
|
||||||
|
gradient: 'from-security-accent to-green-500',
|
||||||
|
popular: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Server,
|
||||||
|
title: 'Installation Vaultwarden NAS Réunion',
|
||||||
|
description: 'Installation complète de Vaultwarden sur votre NAS. Formation incluse.',
|
||||||
|
features: ['Configuration complète', 'Formation personnalisée', 'Documentation détaillée', 'Support 30 jours'],
|
||||||
|
gradient: 'from-purple-500 to-pink-500',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Services() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="services" className="section-padding bg-security-dark relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="particles" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
{/* Section Header - Centered */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<div className="inline-block px-4 py-2 bg-security-accent/10 border border-security-accent/30 rounded-full mb-6">
|
||||||
|
<span className="text-security-accent text-sm font-semibold">Nos Services</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-6 text-white">
|
||||||
|
Des solutions <span className="gradient-text">adaptées</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto leading-relaxed">
|
||||||
|
Des solutions adaptées à vos besoins pour sécuriser vos mots de passe d'entreprise
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Services - Centered Grid Layout */}
|
||||||
|
<div ref={ref} className="flex justify-center">
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 max-w-7xl">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.15 }}
|
||||||
|
className={`relative ${service.popular ? 'neon-border' : 'card-dark'} p-8 rounded-3xl card-hover flex flex-col`}
|
||||||
|
>
|
||||||
|
{service.popular && (
|
||||||
|
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 z-10">
|
||||||
|
<span className="px-4 py-1 bg-gradient-to-r from-security-accent to-green-500 text-black text-sm font-bold rounded-full shadow-lg">
|
||||||
|
POPULAIRE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex-shrink-0 mb-6">
|
||||||
|
<div className={`w-20 h-20 bg-gradient-to-br ${service.gradient} rounded-2xl flex items-center justify-center shadow-2xl`}>
|
||||||
|
<service.icon className="w-10 h-10 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-3">{service.title}</h3>
|
||||||
|
<p className="text-gray-400 leading-relaxed mb-6 text-sm">{service.description}</p>
|
||||||
|
|
||||||
|
{/* Features - Vertical List */}
|
||||||
|
<ul className="space-y-3 mb-6">
|
||||||
|
{service.features.map((feature, idx) => (
|
||||||
|
<li key={idx} className="flex items-center space-x-2 text-gray-300">
|
||||||
|
<CheckCircle className="w-4 h-4 text-security-accent flex-shrink-0" />
|
||||||
|
<span className="text-sm">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<motion.a
|
||||||
|
href="#contact"
|
||||||
|
whileHover={{ x: 5 }}
|
||||||
|
className="inline-flex items-center space-x-2 text-security-accent font-semibold hover:text-green-400 transition-colors"
|
||||||
|
>
|
||||||
|
<span>Découvrir</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
components/sections/Stats.tsx
Normal file
75
components/sections/Stats.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useInView } from 'framer-motion'
|
||||||
|
import { useRef, useState, useEffect } from 'react'
|
||||||
|
import { Lock, Clock, Ban, CheckCircle } from 'lucide-react'
|
||||||
|
import BackgroundAnimations from '@/components/BackgroundAnimations'
|
||||||
|
|
||||||
|
export default function Stats() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
// Générer un nombre aléatoire pour les tentatives bloquées
|
||||||
|
// Initialiser à 0 pour éviter les erreurs d'hydratation (serv/client doivent correspondre)
|
||||||
|
const [blockedAttempts, setBlockedAttempts] = useState<number>(0)
|
||||||
|
|
||||||
|
// Mettre à jour le nombre aléatoire après le montage et périodiquement
|
||||||
|
useEffect(() => {
|
||||||
|
// Générer le premier nombre aléatoire uniquement côté client
|
||||||
|
const randomValue = Math.floor(Math.random() * 10000)
|
||||||
|
setBlockedAttempts(randomValue)
|
||||||
|
|
||||||
|
// Mettre à jour le nombre aléatoire périodiquement pour donner un effet dynamique
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// Générer un nouveau nombre aléatoire entre 0 et 9999
|
||||||
|
const newRandomValue = Math.floor(Math.random() * 10000)
|
||||||
|
setBlockedAttempts(newRandomValue)
|
||||||
|
}, 5000) // Mise à jour toutes les 5 secondes
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ icon: Clock, value: '99.9', unit: '%', label: 'Uptime', gradient: 'from-blue-500 to-cyan-500', isDynamic: false },
|
||||||
|
{ icon: Lock, value: '256', unit: ' Bit', label: 'Encryption', gradient: 'from-security-accent to-green-500', isDynamic: false },
|
||||||
|
{ icon: Clock, value: '24', unit: '/7', label: 'Support', gradient: 'from-purple-500 to-pink-500', isDynamic: false },
|
||||||
|
{ icon: Ban, value: blockedAttempts.toString(), label: 'Tentatives bloquées', gradient: 'from-red-500 to-orange-500', isDynamic: true },
|
||||||
|
{ icon: CheckCircle, value: '100', unit: '%', label: 'Conformité RGPD', gradient: 'from-emerald-500 to-teal-500', isDynamic: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section-padding bg-security-dark relative overflow-hidden">
|
||||||
|
<BackgroundAnimations variant="grid" />
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div ref={ref} className="grid grid-cols-2 md:grid-cols-5 gap-4 md:gap-6">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 30, rotateY: -15 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0, rotateY: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
className="text-center p-6 card-dark card-hover relative overflow-hidden group"
|
||||||
|
>
|
||||||
|
{/* Gradient Background on Hover */}
|
||||||
|
<div className={`absolute inset-0 bg-gradient-to-br ${stat.gradient} opacity-0 group-hover:opacity-10 transition-opacity duration-300`} />
|
||||||
|
|
||||||
|
<div className={`w-14 h-14 bg-gradient-to-br ${stat.gradient} rounded-xl flex items-center justify-center mx-auto mb-4 relative z-10 shadow-lg`}>
|
||||||
|
<stat.icon className="w-7 h-7 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-2 relative z-10">
|
||||||
|
<span className="text-3xl md:text-4xl font-bold text-white">
|
||||||
|
{stat.isDynamic ? blockedAttempts.toString() : (isInView ? stat.value : '0')}
|
||||||
|
</span>
|
||||||
|
{stat.unit && (
|
||||||
|
<span className="text-xl text-gray-400">{stat.unit}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 font-medium relative z-10">{stat.label}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
129
components/sections/Timeline.tsx
Normal file
129
components/sections/Timeline.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useInView } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { Clock, Database, Key, Shield, Bug, FileText } from 'lucide-react'
|
||||||
|
|
||||||
|
const timelineItems = [
|
||||||
|
{
|
||||||
|
time: '24/7',
|
||||||
|
title: 'Surveillance Continue',
|
||||||
|
status: 'Actif',
|
||||||
|
description: 'Surveillance 24/7 avec détection automatique des menaces',
|
||||||
|
metrics: ['99.9% Uptime', '0 Incidents'],
|
||||||
|
icon: Clock,
|
||||||
|
gradient: 'from-security-accent to-green-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 'T+2min',
|
||||||
|
title: 'Sauvegarde Automatique',
|
||||||
|
status: 'À jour',
|
||||||
|
description: 'Sauvegarde incrémentielle toutes les 2 minutes',
|
||||||
|
metrics: ['100% Intégrité', '3x Redondance'],
|
||||||
|
icon: Database,
|
||||||
|
gradient: 'from-blue-500 to-cyan-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 'T+5min',
|
||||||
|
title: 'Rotation des Clés',
|
||||||
|
status: 'Renouvelé',
|
||||||
|
description: 'Rotation automatique des clés de chiffrement',
|
||||||
|
metrics: ['AES-256', '4096 Bits RSA'],
|
||||||
|
icon: Key,
|
||||||
|
gradient: 'from-purple-500 to-pink-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 'T+10min',
|
||||||
|
title: 'Audit de Sécurité',
|
||||||
|
status: 'Validé',
|
||||||
|
description: 'Vérification automatique des permissions',
|
||||||
|
metrics: ['100% Conformité', '0 Anomalies'],
|
||||||
|
icon: Shield,
|
||||||
|
gradient: 'from-yellow-500 to-orange-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 'T+15min',
|
||||||
|
title: 'Test de Pénétration',
|
||||||
|
status: 'Réussi',
|
||||||
|
description: 'Test automatique des vulnérabilités',
|
||||||
|
metrics: ['0 Vulnérabilités', '100% Protection'],
|
||||||
|
icon: Bug,
|
||||||
|
gradient: 'from-red-500 to-pink-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 'T+30min',
|
||||||
|
title: 'Rapport de Sécurité',
|
||||||
|
status: 'Généré',
|
||||||
|
description: 'Rapport détaillé des activités de sécurité',
|
||||||
|
metrics: ['ISO 27001', 'RGPD Conformité'],
|
||||||
|
icon: FileText,
|
||||||
|
gradient: 'from-teal-500 to-cyan-500',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Timeline() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section-padding bg-security-darker relative overflow-hidden">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Centered Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-16"
|
||||||
|
>
|
||||||
|
<h2 className="text-5xl md:text-6xl font-black mb-4 text-white">
|
||||||
|
Timeline de <span className="gradient-text">Sécurité</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||||
|
Protection continue de votre infrastructure
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Horizontal Timeline - Cards in a Row */}
|
||||||
|
<div ref={ref} className="relative">
|
||||||
|
<div className="grid md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{timelineItems.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
className="card-dark p-6 rounded-2xl card-hover relative"
|
||||||
|
>
|
||||||
|
{/* Time Badge */}
|
||||||
|
<div className={`w-12 h-12 bg-gradient-to-br ${item.gradient} rounded-xl flex items-center justify-center mb-4 shadow-lg`}>
|
||||||
|
<item.icon className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs font-bold text-security-accent mb-2">{item.time}</div>
|
||||||
|
<h3 className="text-lg font-bold text-white mb-2">{item.title}</h3>
|
||||||
|
<p className="text-xs text-gray-400 mb-4">{item.description}</p>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{item.metrics.map((metric, idx) => (
|
||||||
|
<div key={idx} className="text-xs text-gray-500">
|
||||||
|
• {metric}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Badge */}
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<span className="px-2 py-1 bg-security-accent/20 text-security-accent text-xs font-semibold rounded-full">
|
||||||
|
{item.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
lib/stats.ts
Normal file
45
lib/stats.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Utilitaire pour gérer les statistiques de sécurité
|
||||||
|
* Permet d'incrémenter le compteur de tentatives bloquées
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenche un événement pour incrémenter le compteur de tentatives bloquées
|
||||||
|
* Cette fonction peut être appelée depuis n'importe où dans l'application
|
||||||
|
* lorsque une tentative d'intrusion ou d'accès non autorisé est détectée
|
||||||
|
*/
|
||||||
|
export function incrementBlockedAttempts() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const event = new CustomEvent('blocked-attempt')
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nombre actuel de tentatives bloquées depuis le localStorage
|
||||||
|
*/
|
||||||
|
export function getBlockedAttempts(): number {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('runlock_blocked_attempts')
|
||||||
|
if (saved) {
|
||||||
|
const count = parseInt(saved, 10)
|
||||||
|
if (!isNaN(count)) {
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le compteur de tentatives bloquées
|
||||||
|
*/
|
||||||
|
export function resetBlockedAttempts() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('runlock_blocked_attempts', '0')
|
||||||
|
// Déclencher un événement pour mettre à jour l'UI
|
||||||
|
const event = new CustomEvent('blocked-attempt-reset')
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
7
next.config.js
Normal file
7
next.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
||||||
2155
package-lock.json
generated
Normal file
2155
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "runlock-v2",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^10.16.16",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"next": "^14.0.4",
|
||||||
|
"nodemailer": "^7.0.10",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"@types/react": "^18.2.45",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
72
tailwind.config.ts
Normal file
72
tailwind.config.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
dark: '#0a0a0f',
|
||||||
|
darker: '#050508',
|
||||||
|
accent: '#00ff88',
|
||||||
|
accentDark: '#00cc6f',
|
||||||
|
glow: '#00ff88',
|
||||||
|
border: '#1a1a24',
|
||||||
|
card: '#111118',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.6s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.6s ease-out',
|
||||||
|
'slide-down': 'slideDown 0.6s ease-out',
|
||||||
|
'glow': 'glow 2s ease-in-out infinite alternate',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
'0%': { transform: 'translateY(-20px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
glow: {
|
||||||
|
'0%': { boxShadow: '0 0 5px #10b981, 0 0 10px #10b981' },
|
||||||
|
'100%': { boxShadow: '0 0 20px #10b981, 0 0 30px #10b981, 0 0 40px #10b981' },
|
||||||
|
},
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0px)' },
|
||||||
|
'50%': { transform: 'translateY(-20px)' },
|
||||||
|
},
|
||||||
|
shimmer: {
|
||||||
|
'0%': { backgroundPosition: '-1000px 0' },
|
||||||
|
'100%': { backgroundPosition: '1000px 0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
export default config
|
||||||
|
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user