db+login signup

main
miranshala 7 months ago
parent 55022e5ed2
commit f5927f2e76

@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from "next/server";
import { getPool } from "@/lib/database";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
// Validate input
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
);
}
const pool = getPool();
const client = await pool.connect();
try {
// Find user by email
const result = await client.query(
`SELECT id, email, password_hash, first_name, last_name, role, subscription_tier, created_at
FROM users WHERE email = $1`,
[email]
);
if (result.rows.length === 0) {
return NextResponse.json(
{ error: "Invalid email or password" },
{ status: 401 }
);
}
const user = result.rows[0];
// Verify password
const isValidPassword = await bcrypt.compare(
password,
user.password_hash
);
if (!isValidPassword) {
return NextResponse.json(
{ error: "Invalid email or password" },
{ status: 401 }
);
}
// Create JWT token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role,
},
process.env.JWT_SECRET || "your-secret-key",
{ expiresIn: "7d" }
);
return NextResponse.json({
success: true,
token,
user: {
id: user.id,
email: user.email,
FirstName: user.first_name,
LastName: user.last_name,
role: user.role,
subscriptionTier: user.subscription_tier,
createdAt: user.created_at,
},
});
} finally {
client.release();
}
} catch (error) {
console.error("Login error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import { getPool } from "@/lib/database";
import bcrypt from "bcryptjs";
export async function POST(request: NextRequest) {
try {
const { firstName, lastName, email, password } = await request.json();
// Validate input
if (!firstName || !lastName || !email || !password) {
return NextResponse.json(
{ error: "All fields are required" },
{ status: 400 }
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: "Invalid email format" },
{ status: 400 }
);
}
// Validate password length
if (password.length < 6) {
return NextResponse.json(
{ error: "Password must be at least 6 characters" },
{ status: 400 }
);
}
const pool = getPool();
const client = await pool.connect();
try {
// Check if user already exists
const existingUser = await client.query(
"SELECT id FROM users WHERE email = $1",
[email]
);
if (existingUser.rows.length > 0) {
return NextResponse.json(
{ error: "User with this email already exists" },
{ status: 409 }
);
}
// Hash the password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Insert new user
const result = await client.query(
`INSERT INTO users (email, password_hash, first_name, last_name, role, subscription_tier)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, email, first_name, last_name, role, subscription_tier, created_at`,
[email, passwordHash, firstName, lastName, "USER", "BASIC"]
);
const newUser = result.rows[0];
// Create a basic subscription for the user
await client.query(
`INSERT INTO subscriptions (user_id, tier, status, storage_limit_gb, current_usage_gb)
VALUES ($1, $2, $3, $4, $5)`,
[newUser.id, "BASIC", "ACTIVE", 5, 0]
);
return NextResponse.json({
success: true,
message: "User created successfully",
user: {
id: newUser.id,
email: newUser.email,
firstName: newUser.first_name,
lastName: newUser.last_name,
role: newUser.role,
subscriptionTier: newUser.subscription_tier,
createdAt: newUser.created_at,
},
});
} finally {
client.release();
}
} catch (error) {
console.error("Registration error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { AuthProvider } from "@/lib/auth-context";
import "./globals.css";
const geistSans = Geist({
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Reya Render",
description: "Professional 3D rendering service",
};
export default function RootLayout({
@ -39,7 +40,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);

@ -0,0 +1,16 @@
import { LoginForm } from "@/components/LoginForm";
export default function LoginPage() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Log in to your account
</h2>
</div>
<LoginForm />
</div>
</div>
);
}

@ -1,13 +1,33 @@
"use client";
import Image from "next/image";
import UploadArea from "@/components/UploadArea";
import { useAuth } from "@/lib/auth-context";
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuLink,
} from "@/components/ui/navigation-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDownIcon } from "lucide-react";
export default function Home() {
const { isAuthenticated, user, logout } = useAuth();
// Get user initials for the circle icon
const getUserInitials = () => {
if (!user) return "U";
const firstName = user.FirstName || "";
const lastName = user.LastName || "";
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase() || "U";
};
return (
<div className="min-h-screen" style={{ backgroundColor: "#FFFFFF" }}>
{/* Sticky Navigation */}
@ -38,17 +58,71 @@ export default function Home() {
Pricing
</NavigationMenuLink>
</NavigationMenuItem>
{/* Show Upload File only when authenticated */}
{isAuthenticated && (
<NavigationMenuItem>
<NavigationMenuLink
href="/upload"
className="px-4 py-2 text-gray-700 hover:text-gray-900"
>
Upload File
</NavigationMenuLink>
</NavigationMenuItem>
)}
</NavigationMenuList>
</NavigationMenu>
{/* Auth Buttons */}
{/* Auth Section */}
<div className="flex items-center space-x-4">
<button className="text-gray-700 hover:text-gray-900 px-4 py-2 border border-gray-300 hover:border-gray-400 rounded-md cursor-pointer transition-colors">
Log In
{isAuthenticated ? (
// Show user circle with dropdown when logged in
<div className="flex items-center space-x-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center space-x-1 bg-white hover:bg-gray-50 text-gray-700 font-semibold text-sm rounded-full px-3 py-2 transition-colors cursor-pointer border border-gray-200 hover:border-gray-300">
<div className="w-8 h-8 bg-orange-500 rounded-full flex items-center justify-center text-white">
{getUserInitials()}
</div>
<ChevronDownIcon className="w-4 h-4 transition-transform duration-200 data-[state=open]:rotate-180" />
</button>
<button className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-md font-medium cursor-pointer transition-colors">
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem asChild>
<a href="/subscriptions" className="cursor-pointer">
My Subscriptions
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a href="/renders" className="cursor-pointer">
My Renders
</a>
</DropdownMenuItem>
<DropdownMenuItem
onClick={logout}
className="cursor-pointer"
>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
// Show login/signup buttons when not authenticated
<>
<a
href="/login"
className="text-gray-700 hover:text-gray-900 px-4 py-2 border border-gray-300 hover:border-gray-400 rounded-md cursor-pointer transition-colors"
>
Log In
</a>
<a
href="/signup"
className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-md font-medium cursor-pointer transition-colors"
>
Sign Up
</button>
</a>
</>
)}
</div>
</div>
</div>
@ -250,7 +324,8 @@ export default function Home() {
</div>
</div>
{/* Upload area below hero, same width as orange container */}
{/* Upload area below hero, same width as orange container - only show when authenticated */}
{isAuthenticated && (
<div className="flex justify-center">
<div className="w-[90%] mt-6 mb-12">
<h2 className="text-4xl font-bold text-left mb-8 text-gray-800">
@ -259,6 +334,7 @@ export default function Home() {
<UploadArea className="w-full shadow-sm" />
</div>
</div>
)}
</div>
);
}

@ -0,0 +1,16 @@
import { CreateAccountForm } from "@/components/CreateAccountForm";
export default function SignUpPage() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
</div>
<CreateAccountForm />
</div>
</div>
);
}

@ -0,0 +1,191 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { toast, Toaster } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const formSchema = z
.object({
firstName: z
.string()
.min(2, { message: "First name must be at least 2 characters." }),
lastName: z
.string()
.min(2, { message: "Last name must be at least 2 characters." }),
email: z.string().email({ message: "Please enter a valid email address." }),
password: z
.string()
.min(6, { message: "Password must be at least 6 characters." }),
repeatPassword: z
.string()
.min(6, { message: "Repeat password must be at least 6 characters." }),
})
.refine((data) => data.password === data.repeatPassword, {
path: ["repeatPassword"],
message: "Passwords do not match.",
});
export function CreateAccountForm() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
password: "",
repeatPassword: "",
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
try {
setIsLoading(true);
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
toast.error(result.error || "Registration failed");
return;
}
// Show success message
toast.success("Account Created", {
duration: 2000,
position: "top-center",
});
// Redirect to login page
setTimeout(() => {
router.push("/login");
}, 1000);
} catch (error) {
console.error("Registration error:", error);
toast.error("Registration failed");
} finally {
setIsLoading(false);
}
};
return (
<div className="max-w-md mx-auto p-10 bg-white rounded-lg shadow-lg">
<Toaster position="top-center" />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="Enter your first name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Enter your last name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Enter your email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repeatPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Repeat Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Repeat your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full bg-orange-500 hover:bg-orange-600"
disabled={isLoading}
>
{isLoading ? "Creating Account..." : "Create Account"}
</Button>
</form>
</Form>
</div>
);
}

@ -0,0 +1,131 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast, Toaster } from "sonner";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/lib/auth-context";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
password: z
.string()
.min(6, { message: "Password must be at least 6 characters" }),
});
export function LoginForm() {
const router = useRouter();
const searchParams = new URLSearchParams(
typeof window !== "undefined" ? window.location.search : ""
);
const redirectPath = searchParams.get("redirect") || "/";
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
});
const { login } = useAuth();
const onSubmit = async (data: z.infer<typeof formSchema>) => {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
toast.error(result.error || "Login failed");
return;
}
// Use the auth context to handle login
login(result.token, {
...result.user,
FirstName: result.user.FirstName,
LastName: result.user.LastName,
Email: result.user.Email,
});
toast.success("Login successful!");
// Redirect to the original destination or default route
router.push(redirectPath);
} catch (error) {
console.error("Login error:", error);
toast.error("Connection error. Please try again.");
}
};
return (
<div className="max-w-md mx-auto p-6 md:p-8 bg-white rounded-lg shadow-md">
<Toaster position="top-center" richColors />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Enter your email"
{...field}
className="focus-visible:ring-orange-500"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
className="focus-visible:ring-orange-500"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full bg-orange-500 hover:bg-orange-600 cursor-pointer text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Login
</Button>
</form>
</Form>
</div>
);
}

@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white 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",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
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: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

@ -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 "@/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,
}

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/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 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-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/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 }

@ -0,0 +1,69 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
interface User {
id: string;
FirstName: string;
LastName: string;
Email: string;
role?: string;
}
interface AuthContextType {
user: User | null;
token: string | null;
login: (token: string, user: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// Check for stored auth data on mount
const storedToken = localStorage.getItem("token");
const storedUser = localStorage.getItem("user");
if (storedToken && storedUser) {
setToken(storedToken);
setUser(JSON.parse(storedUser));
}
}, []);
const login = (newToken: string, newUser: User) => {
setToken(newToken);
setUser(newUser);
localStorage.setItem("token", newToken);
localStorage.setItem("user", JSON.stringify(newUser));
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem("token");
localStorage.removeItem("user");
};
const isAuthenticated = !!token && !!user;
return (
<AuthContext.Provider
value={{ user, token, login, logout, isAuthenticated }}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

@ -0,0 +1,67 @@
import { Pool } from "pg";
// Database configuration
export interface DatabaseConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
}
export function getDatabaseConfig(): DatabaseConfig {
// Parse DATABASE_URL if it exists, otherwise use individual env vars
if (process.env.DATABASE_URL) {
const url = new URL(process.env.DATABASE_URL);
return {
host: url.hostname,
port: parseInt(url.port) || 5432,
database: url.pathname.slice(1), // Remove leading slash
username: url.username,
password: url.password,
};
}
return {
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME || "reya_render",
username: process.env.DB_USER || "miranshala",
password: process.env.DB_PASSWORD || "",
};
}
// Create a connection pool
let pool: Pool | null = null;
export function getPool(): Pool {
if (!pool) {
const config = getDatabaseConfig();
pool = new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.username,
password: config.password,
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
});
}
return pool;
}
// Test database connection
export async function testConnection(): Promise<boolean> {
try {
const pool = getPool();
const client = await pool.connect();
await client.query("SELECT NOW()");
client.release();
console.log("Database connection successful");
return true;
} catch (error) {
console.error("Database connection failed:", error);
return false;
}
}

916
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,14 +9,28 @@
"lint": "eslint"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.3",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/pg": "^8.15.5",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.546.0",
"next": "16.0.0",
"next-intl": "^4.4.0",
"pg": "^8.16.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.3.1"
"react-hook-form": "^7.65.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Loading…
Cancel
Save