diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts new file mode 100644 index 0000000..111f725 --- /dev/null +++ b/app/api/contact/route.ts @@ -0,0 +1,206 @@ +import { NextResponse } from "next/server"; +import nodemailer from "nodemailer"; + +// Fonction pour vérifier le token Turnstile +async function verifyTurnstileToken(token: string): Promise { + const response = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + secret: process.env.TURNSTILE_SECRET_KEY, + response: token, + }), + } + ); + + const data = await response.json(); + return data.success; +} + +export async function POST(request: Request) { + try { + const { name, email, message, captchaToken } = await request.json(); + + // Validation + if (!name || !email || !message) { + return NextResponse.json( + { error: "Tous les champs sont requis" }, + { status: 400 } + ); + } + + // Vérifier le captcha + if (!captchaToken) { + return NextResponse.json( + { error: "Captcha requis" }, + { status: 400 } + ); + } + + const isCaptchaValid = await verifyTurnstileToken(captchaToken); + + if (!isCaptchaValid) { + return NextResponse.json( + { error: "Captcha invalide. Veuillez réessayer." }, + { status: 400 } + ); + } + + // Configuration du transporteur SMTP + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || "587"), + secure: process.env.SMTP_PORT === "465", + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + tls: { + rejectUnauthorized: true, + }, + }); + + // Vérifier la connexion SMTP + await transporter.verify(); + + // HTML de l'email + const htmlContent = ` + + + + + + + +
+
+

✉️ Nouveau message depuis votre portfolio

+
+
+
+
👤 Nom
+
${name}
+
+
+
📧 Email
+
${email}
+
+
+
💬 Message
+
${message}
+
+
+ +
+ + + `; + + const textContent = ` +Nouveau message depuis votre portfolio + +Nom: ${name} +Email: ${email} + +Message: +${message} + +--- +Pour répondre, envoyez un email à: ${email} +Captcha vérifié + `; + + const mailOptions = { + from: process.env.SMTP_FROM, + to: process.env.EMAIL_TO, + replyTo: email, + subject: `💬 Nouveau message de ${name}`, + text: textContent, + html: htmlContent, + }; + + const info = await transporter.sendMail(mailOptions); + + return NextResponse.json({ + success: true, + messageId: info.messageId, + }); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Erreur inconnue"; + + return NextResponse.json( + { + error: "Erreur lors de l'envoi du message", + details: + process.env.NODE_ENV === "development" + ? errorMessage + : "Erreur serveur", + }, + { status: 500 } + ); + } +} diff --git a/app/globals.css b/app/globals.css index f1d8c73..21d86ed 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1 +1,95 @@ +/* globals.css */ @import "tailwindcss"; + +/* BASE */ +html { + scroll-behavior: smooth; +} + +/* GRID PATTERNS (Light) */ +.grid-wrapper { + min-height: 100%; + width: 100%; + position: relative; + background-color: #f8fafc; +} + +.grid-background { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 0; + background-image: linear-gradient(to right, #e2e8f0 1px, transparent 1px), + linear-gradient(to bottom, #e2e8f0 1px, transparent 1px); + background-size: 20px 30px; + -webkit-mask-image: radial-gradient( + ellipse 70% 60% at 50% 0%, + #000 60%, + transparent 100% + ); + mask-image: radial-gradient( + ellipse 70% 60% at 50% 0%, + #000 60%, + transparent 100% + ); +} + +/* GRID PATTERNS (Dark) */ +.grid-wrapper-dark { + min-height: 100%; + width: 100%; + position: absolute; + inset: 0; + background-color: #0f172a; +} + +.grid-background-dark { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 0; + background-image: linear-gradient(to right, #1e293b 1px, transparent 1px), + linear-gradient(to bottom, #1e293b 1px, transparent 1px); + background-size: 20px 30px; + -webkit-mask-image: radial-gradient( + ellipse 70% 60% at 50% 0%, + #000 60%, + transparent 100% + ); + mask-image: radial-gradient( + ellipse 70% 60% at 50% 0%, + #000 60%, + transparent 100% + ); +} + +/* GRID UTILITY */ +.bg-grid-slate-200\/50 { + background-image: linear-gradient( + to right, + rgb(226 232 240 / 0.5) 1px, + transparent 1px + ), + linear-gradient(to bottom, rgb(226 232 240 / 0.5) 1px, transparent 1px); + background-size: 100px 100px; +} + +/* ANIMATIONS */ +.animate-fade-in { + animation: fadeIn 1s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index bb95556..be7a855 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,187 @@ -import type { Metadata } from "next"; +import Footer from "@/component/layout/Footer"; +import NavBar from "@/component/layout/NavBar"; +import GridBackground from "@/component/ui/GridBackground"; +import type { Metadata, Viewport } from "next"; +import { Inter } from "next/font/google"; import { ReactNode } from "react"; import "./globals.css"; +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); + +const siteConfig = { + name: "Jessy David", + title: "Jessy David | Développeur Web Full-Stack", + description: + "Développeur Web Full-Stack passionné, spécialisé en React, Next.js et Node.js. Création de sites web modernes, applications web performantes et solutions digitales sur mesure.", + url: "https://jessy-david.dev", + ogImage: "https://jessy-david.dev/og-image.jpg", + author: "Jessy David", + keywords: [ + "développeur web", + "développeur full-stack", + "React", + "Next.js", + "Node.js", + "TypeScript", + "JavaScript", + "création site web", + "freelance", + "France", + "portfolio", + "Jessy David", + ], + twitterHandle: "@jessydavid", +}; + export const metadata: Metadata = { - title: "Portfolio · Jessy David", - description: "Portfolio de Jessy David", + title: { + default: siteConfig.title, + template: `%s | ${siteConfig.name}`, + }, + description: siteConfig.description, + keywords: siteConfig.keywords, + authors: [{ name: siteConfig.author, url: siteConfig.url }], + creator: siteConfig.author, + publisher: siteConfig.author, + + metadataBase: new URL(siteConfig.url), + alternates: { + canonical: "/", + languages: { + "fr-FR": "/", + }, + }, + + robots: { + index: true, + follow: true, + nocache: false, + googleBot: { + index: true, + follow: true, + noimageindex: false, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + + openGraph: { + type: "website", + locale: "fr_FR", + url: siteConfig.url, + siteName: siteConfig.name, + title: siteConfig.title, + description: siteConfig.description, + images: [ + { + url: siteConfig.ogImage, + width: 1200, + height: 630, + alt: `${siteConfig.name} - Portfolio Développeur Web`, + type: "image/jpeg", + }, + ], + }, + + twitter: { + card: "summary_large_image", + title: siteConfig.title, + description: siteConfig.description, + creator: siteConfig.twitterHandle, + images: [siteConfig.ogImage], + }, + + icons: { + icon: [ + { url: "/favicon.ico", sizes: "any" }, + { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + ], + apple: [ + { + url: "/apple-touch-icon.png", + sizes: "180x180", + type: "image/png", + }, + ], + }, + + manifest: "/site.webmanifest", + + category: "technology", + + verification: { + google: "ton-code-google-search-console", + }, + + other: { + "msapplication-TileColor": "#0f172a", + "theme-color": "#0f172a", + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + { media: "(prefers-color-scheme: dark)", color: "#0f172a" }, + ], + width: "device-width", + initialScale: 1, + maximumScale: 5, + userScalable: true, + colorScheme: "dark", +}; + +const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + "@id": `${siteConfig.url}/#website`, + url: siteConfig.url, + name: siteConfig.name, + description: siteConfig.description, + inLanguage: "fr-FR", + }, + { + "@type": "Person", + "@id": `${siteConfig.url}/#person`, + name: siteConfig.name, + url: siteConfig.url, + image: siteConfig.ogImage, + jobTitle: "Développeur Web Full-Stack", + description: siteConfig.description, + sameAs: [ + "https://github.com/jessydavid-dev", + "https://linkedin.com/in/jessy-david", + "https://twitter.com/jessydavid", + ], + knowsAbout: [ + "React", + "Next.js", + "TypeScript", + "Node.js", + "JavaScript", + "Tailwind CSS", + "PostgreSQL", + "MongoDB", + ], + }, + { + "@type": "ProfilePage", + "@id": `${siteConfig.url}/#profilepage`, + url: siteConfig.url, + name: `Portfolio de ${siteConfig.name}`, + description: siteConfig.description, + mainEntity: { "@id": `${siteConfig.url}/#person` }, + inLanguage: "fr-FR", + }, + ], }; export default function RootLayout({ @@ -13,8 +190,33 @@ export default function RootLayout({ children: ReactNode; }>) { return ( - - {children} + + +