diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..da5d8a7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,49 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug client-side (Firefox)", + "type": "firefox", + "request": "launch", + "url": "http://localhost:3000", + "reAttach": true, + "pathMappings": [ + { + "url": "webpack://_N_E", + "path": "${workspaceFolder}" + } + ] + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": [ + "--inspect" + ], + "skipFiles": [ + "/**" + ], + "serverReadyAction": { + "action": "debugWithEdge", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } + } + ] +} diff --git a/public/ceboola_gradient.mp4 b/public/ceboola_gradient.mp4 new file mode 100644 index 0000000..ad477ca Binary files /dev/null and b/public/ceboola_gradient.mp4 differ diff --git a/public/ceboola_gradient_white.mp4 b/public/ceboola_gradient_white.mp4 new file mode 100644 index 0000000..8cc5158 Binary files /dev/null and b/public/ceboola_gradient_white.mp4 differ diff --git a/public/site.webmanifest b/public/site.webmanifest index ccf313a..a7b5140 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "CebulaCamp", + "short_name": "Cebula", "icons": [ { "src": "/web-app-manifest-192x192.png", @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/src/app/[locale]/error.tsx b/src/app/[locale]/error.tsx new file mode 100644 index 0000000..4c62157 --- /dev/null +++ b/src/app/[locale]/error.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useEffect } from "react" + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + + useEffect(() => { + // Log the error to an error reporting service + console.error(error) + }, [error]) + + return ( +
+
+

+ {error.name} +

+

{error.message}

+
+ +
+
+
+ ) +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 8d002bd..333c093 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -2,10 +2,12 @@ import "../../globals.css"; import type React from "react"; -import { Lang, locales } from "@/i18n/locales"; +import { getLocale, Lang } from "@/i18n/locales"; import Head from 'next/head'; +import { ThemeProvider } from "@/components/providers"; import { Oxanium } from "next/font/google"; +import { headers } from "next/headers"; const oxanium = Oxanium({ subsets: ["latin-ext"] }) @@ -16,20 +18,34 @@ export default async function RootLayout({ children: React.ReactNode params: Promise<{ locale: Lang }> }) { - const { locale } = await params - const currentLang = locales.includes(locale) ? locale : "en" + const [{ locale }, head] = await Promise.all([ + params, + headers(), + ]); + + const preferedTheme = head.get("Sec-CH-Prefers-Color-Scheme")?.toLowerCase(); + const supportedThemes = ["dark", "light"]; + + const isDarkOrLight = + preferedTheme && supportedThemes.includes(preferedTheme); + + const currentLang = getLocale(locale); + const defaultTheme = isDarkOrLight ? preferedTheme : "dark"; + return ( - + - + - {children} + + {children} + ) diff --git a/src/app/[locale]/loading.tsx b/src/app/[locale]/loading.tsx new file mode 100644 index 0000000..558db1e --- /dev/null +++ b/src/app/[locale]/loading.tsx @@ -0,0 +1,4 @@ + +export default function Loading() { + return
Loading...
+} diff --git a/src/app/[locale]/not-found.tsx b/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..c363b3b --- /dev/null +++ b/src/app/[locale]/not-found.tsx @@ -0,0 +1,5 @@ + + +export default function NotFound() { + return
Not Found
+} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 9270e39..b711c17 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,130 +1,17 @@ -"use client" - -import { Nav } from "@/components/nav" -import { translations } from "@/i18n/translations" -import { useEffect, useRef } from "react" +import LandingPage from "@/components/landing-page"; +import { getLocale, Lang } from "@/i18n/locales"; +import { translations } from "@/i18n/translations"; -export default function Home() { - const videoRef = useRef(null) - const t = translations.pl // For now using Polish, could be made dynamic +export default async function Home( + { params } + : + { params: Promise<{ locale: Lang }> } +) { + const { locale } = await params + const currentLocale = getLocale(locale) + const t = translations[currentLocale]; - - useEffect(() => { - const handleScroll = () => { - if (videoRef.current) { - const scrolled = window.scrollY; - videoRef.current.style.willChange = "transform"; - videoRef.current.style.transform = `translateY(${scrolled * 0.5}px)`; - videoRef.current.style.willChange = "unset"; - } - }; - - const throttledHandleScroll = () => { - requestAnimationFrame(handleScroll); - }; - - window.addEventListener('scroll', throttledHandleScroll); - return () => window.removeEventListener('scroll', throttledHandleScroll); - }, []); - - return ( -
-
- ) + return } diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx new file mode 100644 index 0000000..5bde977 --- /dev/null +++ b/src/components/landing-page.tsx @@ -0,0 +1,115 @@ +"use client" + +import { Nav } from "@/components/nav"; +import { Translations } from "@/i18n/translations"; +import { cn } from "@/lib/utils"; +import { ReactElement, useEffect, useRef } from "react"; +import { useTheme } from "./providers"; + +function Section({ + id, + title, + paragraphs +}: { + id: string + title: string; + paragraphs: ReactElement; +}) { + return (
+
+

{title}

+
+ {paragraphs} +
+
+
) +} + +function Video({ src, hidden }: { + src: string; + hidden: boolean; +}) { + const videoRef = useRef(null); + + useEffect(() => { + const handleScroll = () => { + if (!videoRef.current || hidden) return; + + const scrolled = window.scrollY; + videoRef.current.style.willChange = "transform"; + videoRef.current.style.transform = `translateY(${scrolled * 0.5}px)`; + videoRef.current.style.willChange = "unset"; + }; + + const throttledHandleScroll = () => { + requestAnimationFrame(handleScroll); + }; + + window.addEventListener("scroll", throttledHandleScroll); + handleScroll(); + return () => window.removeEventListener("scroll", throttledHandleScroll); + }, [hidden]); + + return ( + + ); +} + +export default function LandingPage( + { t }: { t: Translations } +) { + + const { theme } = useTheme() + + return ( +
+
+ ) +} + diff --git a/src/components/nav.tsx b/src/components/nav.tsx index f261e99..6de00ad 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -3,8 +3,9 @@ import { Button } from "@/components/ui/button" import { Sections, type translations } from "@/i18n/translations" import { cn } from "@/lib/utils" import { MoonIcon, SunIcon } from "lucide-react" -import { useCallback, useEffect, useState } from "react" +import { useEffect, useState } from "react" import { MobileNav } from "./mobile-nav" +import { useTheme } from "./providers" const linksOrder: Array = [ "about", @@ -16,41 +17,12 @@ const linksOrder: Array = [ "contact", ] -function useTheme(): [theme: "light" | "dark", setTheme: (theme: "light" | "dark") => void] { - /* eslint-disable react-hooks/rules-of-hooks */ - if (typeof window === "undefined") return ["dark", () => { }] - const [theme, setTheme] = useState<"light" | "dark">("dark") - - const root = window.document.documentElement - - const changeTheme = useCallback((theme: "light" | "dark") => { - root.classList.remove("light", "dark") - root.classList.add(theme) - }, [root]) - - useEffect(() => { - // Determine the user's preferred color scheme - const preferredTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" - setTheme(preferredTheme) - - changeTheme(preferredTheme) - }, [changeTheme]) - - const updateTheme = useCallback((theme: "light" | "dark") => { - setTheme(theme) - changeTheme(theme) - }, [changeTheme, setTheme]) - - return [theme, updateTheme] - /* eslint-enable react-hooks/rules-of-hooks */ -} - export function Nav({ t, }: { t: typeof translations.pl }) { - const [theme, setTheme] = useTheme() + const { theme, setTheme } = useTheme() const [activeSection, setActiveSection] = useState("about") @@ -90,7 +62,7 @@ export function Nav({ }, []); return ( -