finally building

This commit is contained in:
Om Lanke
2025-07-02 12:05:38 +05:30
parent ba6ee585dc
commit 449629ece2
40 changed files with 2253 additions and 3711 deletions

View 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>
);
}

View File

@@ -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';

View 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',
},
];

View 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>
);
}

View 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>
);
}

View File

@@ -1,2 +1,2 @@
import { handlers } from '@workspace/auth'; import { handlers } from '@/auth';
export const { GET, POST } = handlers; export const { GET, POST } = handlers;

View 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>
);
}

View File

@@ -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;

View File

@@ -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> }) {

View File

@@ -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,

View File

@@ -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).*)'],
}; };

View File

@@ -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;
}
}

View File

@@ -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;

View File

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

View File

@@ -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"
]
} }

View 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>
);
}

View File

@@ -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>

View File

@@ -1,2 +1,2 @@
import { handlers } from '@workspace/auth'; import { handlers } from '@/auth';
export const { GET, POST } = handlers; export const { GET, POST } = handlers;

View 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
View 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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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).*)'],
}; };

View File

@@ -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;
}
}

View File

@@ -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;

View File

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

View File

@@ -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"
]
} }

View File

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

View File

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

View File

@@ -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
View File

@@ -0,0 +1 @@
export * from 'drizzle-orm';

View File

@@ -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';

View File

@@ -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:*"
} }
} }

View File

@@ -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(),
}); });

View File

@@ -0,0 +1,12 @@
{
"extends": "@workspace/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"allowImportingTsExtensions": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

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

View 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,
}

View 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,
}

5023
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff