added register feature for user with db safe

This commit is contained in:
Rajpreet0 2025-11-08 14:30:42 +01:00
parent 602dc165c0
commit aba022ecb6
33 changed files with 2591 additions and 25 deletions

1236
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,9 @@
},
"dependencies": {
"@prisma/client": "^6.19.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@supabase/ssr": "^0.7.0",
"class-variance-authority": "^0.7.1",
@ -21,10 +24,15 @@
"dotenv": "^17.2.3",
"lucide-react": "^0.552.0",
"next": "16.0.1",
"next-themes": "^0.4.6",
"prisma": "^6.19.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.3.1"
"react-icons": "^5.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

View file

@ -0,0 +1,7 @@
import LoginView from "@/src/modules/auth/ui/views/login-view";
const Login = () => {
return <LoginView />;
};
export default Login;

View file

@ -0,0 +1,7 @@
import RegisterAdminView from "@/src/modules/auth/ui/views/register-admin-view";
const RegisterAdmin = () => {
return <RegisterAdminView />;
};
export default RegisterAdmin;

View file

@ -0,0 +1,7 @@
import RegisterUserView from "@/src/modules/auth/ui/views/register-user-view";
const RegisterUser = () => {
return <RegisterUserView />;
};
export default RegisterUser;

View file

@ -0,0 +1,5 @@
const Dashboard = () => {
return <div>Du hast dich erfolgreich eingeloggt</div>;
};
export default Dashboard;

View file

@ -0,0 +1,71 @@
import { supabaseServer } from "@/src/lib/supabaseClient";
import { prisma } from "@/src/lib/prisma";
import { NextResponse } from "next/server";
/**
** Handles POST request to register and save a User in the DB
*
*
* @param {Request} request - Incoming HTTP request containing user data in JSON format
* @returns {Promise<NextResponse>} - JSON response with exisiting or newly created user
*/
export async function POST(request: Request) {
try {
const body = await request.json();
const { email, password, name, birthday, plz, city, street, houseNumber, gender } = body;
if (!email || !password || !name) {
return NextResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
}
const { data, error } = await supabaseServer.auth.signUp({
email,
password,
options: {
emailRedirectTo: "http://localhost:3000/login",
data: {
name,
gender,
},
},
});
if (error) {
console.error("❌ Supabase Auth Error:", error.message);
return NextResponse.json({ error: error.message }, { status: 400 });
}
const supabaseUser = data.user;
if (!supabaseUser) {
return NextResponse.json({ error: "User konnte nicht erstellt werden" }, { status: 500 });
}
// Check if User exists in the DB
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return NextResponse.json({ user: existingUser }, { status: 200 });
}
const newUser = await prisma.user.create({
data: {
id: supabaseUser.id,
name,
email,
birthday: birthday ? new Date(birthday) : null,
plz,
city,
street,
houseNumber,
gender,
},
});
return NextResponse.json({ user: newUser }, { status: 201 });
} catch (error) {
console.error("Error in /api/auth/register:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}

View file

@ -41,6 +41,9 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-white: #ffffff;
--color-blue: #1e293b;
--color-yellow: #f4c00c;
}
:root {

View file

@ -1,15 +1,10 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import localFont from "next/font/local";
import "./globals.css";
import { Toaster } from "../components/ui/sonner";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
const salmondFont = localFont({
src: "../public/font/salmond.woff",
});
export const metadata: Metadata = {
@ -23,8 +18,18 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
<html lang="en" className={salmondFont.className}>
<body className={`antialiased`}>
<main>{children}</main>
<Toaster
toastOptions={{
style: {
background: "#1E293B",
color: "#F4C00C",
},
}}
/>
</body>
</html>
);
}

View file

@ -0,0 +1,57 @@
"use client";
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "../ui/dropdown-menu";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { ChevronDown } from "lucide-react";
interface DropdownComponentProps {
label: string;
selected: string;
onSelect: (value: string) => void;
error?: string;
}
const DropdownComponent: React.FC<DropdownComponentProps> = ({
label,
selected,
onSelect,
error,
}) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="relative w-full">
<Label className="p-1 mb-2 text-blue text-lg font-semibold">{label}</Label>
<Input
className="h-[50px] border-blue rounded-xl w-full cursor-pointer pr-10"
value={selected}
readOnly
/>
<ChevronDown
className="absolute right-3 top-[75%] transform -translate-y-1/2 text-blue pointer-events-none"
size={24}
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => onSelect("")} className="text-gray-500">
Auswahl löschen
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onSelect("Men")}>Männlich</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onSelect("Woman")}>Weiblich</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onSelect("Various")}>Divers</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default DropdownComponent;

View file

@ -0,0 +1,45 @@
import { Label } from "../ui/label";
import { Input } from "../ui/input";
interface InputProps {
label?: string;
type: string;
id: string;
isLabel?: boolean;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
value?: string;
error?: string;
}
const InputComponent: React.FC<InputProps> = ({
label,
type,
id,
placeholder,
isLabel,
onChange,
value,
error,
}) => {
return (
<div>
{isLabel && (
<Label htmlFor={type} className="p-1 mb-2 text-blue text-lg font-semibold">
{label}
</Label>
)}
<Input
type={type}
id={id}
placeholder={placeholder}
onChange={onChange}
value={value}
className="h-[50px] border-blue rounded-xl w-full"
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
);
};
export default InputComponent;

View file

@ -9,13 +9,14 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default:
"bg-blue text-yellow font-bold text-md drop-shadow-lg hover:scale-105 transition-all cursor-pointer",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
@ -25,8 +26,6 @@ const buttonVariants = cva(
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {

View file

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/src/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/src/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View file

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/src/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View file

@ -0,0 +1,20 @@
import * as React from "react"
import { cn } from "@/src/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-lg",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View file

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/src/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View file

@ -0,0 +1,40 @@
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View file

@ -0,0 +1,16 @@
import { Loader2Icon } from "lucide-react"
import { cn } from "@/src/lib/utils"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
}
export { Spinner }

View file

@ -1,6 +1,12 @@
import { createBrowserClient } from "@supabase/ssr";
import { createClient } from "@supabase/supabase-js";
export const supabaseBrowser = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export const supabaseServer = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

View file

@ -0,0 +1,74 @@
import { create } from "zustand";
interface UserRegisterFormState {
fullName: string;
email: string;
password: string;
passwordConfirm: string;
birthday: string;
plz: string;
place: string;
street: string;
gender: string;
role: "user" | "admin" | "";
}
interface AdminRegisterFormState {
tel: string;
companyName: string;
companyPlace: string;
companyPLZ: string;
companyStreet: string;
companyMail: string;
companyNumber: string;
}
interface AuthFormState extends UserRegisterFormState, AdminRegisterFormState {
resetForm: () => void;
setField: (field: string, value: string) => void;
}
export const useAuthFormStore = create<AuthFormState>((set) => ({
// Common + User fields
fullName: "",
email: "",
password: "",
passwordConfirm: "",
birthday: "",
plz: "",
place: "",
street: "",
gender: "",
role: "",
// Admin fields
tel: "",
companyName: "",
companyPlace: "",
companyPLZ: "",
companyStreet: "",
companyMail: "",
companyNumber: "",
setField: (field, value) => set((state) => ({ ...state, [field]: value })),
resetForm: () =>
set({
fullName: "",
email: "",
password: "",
passwordConfirm: "",
birthday: "",
plz: "",
place: "",
street: "",
gender: "",
role: "",
tel: "",
companyName: "",
companyPlace: "",
companyPLZ: "",
companyStreet: "",
companyMail: "",
companyNumber: "",
}),
}));

View file

@ -0,0 +1,15 @@
interface AuthButtonProps {
Icon: React.ComponentType<{ size?: number }>;
label: string;
}
const AuthButton: React.FC<AuthButtonProps> = ({ Icon, label }) => {
return (
<div className="flex gap-2 py-2 px-3 items-center justify-center rounded-xl border-1 border-blue text-blue w-full sm:w-fit cursor-pointer hover:scale-105 transition-all">
<Icon size={20} />
<p className="ml-2 mt-1">{label}</p>
</div>
);
};
export default AuthButton;

View file

@ -0,0 +1,48 @@
import Image from "next/image";
import MultiStepProzess from "./MultiStepProzess";
import Logo from "@/src/public/logo.png";
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/src/components/ui/card";
interface AuthSidebarComponentProps {
title: string;
description?: string;
footer: string;
isSteps?: boolean;
step?: number;
totalSteps?: number;
onStepChange?: (step: number) => void;
}
const AuthSidebarComponent: React.FC<AuthSidebarComponentProps> = ({
title,
description,
footer,
isSteps,
step = 1,
totalSteps = 2,
onStepChange,
}) => {
return (
<Card className="bg-blue border-0 w-fit h-full p-6 sm:p-12 flex flex-col justify-between">
<CardHeader>
<CardTitle>
<Image src={Logo} width={150} height={150} alt="Logo" />
</CardTitle>
</CardHeader>
<CardContent className="text-yellow flex flex-col gap-12 min-h-[70vh]">
<h1 className="text-3xl lg:text-[40px] max-w-[300px] font-bold mt-8 lg:mt-12">{title}</h1>
<p className="text-base">{description}</p>
{isSteps && (
<MultiStepProzess step={step} totalSteps={totalSteps} onStepChange={onStepChange} />
)}
</CardContent>
<CardFooter>
<p className="text-yellow">
Bei Fragen: <span className=" cursor-pointer hover:underline">{footer}</span>
</p>
</CardFooter>
</Card>
);
};
export default AuthSidebarComponent;

View file

@ -0,0 +1,37 @@
import React from "react";
import MultiStepProzessItem from "./MultiStepProzessItem";
interface MultiStepProzessProps {
step: number;
totalSteps: number;
onStepChange?: (step: number) => void;
}
const MultiStepProzess: React.FC<MultiStepProzessProps> = ({ step, totalSteps, onStepChange }) => {
return (
<div>
<MultiStepProzessItem
stepTitle="Schritt 1"
stepDescription="Anmeldedaten"
isActive={step == 1}
onClick={() => onStepChange?.(1)}
/>
<MultiStepProzessItem
stepTitle="Schritt 2"
stepDescription="Persönliche Angaben"
isActive={step == 2}
onClick={() => onStepChange?.(2)}
/>
{totalSteps == 3 && (
<MultiStepProzessItem
stepTitle="Schritt 3"
stepDescription="Geschäftsdaten"
isActive={step == 3}
onClick={() => onStepChange?.(3)}
/>
)}
</div>
);
};
export default MultiStepProzess;

View file

@ -0,0 +1,30 @@
interface MultiStepProzessItemProps {
stepTitle: string;
stepDescription: string;
isActive: boolean;
onClick?: () => void;
}
const MultiStepProzessItem: React.FC<MultiStepProzessItemProps> = ({
stepTitle,
stepDescription,
isActive,
onClick,
}) => {
return (
<div onClick={onClick} className="w-full flex items-center cursor-pointer gap-4 mt-6">
<div
className={`h-full flex justify-center items-center rounded-full p-5 ${isActive ? "bg-yellow" : "bg-transparent"} border-2 border-yellow`}
>
<div className={`w-full p-1 rounded-full ${isActive ? "bg-blue" : "bg-yellow"}`}></div>
</div>
<div className={`flex flex-col text-lg justify-center ${isActive && "font-semibold"}`}>
<h4>{stepTitle}</h4>
<p>{stepDescription}</p>
</div>
</div>
);
};
export default MultiStepProzessItem;

View file

@ -0,0 +1,133 @@
"use client";
import InputComponent from "@/src/components/layout/InputComponent";
import AuthButton from "../components/AuthButton";
import AuthSidebarComponent from "../components/AuthSidebarComponent";
import { FaGoogle, FaFacebook } from "react-icons/fa";
import { Button } from "@/src/components/ui/button";
import { Label } from "@/src/components/ui/label";
import { Checkbox } from "@/src/components/ui/checkbox";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { supabaseBrowser } from "@/src/lib/supabaseClient";
import { Spinner } from "@/src/components/ui/spinner";
const LoginView = () => {
//TODO: RIGHT LOGIN with Schema etc.
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [remember, setRemember] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleLogin = async () => {
setError("");
setLoading(true);
try {
const { data, error } = await supabaseBrowser.auth.signInWithPassword({
email,
password,
});
if (error) {
console.error("❌ Login Error:", error.message);
setError("E-Mail oder Passwort ist falsch");
setLoading(false);
return;
}
// Optional: Session speichern
if (remember) {
localStorage.setItem("since3_remember_me", "true");
} else {
localStorage.removeItem("since3_remember_me");
}
// Redirect nach Login
router.push("/dashboard");
} catch (err) {
console.error(err);
setError("Ein unerwarteter Fehler ist aufgetreten");
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-screen">
{/* Sidebar */}
<div className="hidden lg:flex lg:w-1/6 ">
<div className="fixed p-4 left-0 top-0 h-screen w-1/2 z-10">
<AuthSidebarComponent
title="Deine Reise beginnt hier"
description="Werde Teil einer Plattform, die Trainer und Lernende zusammenbringt."
footer="info@since3.de"
/>
</div>
</div>
{/* Login-Formular */}
<div className="w-full lg:w-5/6 lg:ml-auto flex justify-center items-center min-h-screen px-4">
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
<h1 className="text-2xl font-extrabold text-blue mb-8 ">Login</h1>
<InputComponent
isLabel
label="E-mail Adresse"
type="email"
id="login-email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<InputComponent
isLabel
label="Passwort"
type="password"
id="login-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button onClick={handleLogin} disabled={loading} className="w-full mt-6">
{loading ? <Spinner /> : "Weiter"}
</Button>
<div className="flex flex-col sm:flex-row sm:justify-between w-full mt-6 gap-4 text-sm">
<div className="flex items-center gap-2">
<Checkbox
id="remembering"
checked={remember}
onCheckedChange={(val: boolean) => setRemember(val)}
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
/>
<Label htmlFor="remembering" className="cursor-pointer">
Beim nächsten Mal an mich erinnern
</Label>
</div>
<p className="hover:underline cursor-pointer text-blue">Passwort vergessen?</p>
</div>
<hr className="mt-6 border-blue" />
<div className="flex flex-col sm:flex-row gap-4 mt-4">
<AuthButton Icon={FaGoogle} label="Login mit Google" />
<AuthButton Icon={FaFacebook} label="Login mit Facebook" />
</div>
<div className="flex flex-col sm:flex-row gap-2 items-center mt-16 text-sm">
<p>Ich habe noch keinen Account</p>
<p
className="text-yellow hover:underline cursor-pointer"
onClick={() => router.push("/register")}
>
Sign Up
</p>
</div>
</div>
</div>
</div>
);
};
export default LoginView;

View file

@ -0,0 +1,7 @@
"use client";
const RegisterAdminView = () => {
return <div>Register Admin View</div>;
};
export default RegisterAdminView;

View file

@ -0,0 +1,251 @@
"use client";
import { useState } from "react";
import AuthSidebarComponent from "../components/AuthSidebarComponent";
import InputComponent from "@/src/components/layout/InputComponent";
import { Button } from "@/src/components/ui/button";
import AuthButton from "../components/AuthButton";
import { FaGoogle, FaFacebook } from "react-icons/fa";
import { useAuthFormStore } from "../../store/authFormState";
import { userRegisterSchema, userRegisterStep1Schema } from "../../validation/userRegisterSchema";
import { ZodError } from "zod";
import DropdownComponent from "@/src/components/layout/DropdownComponent";
import { Spinner } from "@/src/components/ui/spinner";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
const RegisterUserView = () => {
const [step, setStep] = useState(1);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const router = useRouter();
const formData = useAuthFormStore();
const handleChange = (field: string, value: string) => {
formData.setField(field, value);
};
const handleNext = () => {
setErrors({});
try {
if (step === 1) {
userRegisterStep1Schema.parse({
fullName: formData.fullName,
email: formData.email,
password: formData.password,
passwordConfirm: formData.passwordConfirm,
});
setStep(2);
}
} catch (err) {
if (err instanceof ZodError) {
const fieldErrors: Record<string, string> = {};
err.issues.forEach((issue) => {
const path = issue.path[0];
if (typeof path === "string") {
fieldErrors[path] = issue.message;
}
});
setErrors(fieldErrors);
}
}
};
const handleRegister = async () => {
setErrors({});
setLoading(true);
try {
userRegisterSchema.parse({
fullName: formData.fullName,
email: formData.email,
password: formData.password,
passwordConfirm: formData.passwordConfirm,
birthday: formData.birthday,
plz: formData.plz,
place: formData.place,
street: formData.street,
gender: formData.gender,
});
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: formData.email,
password: formData.password,
name: formData.fullName,
birthday: formData.birthday,
plz: formData.plz,
city: formData.place,
street: formData.street,
houseNumber: "",
gender: formData.gender,
}),
});
const data = await res.json();
if (!res.ok) {
setErrors({ email: data.error || "Registrierung fehlgeschlagen" });
return;
}
toast.info("Bitte Email bestätigen für erfolgreiche Registrierung");
formData.resetForm();
setStep(1);
setErrors({});
router.push("/login");
} catch (err) {
if (err instanceof ZodError) {
const fieldErrors: Record<string, string> = {};
err.issues.forEach((issue) => {
const path = issue.path[0];
if (typeof path === "string") {
fieldErrors[path] = issue.message;
}
});
setErrors(fieldErrors);
} else {
toast.error(`Registrierung fehlgeschlagen: ${err}`);
}
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-screena">
{/* Sidebar */}
<div className="hidden lg:flex lg:w-1/6 ">
<div className="fixed p-4 left-0 top-0 h-screen w-1/2 z-10">
<AuthSidebarComponent
title="Werde zur besten Version deiner selbst"
footer="info@since3.de"
isSteps
step={step}
totalSteps={2}
onStepChange={setStep}
/>
</div>
</div>
<div className="w-full lg:w-5/6 lg:ml-auto flex justify-center items-center min-h-screen px-4">
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
<h1 className="text-2xl font-extrabold text-blue mb-8 mt-8 ">Sign up for free</h1>
{step === 1 && (
<>
<InputComponent
isLabel
label="Vor- und Nachname"
type="text"
id="register-user-fullName"
value={formData.fullName}
onChange={(e) => handleChange("fullName", e.target.value)}
error={errors.fullName}
/>
<InputComponent
isLabel
label="E-Mail"
type="email"
id="register-user-email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
error={errors.email}
/>
<InputComponent
isLabel
label="Passwort"
type="password"
id="register-user-password"
value={formData.password}
onChange={(e) => handleChange("password", e.target.value)}
error={errors.password}
/>
<InputComponent
isLabel
label="Passwort bestätigen"
type="password"
id="register-user-passwordConform"
value={formData.passwordConfirm}
onChange={(e) => handleChange("passwordConfirm", e.target.value)}
error={errors.passwordConfirm}
/>
<Button className="w-full mt-6" onClick={handleNext}>
Weiter
</Button>
</>
)}
{step === 2 && (
<>
<InputComponent
isLabel
label="Geburtsdatum"
type="date"
id="register-user-birthday"
value={formData.birthday}
onChange={(e) => handleChange("birthday", e.target.value)}
error={errors.birthday}
/>
<InputComponent
isLabel
label="PLZ"
type="text"
id="register-user-plz"
value={formData.plz}
onChange={(e) => handleChange("plz", e.target.value)}
error={errors.plz}
/>
<InputComponent
isLabel
label="Wohnort"
type="text"
id="register-user-place"
value={formData.place}
onChange={(e) => handleChange("place", e.target.value)}
error={errors.place}
/>
<InputComponent
isLabel
label="Straße und Hausnummer"
type="text"
id="register-user-street"
value={formData.street}
onChange={(e) => handleChange("street", e.target.value)}
error={errors.street}
/>
<DropdownComponent
label="Geschlecht"
selected={formData.gender}
onSelect={(value: string) => handleChange("gender", value)}
error={errors.gender}
/>
<Button onClick={handleRegister} disabled={loading} className="w-full mt-6">
{loading ? <Spinner /> : "Anmelden"}
</Button>
</>
)}
<hr className="mt-6 border-blue" />
<div className="flex flex-col sm:flex-row gap-4 mt-4">
<AuthButton Icon={FaGoogle} label="Login mit Google" />
<AuthButton Icon={FaFacebook} label="Login mit Facebook" />
</div>
<div className="flex flex-col sm:flex-row gap-2 items-center mt-8 text-sm">
<p>Ich habe schon einen Account</p>
<p className="text-yellow hover:underline cursor-pointer">Login</p>
</div>
</div>
</div>
</div>
);
};
export default RegisterUserView;

View file

@ -0,0 +1,50 @@
import { z } from "zod";
export const userRegisterStep1Schema = z
.object({
fullName: z.string().min(2, "Name ist zu kurz"),
email: z.string().email("Ungültige E-Mail-Adresse"),
password: z
.string()
.min(8)
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.@$!%*?&])[A-Za-z\d.@$!%*?&]+$/,
"Passwort muss mindestens 8 Zeichen haben, Großbuchstaben, Kleinbuchstaben, Nummer und ein spezielles Zeichen beinhalten"
),
passwordConfirm: z.string(),
})
.refine((data) => data.password === data.passwordConfirm, {
message: "Passwörter stimmen nicht überein",
path: ["passwordConfirm"],
});
export const userRegisterSchema = z
.object({
fullName: z.string().min(2, "Name ist zu kurz"),
email: z.string().email("Ungültige E-Mail-Adresse"),
password: z
.string()
.min(8)
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.@$!%*?&])[A-Za-z\d.@$!%*?&]+$/,
"Passwort muss mindestens 8 Zeichen haben, Großbuchstaben, Kleinbuchstaben, Nummer und ein spezielles Zeichen beinhalten"
),
passwordConfirm: z.string(),
birthday: z.string().min(1, "Geburtsdatum erforderlich"),
plz: z.string().min(1, "PLZ erforderlich"),
place: z.string().min(1, "Wohnort erforderlich"),
street: z.string().min(1, "Straße erforderlich"),
gender: z
.enum(["Men", "Woman", "Various"])
.or(z.literal(""))
.refine((val) => val !== "", {
message: "Geschlecht ist erforderlich",
}),
role: z.literal("user").optional(),
})
.refine((data) => data.password === data.passwordConfirm, {
message: "Passwörter stimmen nicht überein",
path: ["passwordConfirm"],
});
export type UserRegisterSchema = z.infer<typeof userRegisterSchema>;

Binary file not shown.

BIN
src/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB