AI slop goes brr

This commit is contained in:
Anushlinux
2025-07-03 01:11:57 +05:30
parent afd9bb194a
commit ef98f8e1ba
13 changed files with 799 additions and 114 deletions

2
.gitignore vendored
View File

@@ -11,7 +11,7 @@ node_modules
.env.development.local
.env.test.local
.env.production.local
.cursorignore
# Testing
coverage

View File

@@ -0,0 +1,80 @@
import { db, jobs, companies, applications, students } from '@workspace/db';
import { eq } from '@workspace/db/drizzle';
import { notFound } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@workspace/ui/components/card';
import { Table, TableBody, TableHead, TableHeader, TableRow, TableCell } from '@workspace/ui/components/table';
interface JobPageProps {
params: { jobId: string };
}
export const dynamic = 'force-dynamic';
export default async function JobDetailPage({ params }: JobPageProps) {
const jobId = Number(params.jobId);
if (isNaN(jobId)) notFound();
const jobRes = await db.select().from(jobs).where(eq(jobs.id, jobId)).limit(1);
if (jobRes.length === 0) notFound();
const job = jobRes[0];
const companyRes = await db.select().from(companies).where(eq(companies.id, job.companyId)).limit(1);
const company = companyRes[0];
const applicants = await db
.select({
applicationId: applications.id,
status: applications.status,
firstName: students.firstName,
lastName: students.lastName,
email: students.email,
})
.from(applications)
.leftJoin(students, eq(applications.studentId, students.id))
.where(eq(applications.jobId, jobId));
return (
<div className="container mx-auto py-10 space-y-10">
<Card>
<CardHeader>
<CardTitle>{job.title}</CardTitle>
<CardDescription>Company: {company?.name ?? 'Unknown'}</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-sm"><strong>Location:</strong> {job.location}</p>
<p className="text-sm"><strong>Salary:</strong> {job.salary}</p>
<p className="text-sm"><strong>Deadline:</strong> {job.applicationDeadline.toLocaleDateString()}</p>
<p className="whitespace-pre-line mt-4">{job.description}</p>
</CardContent>
</Card>
<section className="space-y-4">
<h2 className="text-2xl font-semibold tracking-tight">Students Applied</h2>
{applicants.length === 0 ? (
<p className="text-muted-foreground text-sm">No applications yet.</p>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{applicants.map((a) => (
<TableRow key={a.applicationId}>
<TableCell>{`${a.firstName ?? ''} ${a.lastName ?? ''}`.trim()}</TableCell>
<TableCell>{a.email}</TableCell>
<TableCell className="capitalize">{a.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</section>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import { db, jobs, companies } from '@workspace/db';
import { eq } from '@workspace/db/drizzle';
import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@workspace/ui/components/card';
import { Button } from '@workspace/ui/components/button';
export const dynamic = 'force-dynamic';
async function getAllJobsWithCompany() {
const allJobs = await db.select().from(jobs);
const allCompanies = await db.select().from(companies);
return allJobs.map(job => ({
...job,
company: allCompanies.find(c => c.id === job.companyId) || null,
}));
}
export default async function JobsListPage() {
const jobsWithCompany = await getAllJobsWithCompany();
return (
<div className="container mx-auto py-10 space-y-8">
<h1 className="text-3xl font-bold mb-6">All Jobs</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{jobsWithCompany.length === 0 && (
<p className="text-muted-foreground">No jobs found.</p>
)}
{jobsWithCompany.map((job) => (
<Card key={job.id} className="flex flex-col bg-white text-black border border-gray-200">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>{job.title}</span>
<span className="ml-auto text-xs px-2 py-1 rounded bg-gray-100 border text-gray-600">
{job.active ? 'Active' : 'Inactive'}
</span>
</CardTitle>
<CardDescription>
<span>Company: {job.company?.name ?? 'Unknown'}</span>
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center gap-2">
<img src={job.company?.imageURL} alt={job.company?.name} className="w-10 h-10 rounded object-cover border" />
<div>
<div className="font-semibold">{job.company?.name}</div>
<div className="text-xs text-gray-500">{job.company?.email}</div>
</div>
</div>
<div className="mt-2">
<div className="text-sm"><strong>Location:</strong> {job.location}</div>
<div className="text-sm"><strong>Salary:</strong> {job.salary}</div>
<div className="text-sm"><strong>Deadline:</strong> {job.applicationDeadline.toLocaleDateString()}</div>
<div className="text-sm"><strong>Min CGPA:</strong> {job.minCGPA}</div>
<div className="text-sm"><strong>Min SSC:</strong> {job.minSSC}</div>
<div className="text-sm"><strong>Min HSC:</strong> {job.minHSC}</div>
<div className="text-sm"><strong>Allow Dead KT:</strong> {job.allowDeadKT ? 'Yes' : 'No'}</div>
<div className="text-sm"><strong>Allow Live KT:</strong> {job.allowLiveKT ? 'Yes' : 'No'}</div>
<div className="text-sm"><strong>Job Link:</strong> <a href={job.link} target="_blank" rel="noopener noreferrer" className="underline text-primary">{job.link}</a></div>
<div className="text-sm"><strong>Description:</strong> <span className="whitespace-pre-line">{job.description}</span></div>
<div className="text-xs text-gray-400 mt-2">Created: {job.createdAt.toLocaleDateString()} | Updated: {job.updatedAt.toLocaleDateString()}</div>
</div>
</CardContent>
<div className="p-4 pt-0 mt-auto flex gap-2">
<Link href={`/jobs/${job.id}`}>
<Button variant="outline" className="bg-white text-primary border-primary">View Details</Button>
</Link>
</div>
</Card>
))}
</div>
</div>
);
}

View File

@@ -9,28 +9,64 @@ import {
} from '@workspace/ui/components/navigation-menu';
import Link from 'next/link';
const navLinks = [
{ href: '/', label: 'Home', icon: '🏠' },
{ href: '/students', label: 'Students', icon: '🎓' },
{ href: '/jobs', label: 'Jobs', icon: '💼' },
];
export default function MainLayout({ children }: { children: React.ReactNode }) {
// Helper to check active link (client-side only)
const isActive = (href: string) => {
if (typeof window === 'undefined') return false;
if (href === '/') return window.location.pathname === '/';
return window.location.pathname.startsWith(href);
};
return (
<div>
<header className="flex h-16 items-center justify-between border-b bg-background px-4 md:px-6">
<nav>
<div className="flex min-h-screen bg-background text-foreground">
{/* Sidebar */}
<aside className="hidden md:flex flex-col w-64 p-6 gap-8">
<div className="sticky top-8">
<div className="flex flex-col gap-6 rounded-2xl shadow-xl bg-sidebar border border-sidebar-border p-6">
<div className="flex items-center gap-3 mb-6">
<img src="/favicon.ico" alt="Logo" className="w-10 h-10" />
<span className="font-extrabold text-xl tracking-tight text-sidebar-primary">Admin Portal</span>
</div>
<div className="text-xs uppercase font-semibold text-muted-foreground mb-2 tracking-widest pl-1">Navigation</div>
<nav className="flex flex-col gap-2">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="flex items-center gap-3 px-4 py-2 rounded-xl font-medium transition-colors text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-accent data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
data-active={typeof window !== 'undefined' && isActive(link.href)}
>
<span className="text-lg">{link.icon}</span>
{link.label}
</Link>
))}
</nav>
</div>
</div>
</aside>
{/* Main content */}
<div className="flex-1 flex flex-col min-h-screen">
<header className="md:hidden flex items-center justify-between h-16 border-b bg-background px-4">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
{navLinks.map((link) => (
<NavigationMenuItem key={link.href}>
<NavigationMenuLink asChild>
<Link href="/">Home</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link href="/students">Students</Link>
<Link href={link.href}>{link.label}</Link>
</NavigationMenuLink>
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
</nav>
</header>
<main>{children}</main>
<main className="flex-1 bg-background text-foreground p-4 md:p-10">{children}</main>
</div>
</div>
);
}

View File

@@ -1,26 +1,159 @@
import Studs from '@/components/studs';
import { db, students } from '@workspace/db';
import { auth, signOut } from '@/auth';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Input } from '@workspace/ui/components/input';
import { Textarea } from '@workspace/ui/components/textarea';
import { Button } from '@workspace/ui/components/button';
import Link from 'next/link';
import { revalidatePath } from 'next/cache';
import { db, companies, jobs } from '@workspace/db';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from '@workspace/ui/components/dialog';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@workspace/ui/components/accordion';
import { Badge } from '@workspace/ui/components/badge';
async function getStudents() {
// -----------------------
// Server Actions
// -----------------------
async function createCompany(formData: FormData) {
'use server';
const s = await db.select().from(students);
console.log(s);
const name = String(formData.get('name') ?? '').trim();
const email = String(formData.get('email') ?? '').trim();
const link = String(formData.get('link') ?? '').trim();
const description = String(formData.get('description') ?? '').trim() || 'N/A';
const imageURL = String(formData.get('imageURL') ?? '').trim() || 'https://via.placeholder.com/200x200?text=Company';
if (!name) return;
await db.insert(companies).values({ name, email, link, description, imageURL });
revalidatePath('/');
}
async function logOut() {
async function createJob(formData: FormData) {
'use server';
await signOut();
const companyId = Number(formData.get('companyId'));
const title = String(formData.get('title') ?? '').trim();
const link = String(formData.get('jobLink') ?? '').trim();
const description = String(formData.get('jobDescription') ?? '').trim() || 'N/A';
const location = String(formData.get('location') ?? '').trim() || 'N/A';
const imageURL = String(formData.get('jobImageURL') ?? '').trim() || 'https://via.placeholder.com/100x100?text=Job';
const salary = String(formData.get('salary') ?? '').trim() || 'N/A';
const deadlineRaw = formData.get('deadline');
const applicationDeadline = deadlineRaw ? new Date(String(deadlineRaw)) : new Date();
if (!companyId || !title) return;
await db.insert(jobs).values({
companyId,
title,
link,
description,
location,
imageURL,
salary,
applicationDeadline,
active: true,
});
revalidatePath('/');
}
export default async function Page() {
const session = await auth();
// -----------------------
// Component Helpers
// -----------------------
async function getDashboardData() {
const comps = await db.select().from(companies);
const allJobs = await db.select().from(jobs);
return comps.map((comp) => ({
...comp,
jobs: allJobs.filter((j) => j.companyId === comp.id),
}));
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<div className="flex items-center justify-center min-h-svh">
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Hello admin {session?.user?.name}</h1>
<Studs action={getStudents} logOut={logOut} />
<div className="container mx-auto py-10 space-y-10">
<section className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-primary">Companies Dashboard</h1>
<p className="text-muted-foreground mt-1">Manage companies and their job openings.</p>
</div>
<Dialog>
<DialogTrigger asChild>
<Button className="bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Add New Company</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a new company</DialogTitle>
<DialogDescription>
Fill in the details below to add a new company to the portal.
</DialogDescription>
</DialogHeader>
<form action={createCompany} className="grid grid-cols-1 gap-4 py-4">
<Input name="name" placeholder="Company name" required />
<Input name="email" placeholder="Contact email" type="email" required />
<Input name="link" placeholder="Website / careers link" />
<Input name="imageURL" placeholder="Image URL" />
<Textarea name="description" placeholder="Short description" />
<Button type="submit" className="w-fit">Add Company</Button>
</form>
</DialogContent>
</Dialog>
</section>
<section className="space-y-6">
{data.length === 0 && <Card className="p-10 text-muted-foreground text-center shadow-lg border-border bg-card">No companies yet. Add your first company to get started!</Card>}
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{data.map((company) => (
<Card key={company.id} className="flex flex-col shadow-xl border border-border bg-card rounded-2xl overflow-hidden">
<CardHeader className="p-6">
<CardTitle className="flex items-center gap-4">
<img src={company.imageURL} alt={company.name} className="w-14 h-14 rounded-xl border-2 border-border object-cover" />
<div>
<h3 className="font-bold text-lg">{company.name}</h3>
<a href={company.link} target="_blank" rel="noopener noreferrer" className="text-sm text-primary hover:underline">{company.link}</a>
</div>
</CardTitle>
</CardHeader>
<CardContent className="px-6 pb-2 space-y-3">
<h4 className="font-semibold text-muted-foreground">Open Positions</h4>
{company.jobs.length === 0 && <p className="text-sm text-muted-foreground/80">No jobs yet.</p>}
{company.jobs.map((job) => (
<Link key={job.id} href={`/jobs/${job.id}`} className="flex justify-between items-center p-3 rounded-lg hover:bg-background transition-colors border border-transparent hover:border-border">
<span>{job.title}</span>
<Badge variant={job.active ? "secondary" : "outline"}>{job.active ? "Active" : "Inactive"}</Badge>
</Link>
))}
</CardContent>
<CardFooter className="mt-auto p-0">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="add-job">
<AccordionTrigger className="px-6 text-primary hover:text-primary/90 font-semibold">
Add New Job
</AccordionTrigger>
<AccordionContent className="p-6 bg-background border-t">
<form action={createJob} className="flex flex-col w-full gap-3">
<input type="hidden" name="companyId" value={company.id} />
<Input name="title" placeholder="Job title" required />
<Input name="jobLink" placeholder="Job link" />
<Input name="location" placeholder="Location" />
<Input name="salary" placeholder="Salary" />
<Input name="deadline" type="date" />
<Textarea name="jobDescription" placeholder="Job description" />
<Button type="submit" size="sm" className="bg-accent text-accent-foreground hover:bg-accent/90 transition-colors self-start">Add Job</Button>
</form>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardFooter>
</Card>
))}
</div>
</section>
</div>
);
}
export const dynamic = 'force-dynamic';

View File

@@ -1,26 +1,61 @@
import { columns, Student } from './columns';
import { DataTable } from './data-table';
import { db, students } from '@workspace/db';
import { Input } from '@workspace/ui/components/input';
import { Button } from '@workspace/ui/components/button';
import { revalidatePath } from 'next/cache';
import { eq } from '@workspace/db/drizzle';
import { Card } from '@workspace/ui/components/card';
async function getData(): Promise<Student[]> {
const data = await db.select().from(students);
return data;
}
async function addStudent(formData: FormData) {
'use server';
const email = String(formData.get('email') ?? '').trim();
if (!email) return;
const exists = await db.select().from(students).where(eq(students.email, email)).limit(1);
if (exists.length === 0) {
await db.insert(students).values({ email });
}
revalidatePath('/students');
}
async function StudentsTable() {
const data = await getData();
return (
<div className="container mx-auto py-10">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold tracking-tight">Students</h1>
<div className="space-y-8">
{/* Add Student */}
<Card className="p-6 shadow-md border border-border bg-card">
<h2 className="text-2xl font-bold mb-4 text-primary">Add Student</h2>
<form action={addStudent} className="flex gap-2 items-end">
<Input name="email" type="email" placeholder="Student email" className="max-w-sm" required />
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Add Student</Button>
</form>
</Card>
{/* Students Table */}
<Card className="p-6 shadow-md border border-border bg-card">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-semibold tracking-tight text-accent">Students</h1>
<div className="text-sm text-muted-foreground">
{data.length} {data.length === 1 ? 'student' : 'students'} total
</div>
</div>
{data.length === 0 ? (
<div className="text-center text-muted-foreground">No students yet. Add your first student above!</div>
) : (
<DataTable columns={columns} data={data} />
)}
</Card>
</div>
{/* Toast placeholder for feedback */}
</div>
);
}

View File

@@ -8,19 +8,34 @@ async function logIn() {
export default async function Page() {
return (
<div className="flex items-center justify-center min-h-svh">
<div className="flex flex-col items-center justify-center gap-4">
<form action={logIn}>
<Button type="submit" variant="outline" className="w-full h-12">
<div className="absolute inset-0 bg-gradient-to-r from-primary/0 via-primary/10 to-primary/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" />
<div className="relative min-h-svh flex items-center justify-center overflow-hidden bg-gradient-to-br from-blue-100 via-red-100 to-pink-100 transition-colors duration-500">
{/* Animated floating shapes */}
<div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute top-10 left-1/4 w-32 h-32 bg-rose-200/40 rounded-full blur-2xl animate-float-slow" />
<div className="absolute bottom-20 right-1/4 w-40 h-40 bg-blue-200/30 rounded-full blur-2xl animate-float-medium" />
<div className="absolute top-1/2 left-1/2 w-24 h-24 bg-red-300/30 rounded-full blur-2xl animate-float-fast" />
</div>
<div className="relative z-10 backdrop-blur-md bg-white/70 rounded-2xl shadow-2xl p-10 flex flex-col items-center gap-8 border border-white/30 max-w-sm w-full transition-all duration-300 hover:shadow-[0_0_32px_4px_rgba(239,68,68,0.25)] hover:border-red-400/60">
<div className="flex flex-col items-center gap-2">
{/* Animated logo */}
<img src="/favicon.ico" alt="Logo" className="w-14 h-14 mb-2 drop-shadow-lg animate-bounce-slow" />
<h1 className="text-2xl font-bold text-gray-800 tracking-tight">Placement Portal Admin</h1>
<p className="text-gray-500 text-sm text-center">Sign in to manage placements and students</p>
<p className="text-xs text-red-500 font-semibold italic mt-1 animate-fade-in">Empower your journey. Shape the future.</p>
</div>
<form action={logIn} className="w-full">
<Button type="submit" variant="outline" className="w-full h-12 relative overflow-hidden group rounded-lg shadow-md hover:shadow-lg transition-all focus:ring-2 focus:ring-red-400">
<span className="absolute inset-0 bg-gradient-to-r from-red-200/0 via-red-200/20 to-red-200/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-out pointer-events-none" />
<img
src="https://static.cdnlogo.com/logos/g/35/google-icon.svg"
alt="Google logo"
className="w-5 h-5 transition-transform duration-200"
className="w-5 h-5 mr-2 inline-block align-middle"
/>
<span className="relative z-10 font-medium transition-colors duration-200 group-hover:text-foreground">
<span className="relative z-10 font-medium transition-colors duration-200 group-hover:text-red-700 group-hover:drop-shadow-md">
Sign in with Google
</span>
{/* Button ripple effect */}
<span className="absolute left-1/2 top-1/2 w-0 h-0 bg-red-300/40 rounded-full opacity-0 group-active:opacity-100 group-active:w-32 group-active:h-32 group-active:animate-ripple -translate-x-1/2 -translate-y-1/2 pointer-events-none" />
</Button>
</form>
</div>

View File

@@ -8,8 +8,10 @@
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",

View File

@@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { cn } from "@workspace/ui/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b-0", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>span:last-child]:rotate-180",
className
)}
{...props}
>
{children}
<span className="h-4 w-4 shrink-0 transition-transform duration-200"></span>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@workspace/ui/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,121 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@workspace/ui/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<span className="text-2xl font-thin" aria-hidden="true">×</span>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -13,74 +13,39 @@
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(0.98 0.003 240);
--foreground: oklch(0.12 0.008 240);
--card: oklch(0.96 0.004 240);
--card-foreground: oklch(0.12 0.008 240);
--background: oklch(0.98 0.01 220); /* Soft blue-tinted white */
--foreground: oklch(0.16 0.01 240); /* Slightly deeper text */
--card: oklch(0.96 0.01 220); /* Slight blue card */
--card-foreground: oklch(0.16 0.01 240);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.12 0.008 240);
--primary: oklch(0.35 0.18 240);
--primary-foreground: oklch(0.98 0.003 240);
--secondary: oklch(0.88 0.008 240);
--secondary-foreground: oklch(0.12 0.008 240);
--muted: oklch(0.92 0.006 240);
--muted-foreground: oklch(0.4 0.01 240);
--accent: oklch(0.42 0.22 10);
--accent-foreground: oklch(0.98 0.003 240);
--destructive: oklch(0.45 0.25 15);
--destructive-foreground: oklch(0.98 0.003 240);
--border: oklch(0.86 0.008 240);
--input: oklch(0.94 0.006 240);
--ring: oklch(0.35 0.18 240);
--chart-1: oklch(0.35 0.18 240);
--chart-2: oklch(0.42 0.22 10);
--popover-foreground: oklch(0.16 0.01 240);
--primary: oklch(0.55 0.18 260); /* Vibrant blue */
--primary-foreground: oklch(0.98 0.01 220);
--secondary: oklch(0.92 0.01 120); /* Soft green */
--secondary-foreground: oklch(0.16 0.01 240);
--muted: oklch(0.94 0.01 220);
--muted-foreground: oklch(0.45 0.01 240);
--accent: oklch(0.72 0.18 40); /* Vibrant orange */
--accent-foreground: oklch(0.98 0.01 220);
--destructive: oklch(0.65 0.22 25); /* Strong red */
--destructive-foreground: oklch(0.98 0.01 220);
--border: oklch(0.88 0.01 220);
--input: oklch(0.96 0.01 220);
--ring: oklch(0.55 0.18 260);
--chart-1: oklch(0.55 0.18 260);
--chart-2: oklch(0.72 0.18 40);
--chart-3: oklch(0.38 0.16 260);
--chart-4: oklch(0.4 0.2 350);
--chart-5: oklch(0.36 0.15 220);
--radius: 0.625rem;
--sidebar: oklch(0.94 0.006 240);
--sidebar-foreground: oklch(0.12 0.008 240);
--sidebar-primary: oklch(0.35 0.18 240);
--sidebar-primary-foreground: oklch(0.98 0.003 240);
--sidebar-accent: oklch(0.42 0.22 10);
--sidebar-accent-foreground: oklch(0.98 0.003 240);
--sidebar-border: oklch(0.86 0.008 240);
--sidebar-ring: oklch(0.35 0.18 240);
}
.dark {
--background: oklch(0.06 0.008 240);
--foreground: oklch(0.94 0.004 240);
--card: oklch(0.1 0.01 240);
--card-foreground: oklch(0.94 0.004 240);
--popover: oklch(0.1 0.01 240);
--popover-foreground: oklch(0.94 0.004 240);
--primary: oklch(0.55 0.22 240);
--primary-foreground: oklch(0.06 0.008 240);
--secondary: oklch(0.16 0.01 240);
--secondary-foreground: oklch(0.94 0.004 240);
--muted: oklch(0.12 0.01 240);
--muted-foreground: oklch(0.6 0.01 240);
--accent: oklch(0.62 0.28 10);
--accent-foreground: oklch(0.06 0.008 240);
--destructive: oklch(0.65 0.3 15);
--destructive-foreground: oklch(0.06 0.008 240);
--border: oklch(0.18 0.01 240);
--input: oklch(0.14 0.01 240);
--ring: oklch(0.55 0.22 240);
--chart-1: oklch(0.55 0.22 240);
--chart-2: oklch(0.62 0.28 10);
--chart-3: oklch(0.58 0.2 260);
--chart-4: oklch(0.6 0.25 350);
--chart-5: oklch(0.56 0.18 220);
--sidebar: oklch(0.08 0.01 240);
--sidebar-foreground: oklch(0.94 0.004 240);
--sidebar-primary: oklch(0.55 0.22 240);
--sidebar-primary-foreground: oklch(0.06 0.008 240);
--sidebar-accent: oklch(0.62 0.28 10);
--sidebar-accent-foreground: oklch(0.06 0.008 240);
--sidebar-border: oklch(0.18 0.01 240);
--sidebar-ring: oklch(0.55 0.22 240);
--radius: 0.75rem;
--sidebar: oklch(0.97 0.01 220);
--sidebar-foreground: oklch(0.16 0.01 240);
--sidebar-primary: oklch(0.55 0.18 260);
--sidebar-primary-foreground: oklch(0.98 0.01 220);
--sidebar-accent: oklch(0.72 0.18 40);
--sidebar-accent-foreground: oklch(0.98 0.01 220);
--sidebar-border: oklch(0.88 0.01 220);
--sidebar-ring: oklch(0.55 0.18 260);
}
@theme inline {
@@ -130,3 +95,36 @@
@apply bg-background text-foreground;
}
}
/* Login page creative animation keyframes (red/rose accent theme) */
@keyframes float-slow {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-20px) scale(1.05); }
}
@keyframes float-medium {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(30px) scale(1.1); }
}
@keyframes float-fast {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-15px) scale(0.95); }
}
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes ripple {
to { opacity: 0; width: 200px; height: 200px; }
}
/* Utility classes for red/rose accent theme */
.animate-float-slow { animation: float-slow 7s ease-in-out infinite; }
.animate-float-medium { animation: float-medium 5s ease-in-out infinite; }
.animate-float-fast { animation: float-fast 3.5s ease-in-out infinite; }
.animate-bounce-slow { animation: bounce-slow 2.5s infinite; }
.animate-fade-in { animation: fade-in 1.2s 0.5s both; }
.animate-ripple { animation: ripple 0.6s linear; }

104
pnpm-lock.yaml generated
View File

@@ -226,11 +226,17 @@ importers:
'@hookform/resolvers':
specifier: ^5.1.1
version: 5.1.1(react-hook-form@7.59.0(react@19.1.0))
'@radix-ui/react-accordion':
specifier: ^1.2.0
version: 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dialog':
specifier: ^1.1.1
version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-label':
specifier: ^2.1.7
specifier: ^2.1.0
version: 2.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-navigation-menu':
specifier: ^1.2.13
specifier: ^1.2.0
version: 1.2.13(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-progress':
specifier: ^1.1.7
@@ -928,6 +934,19 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-accordion@1.2.11':
resolution: {integrity: sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies:
@@ -941,6 +960,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.11':
resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -972,6 +1004,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.14':
resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
@@ -3912,6 +3957,23 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-accordion@1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -3921,6 +3983,22 @@ snapshots:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
@@ -3945,6 +4023,28 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
aria-hidden: 1.2.6
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-direction@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0