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