diff --git a/index.html b/index.html index 2210bcd..7a5bc46 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,18 @@ + + - - - - - naturvardarna - - -
- - - + + + + + + Naturvärdarna + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0ffeb1f..516f3ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.2.2", + "i18next": "^25.8.20", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-i18next": "^16.5.8", "tailwindcss": "^4.2.2" }, "devDependencies": { @@ -221,6 +223,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -2123,6 +2134,46 @@ "hermes-estree": "0.25.1" } }, + "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.20", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.20.tgz", + "integrity": "sha512-xjo9+lbX/P1tQt3xpO2rfJiBppNfUnNIPKgCvNsTKsvTOCro1Qr/geXVg1N47j5ScOSaXAPq8ET93raK3Rr06A==", + "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.6" + }, + "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", @@ -2812,6 +2863,33 @@ "react": "^19.2.4" } }, + "node_modules/react-i18next": { + "version": "16.5.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.8.tgz", + "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", + "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/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3007,7 +3085,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3089,6 +3167,15 @@ "punycode": "^2.1.0" } }, + "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/vite": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", @@ -3166,6 +3253,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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index aacc695..451dd51 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", + "i18next": "^25.8.20", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-i18next": "^16.5.8", "tailwindcss": "^4.2.2" }, "devDependencies": { @@ -30,4 +32,4 @@ "typescript-eslint": "^8.57.0", "vite": "^8.0.1" } -} +} \ No newline at end of file diff --git a/public/Kullaberg © Johan Hammar.jpg b/public/Kullaberg © Johan Hammar.jpg deleted file mode 100644 index afb3e06..0000000 Binary files a/public/Kullaberg © Johan Hammar.jpg and /dev/null differ diff --git a/public/about_us.jpg b/public/about_us.jpg deleted file mode 100644 index 1c4fa22..0000000 Binary files a/public/about_us.jpg and /dev/null differ diff --git a/public/about_us_1.webp b/public/about_us_1.webp new file mode 100644 index 0000000..84e5449 Binary files /dev/null and b/public/about_us_1.webp differ diff --git a/public/about_us_2.webp b/public/about_us_2.webp new file mode 100644 index 0000000..a7575e8 Binary files /dev/null and b/public/about_us_2.webp differ diff --git a/public/about_us_3.webp b/public/about_us_3.webp new file mode 100644 index 0000000..75650b9 Binary files /dev/null and b/public/about_us_3.webp differ diff --git a/public/about_us_4.webp b/public/about_us_4.webp new file mode 100644 index 0000000..11d1c01 Binary files /dev/null and b/public/about_us_4.webp differ diff --git a/public/de.svg b/public/de.svg new file mode 100644 index 0000000..60e5efc --- /dev/null +++ b/public/de.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/favicon-simple.svg b/public/favicon-simple.svg new file mode 100644 index 0000000..951ac72 --- /dev/null +++ b/public/favicon-simple.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/public/gb.svg b/public/gb.svg new file mode 100644 index 0000000..82b1fad --- /dev/null +++ b/public/gb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/kullaberg.webp b/public/kullaberg.webp new file mode 100644 index 0000000..868c9ef Binary files /dev/null and b/public/kullaberg.webp differ diff --git a/public/se.svg b/public/se.svg new file mode 100644 index 0000000..b32a8f9 --- /dev/null +++ b/public/se.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index b799504..4bcfb48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,101 +1,65 @@ // src/App.tsx -import { useState } from 'react'; -import { Menu, X, Globe, Trash2, Map, Leaf, AlertTriangle, Info, Tent, Car } from 'lucide-react'; +import { ChevronDown, Menu, X } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import './i18n'; +import About from './pages/About'; +import Home from './pages/Home'; -// --- TYPES & DATA --- type Tab = 'home' | 'about'; -type Language = 'sv' | 'en'; -const translations = { - sv: { - nav: { home: 'Hem', about: 'Om oss' }, - hero: { - title: 'Vi värnar om Kullaberg', - subtitle: 'Vi är sommarjobbarna som håller naturreservatet rent, säkert och vackert under årets mest hektiska veckor.' - }, - whatWeDo: { - title: 'Vårt uppdrag', - cleaningTitle: 'Håller rent', - cleaningDesc: 'Vi städar toaletter, parkeringsplatser, rastplatser och stränder dagligen för att skydda djurlivet och naturen från slitage.', - guidingTitle: 'Guidar besökare', - guidingDesc: 'Vi hjälper turister att hitta rätt, informerar om reservatets unika regler och ser till att allemansrätten respekteras.', - natureTitle: 'Naturvård', - natureDesc: 'Vi underhåller vandringsleder, bekämpar invasiva arter och fyller på ved för att skydda skogen.' - }, - importantInfo: { - title: 'Viktig information för besökare', - campingTitle: 'Tältning', - campingDesc: 'Tältning är endast tillåten på angivna platser, exempelvis vid Stora parkeringen. Läs skyltarna noga.', - parkingTitle: 'Parkering', - parkingDesc: 'Parkering är gratis så länge du står inom markerade områden. Blockera aldrig räddningsvägar.', - fireTitle: 'Eldning', - fireDesc: 'Använd endast vår utlagda ved vid de fasta grillplatserna. Bryt aldrig grenar från levande eller döda träd.' - }, - about: { - title: 'Om Naturvärdarna', - p1: 'Vi är ett gäng engagerade ungdomar som fått det ärofyllda uppdraget att arbeta som Naturvärdar på vackra Kullaberg under sommarmånaderna, ledda av våra projektledare.', - p2: 'Vårt mål är att göra Kullaberg till en bättre plats för besökare och minska det negativa slitaget på naturen. När trycket från turister och besökare är som allra störst, är det vi som rycker ut.', - p3: 'Våra arbetsuppgifter är väldigt varierande. En vanlig dag börjar med en ordentlig städrunda av faciliteter och parkeringsplatser. Därefter arbetar vi med förvaltning som att röja stigar, bygga räcken, olja in träskyltar och bekämpa invasiva växter. Vi plockar skräp längs kusterna och står även som Naturvärdar ute i fält för att svara på era frågor.' - }, - footer: 'Naturvärdarna Kullaberg. Ta hand om naturen.' - }, - en: { - nav: { home: 'Home', about: 'About Us' }, - hero: { - title: 'Preserving Kullaberg', - subtitle: 'We are the summer workers keeping the nature reserve clean, safe, and beautiful during the busiest weeks of the year.' - }, - whatWeDo: { - title: 'Our Mission', - cleaningTitle: 'Keeping it Clean', - cleaningDesc: 'We clean toilets, parking lots, rest areas, and beaches daily to protect wildlife and nature from wear and tear.', - guidingTitle: 'Guiding Visitors', - guidingDesc: 'We help tourists navigate, inform them about the reserve\'s unique rules, and ensure the Right of Public Access is respected.', - natureTitle: 'Nature Conservation', - natureDesc: 'We maintain hiking trails, combat invasive species, and restock firewood to protect the forest.' - }, - importantInfo: { - title: 'Important Visitor Information', - campingTitle: 'Camping', - campingDesc: 'Tents are only allowed in designated areas, such as the Main Parking lot. Please read the signs carefully.', - parkingTitle: 'Parking', - parkingDesc: 'Parking is free as long as you park within marked areas. Never block emergency routes.', - fireTitle: 'Fires', - fireDesc: 'Only use the provided firewood at designated barbecue areas. Never break branches from living or dead trees.' - }, - about: { - title: 'About the Nature Wardens', - p1: 'We are a group of dedicated youth who have been given the honorable task of working as Nature Wardens at the beautiful Kullaberg during the summer months, led by our project managers.', - p2: 'Our goal is to make Kullaberg a better place for visitors and to reduce negative impact on nature. When the pressure from tourists is at its highest, we are the ones who step in.', - p3: 'Our daily tasks are highly varied. A normal day begins with a thorough cleaning of facilities and parking areas. After that, we do maintenance work such as clearing trails, building fences, oiling wooden signs, and combating invasive species. We pick up trash along the coasts and act as Nature Wardens in the field to answer your questions.' - }, - footer: 'Kullaberg Nature Wardens. Take care of nature.' - } -}; +// --- DATA --- +const LANGUAGES = [ + { code: 'sv', label: 'SV', name: 'Svenska', flag: '/se.svg' }, + { code: 'en', label: 'EN', name: 'English', flag: '/gb.svg' }, + { code: 'de', label: 'DE', name: 'Deutsch', flag: '/de.svg' } +]; // --- COMPONENTS --- -const Navbar = ({ activeTab, setActiveTab, lang, setLang }: any) => { +const Navbar = ({ activeTab, setActiveTab }: { activeTab: Tab, setActiveTab: (t: Tab) => void }) => { const [isOpen, setIsOpen] = useState(false); - const t = translations[lang as Language].nav; + const [isLangOpen, setIsLangOpen] = useState(false); + const [isMobileLangOpen, setIsMobileLangOpen] = useState(false); + const { t, i18n } = useTranslation(); + const dropdownRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsLangOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); const handleNav = (tab: Tab) => { setActiveTab(tab); setIsOpen(false); + setIsMobileLangOpen(false); }; + const selectLanguage = (code: string) => { + i18n.changeLanguage(code); + localStorage.setItem('kullaberg_lang', code); + setIsLangOpen(false); + setIsOpen(false); + setIsMobileLangOpen(false); + }; + + const currentLang = LANGUAGES.find(l => l.code === i18n.language) || LANGUAGES[0]; + return ( -