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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 9.0 KiB |
Loading…
Reference in new issue