frontend and backend base setup
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import BaseLayout from "@/components/Layout/BaseLayout";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return <BaseLayout locale={routing.defaultLocale}>{children}</BaseLayout>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const LoginLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginLayout;
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
console.log("Email:", email);
|
||||
console.log("Password:", password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md bg-white rounded-lg shadow-md p-8">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-gray-700 font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-gray-700 font-medium mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition duration-200">
|
||||
Login
|
||||
</button>
|
||||
<Link className="block text-center mt-4 text-blue-500" href="register">
|
||||
Create an account
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
import BaseLayout from "@/components/Layout/BaseLayout";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import UnauthenticatedLayout from "@/components/Layout/UnauthenticatedLayout";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return (
|
||||
<BaseLayout locale={routing.defaultLocale}>
|
||||
<UnauthenticatedLayout>{children}</UnauthenticatedLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex items-center justify-center">
|
||||
<div className="max-w-2xl pt-5 sm:pt-10 lg:pt-20">
|
||||
<div className="text-center">
|
||||
<h1 className="text-balance text-5xl font-semibold tracking-tight text-gray-900 sm:text-7xl">
|
||||
Track your working hours with ease
|
||||
</h1>
|
||||
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
|
||||
With ActaTempus, you can easily track your working hours and get
|
||||
insights into your work habits. Get started today and be more
|
||||
productive.
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a
|
||||
href="#"
|
||||
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||
Register
|
||||
</a>
|
||||
<a href="#" className="text-sm/6 font-semibold text-gray-900">
|
||||
Learn more <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {ReactNode} from 'react';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// Since we have a `not-found.tsx` page on the root, a layout file
|
||||
// is required, even if it's just passing children through.
|
||||
export default function RootLayout({children}: Props) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import BaseLayout from '@/components/Layout/BaseLayout';
|
||||
import Error404Page from '@/components/ErrorPages/404';
|
||||
import {routing} from '@/i18n/routing';
|
||||
|
||||
// This page renders when a route like `/unknown.txt` is requested.
|
||||
// In this case, the layout at `app/[locale]/layout.tsx` receives
|
||||
// an invalid value as the `[locale]` param and calls `notFound()`.
|
||||
|
||||
export default function GlobalNotFound() {
|
||||
return (
|
||||
<BaseLayout locale={routing.defaultLocale}>
|
||||
<Error404Page />
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {redirect} from 'next/navigation';
|
||||
|
||||
// This page only renders when the app is built statically (output: 'export')
|
||||
export default function RootPage() {
|
||||
redirect('/en');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ErrorPageLayout from './ErrorPageLayout';
|
||||
|
||||
export default function Error404Page() {
|
||||
const t = useTranslations('NotFoundPage');
|
||||
return (
|
||||
<ErrorPageLayout errorCode={404} errorMessage={t('title')}>
|
||||
<p className="text-center mt-4">{t('description')}</p>
|
||||
</ErrorPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client"; // Kennzeichnet die Komponente als Client-Komponente
|
||||
|
||||
type BackButtonProps = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function BackButton({ label }: BackButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="inline-flex px-6 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg shadow-md">
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ThemeProvider } from "@/context/ThemeContext";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ReactNode } from "react";
|
||||
import BackButton from "./BackButton";
|
||||
import HomeButton from "./HomeButton";
|
||||
|
||||
type ErrorPageLayoutProps = {
|
||||
children: ReactNode;
|
||||
errorCode?: number;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export default function ErrorPageLayout({
|
||||
children,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
}: ErrorPageLayoutProps) {
|
||||
const t = useTranslations("Error");
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-100 dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<AlertCircle className="h-16 w-16 text-red-600 mb-4" />
|
||||
<h1 className="text-5xl font-bold mb-4">
|
||||
{errorCode || t("defaultErrorCode")}
|
||||
</h1>
|
||||
<p className="text-xl mb-6">
|
||||
{errorMessage || t("defaultErrorMessage")}
|
||||
</p>
|
||||
<div className="row space-x-4">
|
||||
<HomeButton label={t("backToHome")} />
|
||||
|
||||
<BackButton label={t("back")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">{children}</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type HomeButtonProps = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function HomeButton({ label }: HomeButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex px-6 py-3 bg-cyan-500 hover:bg-cyan-600 text-white hover:text-white font-semibold rounded-lg shadow-md">
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import Header from "./Header/Header";
|
||||
|
||||
type AuthenticatedLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function AuthenticatedLayout({
|
||||
children,
|
||||
}: AuthenticatedLayoutProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Beispiel zur Überprüfung der Authentifizierung; ersetze dies mit deiner Authentifizierungslogik
|
||||
const isAuthenticated = true; // Hier eine echte Überprüfung einfügen
|
||||
if (!isAuthenticated) {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-slate-100 dark:bg-slate-900">
|
||||
<Header />
|
||||
<main className="flex-grow p-4">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ThemeProvider } from "@/context/ThemeContext";
|
||||
import "@/styles/globals.scss";
|
||||
import type { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import localFont from "next/font/local";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Time Tracker",
|
||||
description: "Time Tracker",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default async function BaseLayout({ children, locale }: Props) {
|
||||
const messages = await getMessages();
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased `}>
|
||||
{children}
|
||||
</body>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import LocaleSwitcher from "../Navigation/LocaleSwitcher";
|
||||
import NavigationBar from "../Navigation/NavigationBar";
|
||||
import { ThemeSwitcher } from "../Navigation/ThemeSwitcher";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-900 shadow-md">
|
||||
{/* Logo */}
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
ActaTempus
|
||||
</div>
|
||||
<NavigationBar />
|
||||
{/* Theme Switcher Dropdown */}
|
||||
<div className="flex items-center">
|
||||
<ThemeSwitcher />
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import {useLocale, useTranslations} from 'next-intl';
|
||||
import {routing} from '@/i18n/routing';
|
||||
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
|
||||
|
||||
export default function LocaleSwitcher() {
|
||||
const t = useTranslations('LocaleSwitcher');
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<LocaleSwitcherSelect defaultValue={locale} label={t('label')}>
|
||||
{routing.locales.map((cur) => (
|
||||
<option key={cur} value={cur}>
|
||||
{t('locale', {locale: cur})}
|
||||
</option>
|
||||
))}
|
||||
</LocaleSwitcherSelect>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { Locale, usePathname, useRouter } from "@/i18n/routing";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChangeEvent, ReactNode, useTransition } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
defaultValue: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function LocaleSwitcherSelect({
|
||||
children,
|
||||
defaultValue,
|
||||
label,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
|
||||
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||
const nextLocale = event.target.value as Locale;
|
||||
startTransition(() => {
|
||||
router.replace(
|
||||
// @ts-expect-error -- TypeScript will validate that only known `params`
|
||||
// are used in combination with a given `pathname`. Since the two will
|
||||
// always match for the current route, we can skip runtime checks.
|
||||
{ pathname, params },
|
||||
{ locale: nextLocale }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="relative text-gray-400">
|
||||
<p className="sr-only">{label}</p>
|
||||
<select
|
||||
className="inline-flex appearance-none bg-transparent py-3 pl-2 pr-6"
|
||||
defaultValue={defaultValue}
|
||||
disabled={isPending}
|
||||
onChange={onSelectChange}>
|
||||
{children}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function NavigationBar() {
|
||||
const t = useTranslations("Navigation");
|
||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Menu Button for Mobile */}
|
||||
<div className="md:hidden">
|
||||
<button onClick={() => setMenuOpen(!menuOpen)}>
|
||||
{menuOpen ? (
|
||||
<X className="h-6 w-6 text-gray-900 dark:text-white" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6 text-gray-900 dark:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<nav
|
||||
className={`md:flex ${
|
||||
menuOpen ? "block" : "hidden"
|
||||
} md:block space-x-4`}>
|
||||
<a
|
||||
href="/"
|
||||
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||
{t("home")}
|
||||
</a>
|
||||
<a
|
||||
href="/about"
|
||||
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
className="text-gray-900 dark:text-white hover:text-blue-500">
|
||||
Contact
|
||||
</a>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import {useSelectedLayoutSegment} from 'next/navigation';
|
||||
import {ComponentProps} from 'react';
|
||||
import {Link} from '@/i18n/routing';
|
||||
|
||||
export default function NavigationLink({
|
||||
href,
|
||||
...rest
|
||||
}: ComponentProps<typeof Link>) {
|
||||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||
const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/';
|
||||
const isActive = pathname === href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={clsx(
|
||||
'inline-block px-2 py-3 transition-colors',
|
||||
isActive ? 'text-white' : 'text-gray-400 hover:text-gray-200'
|
||||
)}
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { Monitor, Sun, Moon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
|
||||
|
||||
const handleThemeChange = (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="flex items-center space-x-2 p-2 rounded hover:bg-gray-300 dark:hover:bg-gray-600">
|
||||
<Monitor className="h-5 w-5 text-gray-800 dark:text-gray-200" />
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded shadow-lg z-10">
|
||||
<ul>
|
||||
<li
|
||||
onClick={() => handleThemeChange("light")}
|
||||
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||
<Sun className="inline-block h-5 w-5 mr-2 text-yellow-500" />{" "}
|
||||
Light
|
||||
</li>
|
||||
<li
|
||||
onClick={() => handleThemeChange("dark")}
|
||||
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||
<Moon className="inline-block h-5 w-5 mr-2 text-blue-500" /> Dark
|
||||
</li>
|
||||
<li
|
||||
onClick={() => handleThemeChange("system")}
|
||||
className={`cursor-pointer px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white`}>
|
||||
<Monitor className="inline-block h-5 w-5 mr-2 text-gray-800 dark:text-gray-200" />{" "}
|
||||
Default
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Header from './Header/Header';
|
||||
|
||||
type UnauthenticatedLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white dark:bg-gray-800">
|
||||
<Header />
|
||||
<main className="flex-grow p-4">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type ThemeContextType = {
|
||||
theme: string;
|
||||
setTheme: (theme: string) => void;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<string>('system');
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'system';
|
||||
setTheme(savedTheme);
|
||||
applyTheme(savedTheme);
|
||||
}, []);
|
||||
|
||||
const applyTheme = (selectedTheme: string) => {
|
||||
if (selectedTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else if (selectedTheme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
localStorage.removeItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (systemPrefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleThemeChange = (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
applyTheme(newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme: handleThemeChange }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
import {routing} from './routing';
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
// This typically corresponds to the `[locale]` segment
|
||||
let locale = await requestLocale;
|
||||
type LocaleType = (typeof routing.locales)[number];
|
||||
|
||||
// Ensure that a valid locale is used
|
||||
if (!locale || !routing.locales.includes(locale as LocaleType)) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {createNavigation} from 'next-intl/navigation';
|
||||
import {defineRouting} from 'next-intl/routing';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
pathnames: {
|
||||
'/': '/',
|
||||
'/pathnames': {
|
||||
en: '/pathnames',
|
||||
de: '/pfadnamen'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type Pathnames = keyof typeof routing.pathnames;
|
||||
export type Locale = (typeof routing.locales)[number];
|
||||
|
||||
export const {Link, getPathname, redirect, usePathname, useRouter} =
|
||||
createNavigation(routing);
|
||||
@@ -0,0 +1,20 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import {routing} from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: [
|
||||
// Enable a redirect to a matching locale at the root
|
||||
'/',
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
'/(de|en)/:path*',
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--primary-color: #1d4ed8; /* Primärfarbe für Akzente */
|
||||
--secondary-color: #3b82f6; /* Sekundärfarbe für Hover-Effekte */
|
||||
--border-color: #d1d5db; /* Farbe für Ränder */
|
||||
--shadow-color: rgba(0, 0, 0, 0.1); /* Schattenfarbe */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--primary-color: #2563eb; /* Angepasste Primärfarbe für den Dunkelmodus */
|
||||
--secondary-color: #60a5fa; /* Angepasste Sekundärfarbe */
|
||||
--border-color: #374151; /* Angepasste Farbe für Ränder */
|
||||
--shadow-color: rgba(0, 0, 0, 0.4); /* Stärkere Schattenfarbe */
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
//color: var(--foreground);
|
||||
//background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6; /* Verbesserte Lesbarkeit */
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user