finally building
This commit is contained in:
36
apps/admin/app/(main)/layout.tsx
Normal file
36
apps/admin/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from '@workspace/ui/components/navigation-menu';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className="flex h-16 items-center justify-between border-b bg-background px-4 md:px-6">
|
||||||
|
<nav>
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/students">Students</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Login from '@/components/login';
|
import Login from '@/components/login';
|
||||||
import Studs from '@/components/studs';
|
import Studs from '@/components/studs';
|
||||||
import { db, admins } from '@workspace/db';
|
import { db, admins } from '@workspace/db';
|
||||||
import { auth, signIn, signOut } from '@workspace/auth';
|
import { auth, signIn, signOut } from '@/auth';
|
||||||
|
|
||||||
async function getStudents() {
|
async function getStudents() {
|
||||||
'use server';
|
'use server';
|
||||||
25
apps/admin/app/(main)/students/columns.tsx
Normal file
25
apps/admin/app/(main)/students/columns.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { createSelectSchema, students } from '@workspace/db';
|
||||||
|
import * as z from 'zod/v4';
|
||||||
|
|
||||||
|
const studentSelectSchema = createSelectSchema(students);
|
||||||
|
export type Student = z.infer<typeof studentSelectSchema>;
|
||||||
|
|
||||||
|
export const columns: ColumnDef<Student>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'firstName',
|
||||||
|
header: 'First Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'lastName',
|
||||||
|
header: 'Last Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
},
|
||||||
|
];
|
||||||
66
apps/admin/app/(main)/students/data-table.tsx
Normal file
66
apps/admin/app/(main)/students/data-table.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@workspace/ui/components/table';
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/admin/app/(main)/students/page.tsx
Normal file
18
apps/admin/app/(main)/students/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { columns, Student } from './columns';
|
||||||
|
import { DataTable } from './data-table';
|
||||||
|
import { db, students } from '@workspace/db';
|
||||||
|
|
||||||
|
async function getData(): Promise<Student[]> {
|
||||||
|
const data = db.select().from(students);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function DemoPage() {
|
||||||
|
const data = await getData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10">
|
||||||
|
<DataTable columns={columns} data={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
import { handlers } from '@workspace/auth';
|
import { handlers } from '@/auth';
|
||||||
export const { GET, POST } = handlers;
|
export const { GET, POST } = handlers;
|
||||||
|
|||||||
29
apps/admin/app/login/page.tsx
Normal file
29
apps/admin/app/login/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { signIn } from '@/auth';
|
||||||
|
|
||||||
|
async function logIn() {
|
||||||
|
'use server';
|
||||||
|
await signIn('google', { redirectTo: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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" />
|
||||||
|
<img
|
||||||
|
src="https://static.cdnlogo.com/logos/g/35/google-icon.svg"
|
||||||
|
alt="Google logo"
|
||||||
|
className="w-5 h-5 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
<span className="relative z-10 font-medium transition-colors duration-200 group-hover:text-foreground">
|
||||||
|
Sign in with Google
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,31 @@
|
|||||||
import NextAuth, { type NextAuthConfig } from 'next-auth';
|
import NextAuth, { type DefaultSession } from 'next-auth';
|
||||||
import Google from 'next-auth/providers/google';
|
import type { NextAuthConfig } from 'next-auth';
|
||||||
|
import Google from "next-auth/providers/google";
|
||||||
import { db, admins, students } from '@workspace/db';
|
import { db, admins, students } from '@workspace/db';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from '@workspace/db/drizzle';
|
||||||
|
|
||||||
|
declare module 'next-auth' {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
role?: 'ADMIN' | 'USER';
|
||||||
|
adminId?: number;
|
||||||
|
studentId?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JWT {
|
||||||
|
role?: 'ADMIN' | 'USER';
|
||||||
|
adminId?: number;
|
||||||
|
studentId?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'next/server' {
|
||||||
|
interface NextRequest {
|
||||||
|
auth: import('next-auth').Session | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const authConfig: NextAuthConfig = {
|
const authConfig: NextAuthConfig = {
|
||||||
providers: [Google],
|
providers: [Google],
|
||||||
@@ -58,9 +82,6 @@ const authConfig: NextAuthConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAuth = NextAuth(authConfig);
|
// Note: TypeScript warnings about inferred types are expected with NextAuth v5 beta
|
||||||
|
// These warnings don't affect functionality
|
||||||
export const handlers: typeof nextAuth.handlers = nextAuth.handlers;
|
export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);
|
||||||
export const signIn: typeof nextAuth.signIn = nextAuth.signIn;
|
|
||||||
export const signOut: typeof nextAuth.signOut = nextAuth.signOut;
|
|
||||||
export const auth: typeof nextAuth.auth = nextAuth.auth;
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
import { signIn } from '@workspace/auth';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
|
||||||
export default function Login({ action }: { action: () => Promise<void> }) {
|
export default function Login({ action }: { action: () => Promise<void> }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { auth } from '@workspace/auth';
|
import { auth } from '@/auth';
|
||||||
|
|
||||||
export default async function Studs({
|
export default async function Studs({
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { auth } from '@workspace/auth';
|
import { auth } from '@/auth';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
|
||||||
export default auth((req: any) => {
|
export default auth((req: NextRequest) => {
|
||||||
// If the user is unauthenticated or an admin, allow the request.
|
if (!req.auth) {
|
||||||
if (!req.auth || req.auth.user?.role === 'ADMIN') {
|
return NextResponse.redirect(new URL('/login', req.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.auth.user?.role === 'ADMIN') {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,8 +14,8 @@ export default auth((req: any) => {
|
|||||||
const studentUrl = process.env.STUDENT_URL ?? 'http://localhost:3000';
|
const studentUrl = process.env.STUDENT_URL ?? 'http://localhost:3000';
|
||||||
|
|
||||||
return NextResponse.redirect(new URL(studentUrl, req.url));
|
return NextResponse.redirect(new URL(studentUrl, req.url));
|
||||||
}) as any;
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login).*)'],
|
||||||
};
|
};
|
||||||
|
|||||||
19
apps/admin/next-auth.d.ts
vendored
19
apps/admin/next-auth.d.ts
vendored
@@ -1,19 +0,0 @@
|
|||||||
import 'next-auth';
|
|
||||||
|
|
||||||
declare module 'next-auth' {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
declare module 'next-auth/jwt' {
|
|
||||||
interface JWT {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: ['@workspace/ui', '@workspace/db', '@workspace/auth'],
|
transpilePackages: ['@workspace/ui', '@workspace/db'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -12,24 +12,25 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workspace/auth": "workspace:*",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
"framer-motion": "^12.19.1",
|
"framer-motion": "^12.22.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.3.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.19.4",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@workspace/eslint-config": "workspace:^",
|
"@workspace/eslint-config": "workspace:^",
|
||||||
"@workspace/typescript-config": "workspace:*",
|
"@workspace/typescript-config": "workspace:*",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,18 @@
|
|||||||
"extends": "@workspace/typescript-config/nextjs.json",
|
"extends": "@workspace/typescript-config/nextjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": [
|
||||||
"@workspace/ui/*": ["../../packages/ui/src/*"],
|
"./*"
|
||||||
"@workspace/db/*": ["../../packages/db/src/*"],
|
],
|
||||||
"@workspace/auth/*": ["../../packages/auth/src/*"]
|
"@workspace/ui/*": [
|
||||||
|
"../../packages/ui/src/*"
|
||||||
|
],
|
||||||
|
"@workspace/db": [
|
||||||
|
"../../packages/db/index.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -14,6 +21,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"next-env.d.ts",
|
||||||
|
"next.config.ts",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
36
apps/student/app/(main)/layout.tsx
Normal file
36
apps/student/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from '@workspace/ui/components/navigation-menu';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className="flex h-16 items-center justify-between border-b bg-background px-4 md:px-6">
|
||||||
|
<nav>
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/signup">Signup</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Login from '@/components/login';
|
import Login from '@/components/login';
|
||||||
import Studs from '@/components/studs';
|
import Studs from '@/components/studs';
|
||||||
import { db, admins } from '@workspace/db';
|
import { db, admins } from '@workspace/db';
|
||||||
import { auth, signIn, signOut } from '@workspace/auth';
|
import { auth, signIn, signOut } from '@/auth';
|
||||||
|
|
||||||
async function getStudents() {
|
async function getStudents() {
|
||||||
'use server';
|
'use server';
|
||||||
@@ -25,7 +25,7 @@ export default async function Page() {
|
|||||||
<div className="flex items-center justify-center min-h-svh">
|
<div className="flex items-center justify-center min-h-svh">
|
||||||
<div className="flex flex-col items-center justify-center gap-4">
|
<div className="flex flex-col items-center justify-center gap-4">
|
||||||
<h1 className="text-2xl font-bold">Hello student {session?.user?.name}</h1>
|
<h1 className="text-2xl font-bold">Hello student {session?.user?.name}</h1>
|
||||||
{!session?.user && <Login logIn={logIn} />}
|
{!session?.user && <Login action={logIn} />}
|
||||||
<Studs action={getStudents} logOut={logOut} />
|
<Studs action={getStudents} logOut={logOut} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
import { handlers } from '@workspace/auth';
|
import { handlers } from '@/auth';
|
||||||
export const { GET, POST } = handlers;
|
export const { GET, POST } = handlers;
|
||||||
|
|||||||
29
apps/student/app/login/page.tsx
Normal file
29
apps/student/app/login/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { signIn } from '@/auth';
|
||||||
|
|
||||||
|
async function logIn() {
|
||||||
|
'use server';
|
||||||
|
await signIn('google', { redirectTo: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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" />
|
||||||
|
<img
|
||||||
|
src="https://static.cdnlogo.com/logos/g/35/google-icon.svg"
|
||||||
|
alt="Google logo"
|
||||||
|
className="w-5 h-5 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
<span className="relative z-10 font-medium transition-colors duration-200 group-hover:text-foreground">
|
||||||
|
Sign in with Google
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
apps/student/auth.ts
Normal file
87
apps/student/auth.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import NextAuth, { type DefaultSession } from 'next-auth';
|
||||||
|
import type { NextAuthConfig } from 'next-auth';
|
||||||
|
import Google from "next-auth/providers/google";
|
||||||
|
import { db, admins, students } from '@workspace/db';
|
||||||
|
import { eq } from '@workspace/db/drizzle';
|
||||||
|
|
||||||
|
declare module 'next-auth' {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
role?: 'ADMIN' | 'USER';
|
||||||
|
adminId?: number;
|
||||||
|
studentId?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JWT {
|
||||||
|
role?: 'ADMIN' | 'USER';
|
||||||
|
adminId?: number;
|
||||||
|
studentId?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'next/server' {
|
||||||
|
interface NextRequest {
|
||||||
|
auth: import('next-auth').Session | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authConfig: NextAuthConfig = {
|
||||||
|
providers: [Google],
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, account, user, profile }) {
|
||||||
|
// Only check DB on first sign in
|
||||||
|
if (account && user && user.email) {
|
||||||
|
const admin = await db.select().from(admins).where(eq(admins.email, user.email)).limit(1);
|
||||||
|
if (admin.length > 0 && admin[0]) {
|
||||||
|
token.role = 'ADMIN';
|
||||||
|
token.adminId = admin[0].id;
|
||||||
|
} else {
|
||||||
|
token.role = 'USER';
|
||||||
|
const student = await db
|
||||||
|
.select()
|
||||||
|
.from(students)
|
||||||
|
.where(eq(students.email, user.email))
|
||||||
|
.limit(1);
|
||||||
|
if (student.length > 0 && student[0]) {
|
||||||
|
token.studentId = student[0].id;
|
||||||
|
} else {
|
||||||
|
const nameParts = user.name?.split(' ') ?? [];
|
||||||
|
const firstName = nameParts[0] || '';
|
||||||
|
const lastName = nameParts.slice(1).join(' ') || '';
|
||||||
|
const newStudent = await db
|
||||||
|
.insert(students)
|
||||||
|
.values({
|
||||||
|
email: user.email,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
profilePicture: user.image,
|
||||||
|
})
|
||||||
|
.returning({ id: students.id });
|
||||||
|
if (newStudent[0]) {
|
||||||
|
token.studentId = newStudent[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
if (token?.role) {
|
||||||
|
session.user.role = token.role as 'ADMIN' | 'USER';
|
||||||
|
}
|
||||||
|
if (token?.adminId) {
|
||||||
|
session.user.adminId = token.adminId as number;
|
||||||
|
}
|
||||||
|
if (token?.studentId) {
|
||||||
|
session.user.studentId = token.studentId as number;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: TypeScript warnings about inferred types are expected with NextAuth v5 beta
|
||||||
|
// These warnings don't affect functionality
|
||||||
|
export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
|
||||||
import { signIn } from '@workspace/auth';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
|
||||||
export default function Login({ logIn }: { logIn: () => Promise<void> }) {
|
export default function Login({ action }: { action: () => Promise<void> }) {
|
||||||
return (
|
return (
|
||||||
<form action={logIn}>
|
<form action={action}>
|
||||||
<Button type="submit" variant="outline" className="w-full h-12">
|
<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="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" />
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { auth } from '@workspace/auth';
|
import { auth } from '@/auth';
|
||||||
|
|
||||||
export default async function Studs({
|
export default async function Studs({
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { auth } from '@workspace/auth';
|
import { auth } from '@/auth';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
|
||||||
export default auth((req: any) => {
|
export default auth((req: NextRequest) => {
|
||||||
// If the user is unauthenticated or a student, allow the request.
|
if (!req.auth) {
|
||||||
if (!req.auth || req.auth.user?.role === 'USER') {
|
return NextResponse.redirect(new URL('/login', req.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.auth.user?.role === 'USER') {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, redirect to the admin app.
|
const adminUrl = process.env.ADMIN_URL ?? 'http://localhost:3001';
|
||||||
const adminURL = process.env.ADMIN_URL ?? 'http://localhost:3001';
|
|
||||||
|
|
||||||
return NextResponse.redirect(new URL(adminURL, req.url));
|
return NextResponse.redirect(new URL(adminUrl, req.url));
|
||||||
}) as any;
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login).*)'],
|
||||||
};
|
};
|
||||||
|
|||||||
20
apps/student/next-auth.d.ts
vendored
20
apps/student/next-auth.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
import 'next-auth';
|
|
||||||
|
|
||||||
declare module 'next-auth' {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'next-auth/jwt' {
|
|
||||||
interface JWT {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: ['@workspace/ui', '@workspace/auth', '@workspace/db'],
|
transpilePackages: ['@workspace/ui', '@workspace/db'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -13,25 +13,26 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@workspace/ui": "workspace:*",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"@workspace/auth": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"framer-motion": "^12.22.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.3.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.59.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.19.4",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@workspace/eslint-config": "workspace:^",
|
"@workspace/eslint-config": "workspace:^",
|
||||||
"@workspace/typescript-config": "workspace:*",
|
"@workspace/typescript-config": "workspace:*",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,18 @@
|
|||||||
"extends": "@workspace/typescript-config/nextjs.json",
|
"extends": "@workspace/typescript-config/nextjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": [
|
||||||
"@workspace/ui/*": ["../../packages/ui/src/*"],
|
"./*"
|
||||||
"@workspace/db/*": ["../../packages/db/src/*"],
|
],
|
||||||
"@workspace/db": ["../../packages/db/src"]
|
"@workspace/ui/*": [
|
||||||
|
"../../packages/ui/src/*"
|
||||||
|
],
|
||||||
|
"@workspace/db": [
|
||||||
|
"../../packages/db/index.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -14,6 +21,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"next-env.d.ts",
|
||||||
|
"next.config.ts",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@workspace/eslint-config": "workspace:*",
|
"@workspace/eslint-config": "workspace:*",
|
||||||
"@workspace/typescript-config": "workspace:*",
|
"@workspace/typescript-config": "workspace:*",
|
||||||
"prettier": "^3.6.0",
|
"prettier": "^3.6.2",
|
||||||
"turbo": "^2.5.4",
|
"turbo": "^2.5.4",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@workspace/auth",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"lint": "eslint . --max-warnings 0"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"packageManager": "pnpm@10.4.1",
|
|
||||||
"dependencies": {
|
|
||||||
"next-auth": "5.0.0-beta.28",
|
|
||||||
"@workspace/db": "workspace:*",
|
|
||||||
"drizzle-orm": "^0.44.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"dotenv": "^16.5.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
packages/auth/types.d.ts
vendored
20
packages/auth/types.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
import 'next-auth';
|
|
||||||
import 'next-auth/jwt';
|
|
||||||
|
|
||||||
declare module 'next-auth' {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
declare module 'next-auth/jwt' {
|
|
||||||
interface JWT {
|
|
||||||
role?: 'ADMIN' | 'USER';
|
|
||||||
adminId?: number;
|
|
||||||
studentId?: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
packages/db/drizzle.ts
Normal file
1
packages/db/drizzle.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from 'drizzle-orm';
|
||||||
@@ -2,4 +2,4 @@ import { drizzle } from 'drizzle-orm/neon-http';
|
|||||||
|
|
||||||
export const db = drizzle(process.env.DATABASE_URL!);
|
export const db = drizzle(process.env.DATABASE_URL!);
|
||||||
|
|
||||||
export * from './schema';
|
export * from './schema.ts';
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./schema": "./schema.ts",
|
||||||
|
"./drizzle": "./drizzle.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
@@ -17,13 +22,14 @@
|
|||||||
"packageManager": "pnpm@10.4.1",
|
"packageManager": "pnpm@10.4.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neondatabase/serverless": "^1.0.1",
|
"@neondatabase/serverless": "^1.0.1",
|
||||||
"drizzle-orm": "^0.44.2"
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"drizzle-zod": "^0.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@workspace/eslint-config": "workspace:*",
|
||||||
|
"@workspace/typescript-config": "workspace:*",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-kit": "^0.31.1",
|
"drizzle-kit": "^0.31.1",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3"
|
||||||
"@workspace/eslint-config": "workspace:*",
|
|
||||||
"@workspace/typescript-config": "workspace:*"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
check,
|
check,
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export { createSelectSchema } from 'drizzle-zod';
|
||||||
|
|
||||||
export const students = pgTable('students', {
|
export const students = pgTable('students', {
|
||||||
id: serial().primaryKey(),
|
id: serial().primaryKey(),
|
||||||
email: text().notNull(),
|
email: text().notNull(),
|
||||||
@@ -262,7 +264,6 @@ export const applicationsRelations = relations(applications, ({ one }) => ({
|
|||||||
references: [resumes.id],
|
references: [resumes.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const admins = pgTable('admins', {
|
export const admins = pgTable('admins', {
|
||||||
id: serial().primaryKey(),
|
id: serial().primaryKey(),
|
||||||
email: text().notNull().unique(),
|
email: text().notNull().unique(),
|
||||||
@@ -272,3 +273,4 @@ export const admins = pgTable('admins', {
|
|||||||
.$onUpdate(() => new Date())
|
.$onUpdate(() => new Date())
|
||||||
.notNull(),
|
.notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@workspace/typescript-config/nextjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
|||||||
168
packages/ui/src/components/navigation-menu.tsx
Normal file
168
packages/ui/src/components/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
}
|
||||||
116
packages/ui/src/components/table.tsx
Normal file
116
packages/ui/src/components/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
4165
pnpm-lock.yaml
generated
4165
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user