nextjs improvements, auth dummies, auth guard

This commit is contained in:
Jean Jacques Avril 2024-12-31 15:30:28 +00:00
parent 872ad76f59
commit e06753fb24
No known key found for this signature in database
19 changed files with 448 additions and 16 deletions

6
.gitignore vendored
View File

@ -13,12 +13,6 @@ node_modules/
.DS_Store
Thumbs.db
# IDE spezifische Ignorierungen
.idea/
.vscode/
*.swp
*~
# Weitere Build-Verzeichnisse
**/dist/
**/build/

View File

@ -1,4 +1,9 @@
{
"editor.formatOnSave": true,
"cSpell.language": "en,de-DE"
"cSpell.language": "en,de-DE",
"files.autoSave": "afterDelay",
"editor.formatOnPaste": false,
"deno.disablePaths": [
"frontend-react"
]
}

2
backend-dart/run.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
dart run bin/backend_dart.dart # Backend on port 8080

3
frontend-react/deno.json Normal file
View File

@ -0,0 +1,3 @@
{
"unstable": ["unsafe-proto"]
}

View File

@ -0,0 +1,5 @@
{
"imports": {
"@/components/": "./components/"
}
}

View File

@ -19,5 +19,12 @@
"NotFoundPage": {
"description": "Bitte überprüfe die Addressleiste deines Browsers oder verwende die Navigation um zu einer bekannten Seite zu wechseln.",
"title": "Seite nicht gefunden"
},
"Auth": {
"login": "Anmelden",
"logout": "Abmelden",
"profile": "Profil",
"register": "Registrieren",
"resetPassword": "Passwort zurücksetzen"
}
}

View File

@ -1,4 +1,6 @@
"use client";
import BackButton from "@/components/Buttons/BackButton";
import StartpageButton from "@/components/Buttons/StartpageButton";
import Link from "next/link";
import React from "react";
import { useState } from "react";
@ -20,7 +22,8 @@ const LoginPage = () => {
<div className="mb-4">
<label
htmlFor="email"
className="block text-gray-700 font-medium mb-2">
className="block text-gray-700 font-medium mb-2"
>
Email
</label>
<input
@ -36,7 +39,8 @@ const LoginPage = () => {
<div className="mb-4">
<label
htmlFor="password"
className="block text-gray-700 font-medium mb-2">
className="block text-gray-700 font-medium mb-2"
>
Password
</label>
<input
@ -51,12 +55,17 @@ const LoginPage = () => {
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition duration-200">
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>
<div className="flex justify-between mt-8">
<BackButton />
<StartpageButton />
</div>
</form>
</div>
);

View File

@ -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;

View File

@ -0,0 +1,108 @@
"use client";
import Link from "next/link";
import React, { FormEvent, useState } from "react";
import BackButton from "@/components/Buttons/BackButton";
import StartpageButton from "@/components/Buttons/StartpageButton";
const RegisterPage = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
interface RegisterFormState {
email: string;
password: string;
confirmPassword: string;
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
alert("Passwords do not match");
return;
}
const formData: RegisterFormState = {
email,
password,
confirmPassword,
};
// Registrierungshandhabung hier hinzufügen
console.log("Registering user with data", formData);
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<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">Registrieren</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="Geben Sie Ihre Email ein"
required
/>
</div>
<div className="mb-4">
<label
htmlFor="password"
className="block text-gray-700 font-medium mb-2"
>
Passwort
</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="Geben Sie Ihr Passwort ein"
required
/>
</div>
<div className="mb-4">
<label
htmlFor="confirmPassword"
className="block text-gray-700 font-medium mb-2"
>
Passwort bestätigen
</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Bestätigen Sie Ihr Passwort"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition duration-200"
>
Registrieren
</button>
<Link className="block text-center mt-4 text-blue-500" href="/login">
Bereits ein Konto? Anmelden
</Link>
<div className="flex justify-between mt-8">
<BackButton />
<StartpageButton />
</div>
</form>
</div>
</div>
);
};
export default RegisterPage;

View File

@ -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>
);
}

View File

@ -0,0 +1,48 @@
"use client";
import React from "react";
export default function About() {
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">
Über das Projekt
</h1>
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
Entwickler: Jean Jacques Avril
</p>
<p className="mt-2 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
Webseite:{" "}
<a
href="https://jeanavril.com"
className="text-indigo-600 hover:underline"
>
jeanavril.com
</a>
</p>
<p className="mt-2 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
Technologien: Next.js, Go, Docker, Deno, Dart
</p>
<p className="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">
Mit ActaTempus können Sie Ihre Arbeitszeiten mühelos erfassen und
Einblicke in Ihre Arbeitsgewohnheiten gewinnen. Beginnen Sie noch
heute und steigern Sie Ihre Produktivität.
</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href="/register"
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"
>
Registrieren
</a>
<a href="#" className="text-sm/6 font-semibold text-gray-900">
Mehr erfahren <span aria-hidden="true"></span>
</a>
</div>
</div>
</div>
</main>
);
}

View File

@ -1,4 +1,7 @@
"use client";
import Link from "next/link";
export default function Home() {
return (
<main className="flex items-center justify-center">
@ -14,13 +17,17 @@ export default function Home() {
</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">
href="/register"
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>
<Link href={"/dashboard"}>
Go to Dashboard
</Link>
</div>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { ReactNode } from "react";
import BaseLayout from "@/components/Layout/BaseLayout";
import { routing } from "@/i18n/routing";
import UnauthenticatedLayout from "@/components/Layout/UnauthenticatedLayout";
import AuthGuard from "@/components/guards/AuthGuard";
type Props = {
children: ReactNode;
};
export default function RootLayout({ children }: Props) {
return (
<BaseLayout locale={routing.defaultLocale}>
<AuthGuard>
<UnauthenticatedLayout>{children}</UnauthenticatedLayout>
</AuthGuard>
</BaseLayout>
);
}

View File

@ -0,0 +1,97 @@
"use client";
import React, { FormEvent, useState } from "react";
const Dashboard = () => {
const [newEntry, setNewEntry] = useState({ task: "", duration: "" });
const [entries, setEntries] = useState([
{ id: 1, task: "Meeting", duration: "1h 30m", date: "2024-12-30" },
{ id: 2, task: "Development", duration: "3h 45m", date: "2024-12-29" },
{ id: 3, task: "Code Review", duration: "2h", date: "2024-12-28" },
]);
const handleInputChange = (e: FormEvent) => {
const { name, value } = e.target as HTMLInputElement;
setNewEntry({ ...newEntry, [name]: value });
};
const handleAddEntry = () => {
if (!newEntry.task || !newEntry.duration) return;
const newId = entries.length ? entries[entries.length - 1].id + 1 : 1;
setEntries([
...entries,
{ id: newId, ...newEntry, date: new Date().toISOString().split("T")[0] },
]);
setNewEntry({ task: "", duration: "" });
};
return (
<main className="min-h-screen bg-gray-100 py-10 px-4">
<div className="max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
{/* Timer Section */}
<section className="mb-8">
<h2 className="text-2xl font-bold text-gray-800 mb-4">New Entry</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<input
type="text"
name="task"
value={newEntry.task}
onChange={handleInputChange}
placeholder="Task Name"
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none"
/>
<input
type="text"
name="duration"
value={newEntry.duration}
onChange={handleInputChange}
placeholder="Duration (e.g., 1h 30m)"
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none"
/>
<button
onClick={handleAddEntry}
className="w-full bg-indigo-600 text-white py-2 rounded-lg hover:bg-indigo-500 transition duration-200"
>
Add Entry
</button>
</div>
</section>
{/* Past Entries Section */}
<section>
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Past Entries
</h2>
<div className="bg-gray-50 p-4 rounded-lg shadow-inner">
{entries.length
? (
<ul className="divide-y divide-gray-300">
{entries.map((entry) => (
<li
key={entry.id}
className="py-3 flex justify-between items-center"
>
<div>
<p className="text-lg font-medium text-gray-700">
{entry.task}
</p>
<p className="text-sm text-gray-500">
{entry.date} - {entry.duration}
</p>
</div>
</li>
))}
</ul>
)
: (
<p className="text-gray-500">
No entries yet. Start adding some!
</p>
)}
</div>
</section>
</div>
</main>
);
};
export default Dashboard;

View File

@ -1,4 +1,5 @@
import {ReactNode} from 'react';
import { UserProvider } from "@/context/UserContext";
import { ReactNode } from "react";
type Props = {
children: ReactNode;
@ -6,6 +7,10 @@ type Props = {
// 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;
}
export default function RootLayout({ children }: Props) {
return (
<UserProvider>
{children}
</UserProvider>
);
}

View File

@ -0,0 +1,26 @@
import { ChevronLeft } from "lucide-react";
import { useRouter } from "next/navigation";
const BackButton = () => {
const router = useRouter();
const handleBack = () => {
if (window.history.length > 1) {
router.back();
} else {
router.push("/");
}
};
return (
<button
onClick={handleBack}
className="text-blue-500 hover:underline focus:outline-none flex items-center"
>
<ChevronLeft className="inline-block w-4 h-4 mr-2" />
Zurück
</button>
);
};
export default BackButton;

View File

@ -0,0 +1,16 @@
import { HomeIcon } from "lucide-react";
import Link from "next/link";
const StartpageButton = () => {
return (
<Link
className="text-blue-500 hover:underline focus:outline-none flex items-center"
href="/"
>
<HomeIcon className="inline-block w-4 h-4 mr-2" />
Startseite
</Link>
);
};
export default StartpageButton;

View File

@ -0,0 +1,23 @@
"use client";
import { useRouter } from "next/navigation";
import { useUser } from "@/context/UserContext";
import React, { useEffect } from "react";
const AuthGuard = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { user } = useUser(); // Benutzer aus dem globalen Zustand abrufen
useEffect(() => {
if (!user) {
router.push("/login"); // Weiterleitung zur Login-Seite
}
}, [user, router]);
if (!user) {
return <p>Lade...</p>; // Anzeige während des Ladens oder Weiterleitens
}
return <>{children}</>;
};
export default AuthGuard;

View File

@ -0,0 +1,33 @@
"use client";
import React, { createContext, ReactNode, useContext, useState } from "react";
type User = {
id: string;
name: string;
email: string;
};
type UserContextType = {
user: User | null;
setUser: (user: User | null) => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export const useUser = (): UserContextType => {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser must be used within a UserProvider");
}
return context;
};