diff --git a/package-lock.json b/package-lock.json index d667618..b1cb7e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "cmdk": "1.1.1", "date-fns": "3.6.0", "embla-carousel-react": "8.6.0", + "i18next": "^25.8.13", "input-otp": "1.4.2", "lucide-react": "0.487.0", "motion": "12.23.24", @@ -52,6 +53,7 @@ "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-hook-form": "7.55.0", + "react-i18next": "^16.5.4", "react-popper": "2.3.0", "react-resizable-panels": "2.1.7", "react-responsive-masonry": "2.7.1", @@ -69,7 +71,7 @@ "@vitejs/plugin-react": "4.7.0", "gh-pages": "^6.3.0", "tailwindcss": "4.1.12", - "vite": "6.3.5" + "vite": "^6.4.1" }, "peerDependencies": { "react": "18.3.1", @@ -4357,6 +4359,46 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.8.13", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.13.tgz", + "integrity": "sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5299,6 +5341,33 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz", + "integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", @@ -5974,6 +6043,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -6010,9 +6088,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6084,6 +6162,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/package.json b/package.json index 035cebb..b0a86f4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "cmdk": "1.1.1", "date-fns": "3.6.0", "embla-carousel-react": "8.6.0", + "i18next": "^25.8.13", "input-otp": "1.4.2", "lucide-react": "0.487.0", "motion": "12.23.24", @@ -56,6 +57,7 @@ "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-hook-form": "7.55.0", + "react-i18next": "^16.5.4", "react-popper": "2.3.0", "react-resizable-panels": "2.1.7", "react-responsive-masonry": "2.7.1", @@ -73,7 +75,7 @@ "@vitejs/plugin-react": "4.7.0", "gh-pages": "^6.3.0", "tailwindcss": "4.1.12", - "vite": "6.3.5" + "vite": "^6.4.1" }, "peerDependencies": { "react": "18.3.1", diff --git a/src/app/components/Footer.tsx b/src/app/components/Footer.tsx index 3389ca1..031051e 100644 --- a/src/app/components/Footer.tsx +++ b/src/app/components/Footer.tsx @@ -1,7 +1,10 @@ import { Link } from "react-router"; import { Phone, Mail, MapPin, Clock, Wrench, Facebook, Instagram, Twitter } from "lucide-react"; +import { useTranslation } from "react-i18next"; export function Footer() { + const { t } = useTranslation(); + return ( diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index b81734c..76b7a5d 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -1,17 +1,20 @@ import { Link, useLocation } from "react-router"; import { Menu, X, Wrench } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { LanguageSwitcher } from "./LanguageSwitcher"; export function Header() { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const location = useLocation(); + const { t } = useTranslation(); const navItems = [ - { name: "Home", path: "/" }, - { name: "Services", path: "/services" }, - { name: "About", path: "/about" }, - { name: "Gallery", path: "/gallery" }, - { name: "Contact", path: "/contact" }, + { name: t("header.nav.home"), path: "/" }, + { name: t("header.nav.services"), path: "/services" }, + { name: t("header.nav.about"), path: "/about" }, + { name: t("header.nav.gallery"), path: "/gallery" }, + { name: t("header.nav.contact"), path: "/contact" }, ]; const isActive = (path: string) => location.pathname === path; @@ -23,11 +26,13 @@ export function Header() { {/* Logo */} - Lucris Handyman + + {t("header.brand")} + {/* Desktop Navigation */} - )} diff --git a/src/app/components/LanguageSwitcher.tsx b/src/app/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..dd17757 --- /dev/null +++ b/src/app/components/LanguageSwitcher.tsx @@ -0,0 +1,46 @@ +import { useTranslation } from "react-i18next"; + +function FlagGB() { + return ( + + ); +} + +function FlagNL() { + return ( + + ); +} + +export function LanguageSwitcher() { + const { i18n, t } = useTranslation(); + + const current = i18n.language === "nl" ? "nl" : "en"; + const other = current === "en" ? "nl" : "en"; + + const setLang = (lng: "en" | "nl") => i18n.changeLanguage(lng); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/pages/About.tsx b/src/app/pages/About.tsx index 2f7ac3a..e1026ca 100644 --- a/src/app/pages/About.tsx +++ b/src/app/pages/About.tsx @@ -1,45 +1,77 @@ import { CheckCircle, Award, Users, Clock } from "lucide-react"; import { ImageWithFallback } from "../components/figma/ImageWithFallback"; +import { useTranslation } from "react-i18next"; export function About() { + const { t } = useTranslation(); + const stats = [ - { label: "Years in Business", value: "15+" }, - { label: "Happy Clients", value: "5,000+" }, - { label: "Projects Completed", value: "10,000+" }, - { label: "Customer Satisfaction", value: "99%" }, + { label: t("about.stats.years"), value: "15+" }, + { label: t("about.stats.clients"), value: "5,000+" }, + { label: t("about.stats.projects"), value: "10,000+" }, + { label: t("about.stats.satisfaction"), value: "99%" }, ]; const values = [ { icon: CheckCircle, - title: "Quality First", - description: "We never compromise on the quality of our work. Every job is done right the first time.", + title: t("about.values.quality.title"), + description: t("about.values.quality.description"), }, { icon: Award, - title: "Professional Excellence", - description: "Our team consists of skilled, licensed professionals with years of experience.", + title: t("about.values.excellence.title"), + description: t("about.values.excellence.description"), }, { icon: Users, - title: "Customer Focus", - description: "Your satisfaction is our priority. We listen to your needs and deliver results.", + title: t("about.values.customer.title"), + description: t("about.values.customer.description"), }, { icon: Clock, - title: "Reliable Service", - description: "We show up on time, every time. Count on us to be there when you need us.", + title: t("about.values.reliable.title"), + description: t("about.values.reliable.description"), }, ]; + const team = [ + { + name: t("about.team.members.john.name"), + role: t("about.team.members.john.role"), + description: t("about.team.members.john.description"), + }, + { + name: t("about.team.members.david.name"), + role: t("about.team.members.david.role"), + description: t("about.team.members.david.description"), + }, + { + name: t("about.team.members.robert.name"), + role: t("about.team.members.robert.role"), + description: t("about.team.members.robert.description"), + }, + ]; + + const certifications = [ + t("about.certifications.items.0"), + t("about.certifications.items.1"), + t("about.certifications.items.2"), + t("about.certifications.items.3"), + t("about.certifications.items.4"), + t("about.certifications.items.5"), + ]; + return (
{/* Hero Section */}
-

About Lucris Handyman

+

+ {t("about.hero.title")} +

- Your trusted partner for all home repair and maintenance needs since 2011 + {t("about.hero.subtitle")}

@@ -49,20 +81,22 @@ export function About() {
-

Our Story

+

+ {t("about.story.title")} +

- Lucris Handyman was founded in 2011 with a simple mission: to provide reliable, high-quality handyman services at fair prices. What started as a one-person operation has grown into a trusted team of skilled professionals serving the community. + {t("about.story.p1")}

- Over the years, we've completed thousands of projects, from simple repairs to complete home renovations. Our reputation for quality work and excellent customer service has made us the go-to handyman service in the area. + {t("about.story.p2")}

- Today, we continue to uphold the same values that guided us from the beginning: integrity, quality, and customer satisfaction. Every member of our team is dedicated to exceeding your expectations. + {t("about.story.p3")}

@@ -75,7 +109,9 @@ export function About() {
{stats.map((stat, index) => (
-
{stat.value}
+
+ {stat.value} +
{stat.label}
))} @@ -87,9 +123,11 @@ export function About() {
-

Our Values

+

+ {t("about.values.title")} +

- These core principles guide everything we do + {t("about.values.subtitle")}

@@ -113,29 +151,15 @@ export function About() {
-

Meet Our Team

+

+ {t("about.team.title")} +

- Skilled professionals dedicated to your home's care + {t("about.team.subtitle")}

- {[ - { - name: "John Mitchell", - role: "Founder & Lead Handyman", - description: "15+ years of experience in all aspects of home repair and maintenance.", - }, - { - name: "David Chen", - role: "Master Electrician", - description: "Licensed electrician specializing in residential electrical work.", - }, - { - name: "Robert Garcia", - role: "Master Plumber", - description: "Expert plumber with extensive experience in repairs and installations.", - }, - ].map((member, index) => ( + {team.map((member, index) => (
@@ -153,20 +177,15 @@ export function About() {
-

Licensed & Insured

+

+ {t("about.certifications.title")} +

- We maintain all necessary licenses and insurance for your protection and peace of mind + {t("about.certifications.subtitle")}

- {[ - "State Licensed Contractor", - "Fully Insured & Bonded", - "BBB Accredited Business", - "Master Electrician License", - "Master Plumber License", - "OSHA Safety Certified", - ].map((cert, index) => ( + {certifications.map((cert, index) => (
{cert} diff --git a/src/app/pages/Contact.tsx b/src/app/pages/Contact.tsx index 6b1a5ac..1408593 100644 --- a/src/app/pages/Contact.tsx +++ b/src/app/pages/Contact.tsx @@ -1,7 +1,10 @@ import { useState } from "react"; import { MapPin, Phone, Mail, Clock, Send } from "lucide-react"; +import { useTranslation } from "react-i18next"; export function Contact() { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ name: "", email: "", @@ -12,7 +15,9 @@ export function Contact() { const [formSubmitted, setFormSubmitted] = useState(false); - const handleChange = (e: React.ChangeEvent) => { + const handleChange = ( + e: React.ChangeEvent, + ) => { setFormData({ ...formData, [e.target.name]: e.target.value, @@ -21,11 +26,9 @@ export function Contact() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - // In a real application, this would send the data to a server console.log("Form submitted:", formData); setFormSubmitted(true); - - // Reset form after 3 seconds + setTimeout(() => { setFormSubmitted(false); setFormData({ @@ -39,13 +42,13 @@ export function Contact() { }; const services = [ - "General Repair", - "Plumbing", - "Electrical", - "Painting", - "Carpentry", - "Home Maintenance", - "Other", + t("contact.services.generalRepair"), + t("contact.services.plumbing"), + t("contact.services.electrical"), + t("contact.services.painting"), + t("contact.services.carpentry"), + t("contact.services.homeMaintenance"), + t("contact.services.other"), ]; return ( @@ -53,9 +56,11 @@ export function Contact() { {/* Hero Section */}
-

Contact Us

+

+ {t("contact.hero.title")} +

- Get in touch with us for a free quote or to schedule a service + {t("contact.hero.subtitle")}

@@ -67,9 +72,11 @@ export function Contact() { {/* Contact Information */}
-

Get in Touch

+

+ {t("contact.info.title")} +

- Have a question or ready to start your project? We'd love to hear from you. Reach out using any of the methods below. + {t("contact.info.subtitle")}

@@ -79,11 +86,13 @@ export function Contact() {
-

Phone

+

{t("contact.info.phone")}

(555) 123-4567 -

Mon-Sat: 8AM-6PM

+

+ {t("contact.info.phoneHours")} +

@@ -92,11 +101,13 @@ export function Contact() {
-

Email

+

{t("contact.info.email")}

info@lucris.nl -

We'll respond within 24 hours

+

+ {t("contact.info.emailNote")} +

@@ -105,10 +116,10 @@ export function Contact() {
-

Address

+

{t("contact.info.address")}

- 123 Main Street
- New York, NY 10001 + {t("contact.info.addressLine1")}
+ {t("contact.info.addressLine2")}

@@ -118,20 +129,20 @@ export function Contact() {
-

Business Hours

+

{t("contact.info.hours")}

- Monday - Friday: 8:00 AM - 6:00 PM
- Saturday: 9:00 AM - 4:00 PM
- Sunday: Closed + {t("contact.info.hoursLine1")}
+ {t("contact.info.hoursLine2")}
+ {t("contact.info.hoursLine3")}

-

Emergency Services

+

{t("contact.emergency.title")}

- For urgent repairs outside business hours, call our emergency line: + {t("contact.emergency.subtitle")}

(555) 987-6543 @@ -141,16 +152,20 @@ export function Contact() { {/* Contact Form */}
-

Request a Free Quote

- +

+ {t("contact.form.title")} +

+ {formSubmitted ? (
-

Thank You!

+

+ {t("contact.form.successTitle")} +

- Your message has been received. We'll get back to you within 24 hours. + {t("contact.form.successMessage")}

) : ( @@ -158,7 +173,7 @@ export function Contact() {