diff --git a/apps/admin/app/(main)/actions.ts b/apps/admin/app/(main)/actions.ts new file mode 100644 index 0000000..9ef1e5e --- /dev/null +++ b/apps/admin/app/(main)/actions.ts @@ -0,0 +1,6 @@ +'use server' +import { signOut } from "@/auth"; + +export async function signOutAction() { + await signOut(); +} \ No newline at end of file diff --git a/apps/admin/app/(main)/layout.tsx b/apps/admin/app/(main)/layout.tsx index d318fce..1259cf4 100644 --- a/apps/admin/app/(main)/layout.tsx +++ b/apps/admin/app/(main)/layout.tsx @@ -17,6 +17,7 @@ import { Search, ChevronDown } from 'lucide-react'; +import { signOutAction } from './actions'; const navLinks = [ { @@ -180,6 +181,7 @@ export default function MainLayout({ children }: { children: React.ReactNode }) + + {/* Notifications */} + + + {/* Profile Dropdown */} +
+ + + {/* Profile Dropdown Menu */} + {isProfileDropdownOpen && ( +
+
+

Admin User

+

admin@nextplacement.com

+
+
+ + +
+
+ )} +
+ + {/* Mobile Menu Button */} + + + + + {/* Mobile Navigation Menu */} + {isMobileMenuOpen && ( +
+ +
+ )} + -
{children}
+ + {/* Main Content */} +
+ {children} +
+ + {/* Click outside to close dropdowns */} + {isProfileDropdownOpen && ( +
setIsProfileDropdownOpen(false)} + /> + )}
); } diff --git a/apps/student/app/(main)/page.tsx b/apps/student/app/(main)/page.tsx index cebd26c..d8218d7 100644 --- a/apps/student/app/(main)/page.tsx +++ b/apps/student/app/(main)/page.tsx @@ -1,33 +1,222 @@ -import Login from '@/components/login'; -import Studs from '@/components/studs'; -import { db, admins } from '@workspace/db'; -import { auth, signIn, signOut } from '@/auth'; +import { Card, CardContent, CardHeader, CardTitle } from "@workspace/ui/components/card" +import { Button } from "@workspace/ui/components/button" +import { Badge } from "@workspace/ui/components/badge" +import { Separator } from "@workspace/ui/components/separator" +import Link from "next/link" +import { db, companies } from "@workspace/db" +import { Plus, Building2, Briefcase, MapPin, DollarSign, Calendar, ExternalLink } from "lucide-react" -async function getStudents() { - 'use server'; - const s = await db.select().from(admins); - console.log(s); +async function getDashboardData() { + try { + // Get companies with their jobs + const result = await db.query.companies.findMany({ + with: { + jobs: { + where: (job, {eq}) => eq(job.active, true), // Only include active jobs + } + } + }) + + // Filter to only include companies that have active jobs + const companiesWithActiveJobs = result.filter((company) => company.jobs.length > 0) + + console.log("Companies with active jobs:", companiesWithActiveJobs.length) + return companiesWithActiveJobs + } catch (error) { + console.error("Database query error:", error) + // Fallback to companies only if the relation query fails + const companiesOnly = await db.select().from(companies) + return companiesOnly.map((company) => ({ ...company, jobs: [] })) + } } -async function logIn() { - 'use server'; - await signIn('google'); -} +export default async function DashboardPage() { + const data = await getDashboardData() -async function logOut() { - 'use server'; - await signOut(); -} + // Calculate stats for companies with active jobs only + const totalActiveJobs = data.reduce((acc, company) => acc + company.jobs.filter((job) => job.active).length, 0) -export default async function Page() { - const session = await auth(); return ( -
-
-

Hello student {session?.user?.name}

- {!session?.user && } - +
+
+ {/* Header Section */} +
+
+
+

Companies Dashboard

+

Companies with active job listings

+
+ + + +
+ + {/* Stats Cards */} +
+ + +
+
+

Companies with Active Jobs

+

{data.length}

+
+ +
+
+
+ + + +
+
+

Total Active Jobs

+

{totalActiveJobs}

+
+ +
+
+
+ + + +
+
+

Avg Jobs per Company

+

+ {data.length > 0 ? Math.round((totalActiveJobs / data.length) * 10) / 10 : 0} +

+
+
+
+
+
+
+
+
+
+ + {/* Companies Section */} +
+ {data.length === 0 ? ( + + + +

No companies with active jobs

+

Get started by adding your first job listing

+ + + +
+
+ ) : ( + data.map((company) => ( + + +
+
+
+ +
+
+ {company.name} +

{company.email}

+
+
+
+ + {company.jobs.filter((job) => job.active).length} active job + {company.jobs.filter((job) => job.active).length !== 1 ? "s" : ""} + +
+
+ {company.description && company.description !== "N/A" && ( +

{company.description}

+ )} +
+ + + + +
+

Active Job Listings

+
+ +
+ {company.jobs + .filter((job) => job.active) + .map((job) => ( + + +
+
+

+ {job.title} +

+ + Active + +
+ +
+ {job.location && job.location !== "N/A" && ( +
+ + {job.location} +
+ )} + {job.salary && job.salary !== "N/A" && ( +
+ + {job.salary} +
+ )} +
+ + Deadline: {job.applicationDeadline.toLocaleDateString()} +
+
+ + {job.link && ( + + )} +
+
+
+ ))} +
+
+
+ )) + )} +
- ); + ) } + +export const dynamic = "force-dynamic" diff --git a/apps/student/app/login/page.tsx b/apps/student/app/login/page.tsx index 88cea65..8a822dd 100644 --- a/apps/student/app/login/page.tsx +++ b/apps/student/app/login/page.tsx @@ -8,19 +8,34 @@ async function logIn() { export default async function Page() { return ( -
-
-
-
diff --git a/apps/student/app/signup/action.ts b/apps/student/app/signup/action.ts new file mode 100644 index 0000000..4ca5171 --- /dev/null +++ b/apps/student/app/signup/action.ts @@ -0,0 +1,46 @@ +'use server' +import { db, students } from '@workspace/db'; +import { eq } from '@workspace/db/drizzle'; +import { studentSignupSchema } from './schema'; +import { auth } from '@/auth'; + +export async function signupAction(data: FormData) { + const session = await auth(); + const studentId = session?.user?.studentId; + if (!studentId) { + return { error: 'Student ID not found in session.' }; + } + + const formData = Object.fromEntries(data.entries()); + const parsedData = await studentSignupSchema.safeParseAsync(formData); + + if (!parsedData.success) { + return { error: parsedData.error.issues }; + } + + const student = parsedData.data; + + await db.update(students).set({ + rollNumber: student.rollNumber, + firstName: student.firstName, + middleName: student.middleName, + lastName: student.lastName, + mothersName: student.mothersName, + gender: student.gender, + dob: student.dob, + personalGmail: student.personalGmail, + phoneNumber: student.phoneNumber, + address: student.address, + degree: student.degree, + branch: student.branch, + year: student.year, + skills: student.skills, + linkedin: student.linkedin, + github: student.github, + ssc: String(student.ssc), + hsc: String(student.hsc), + isDiploma: student.isDiploma, + }).where(eq(students.id, studentId)); + +} + diff --git a/apps/student/app/(main)/signup/page.tsx b/apps/student/app/signup/page.tsx similarity index 92% rename from apps/student/app/(main)/signup/page.tsx rename to apps/student/app/signup/page.tsx index 9a86ddc..8ad6594 100644 --- a/apps/student/app/(main)/signup/page.tsx +++ b/apps/student/app/signup/page.tsx @@ -26,39 +26,7 @@ import { SelectValue, } from '@workspace/ui/components/select'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; - -// Form schema -const formSchema = z.object({ - // Personal Details - firstName: z.string().min(1, 'First name is required'), - lastName: z.string().min(1, 'Last name is required'), - fathersName: z.string().optional(), - mothersName: z.string().optional(), - email: z.string().email('Invalid email address'), - rollNumber: z.string().min(1, 'Roll number is required'), - phoneNumber: z.string().min(10, 'Phone number must be at least 10 digits'), - address: z.string().min(1, 'Address is required'), - - // Academic Details - degree: z.string().min(1, 'Degree is required'), - year: z.string().min(1, 'Year is required'), - branch: z.string().min(1, 'Branch is required'), - ssc: z.string().min(1, 'SSC percentage is required'), - hsc: z.string().min(1, 'HSC percentage is required'), - - // Semester Grades - sem1: z.string().min(1, 'Semester 1 grade is required'), - sem1KT: z.string().min(1, 'Semester 1 KT status is required'), - sem2: z.string().min(1, 'Semester 2 grade is required'), - sem2KT: z.string().min(1, 'Semester 2 KT status is required'), - - // Additional Details - linkedin: z.string().url('Invalid LinkedIn URL'), - github: z.string().url('Invalid GitHub URL'), - skills: z.string().optional(), -}); - -type FormData = z.infer; +import { studentSignupSchema, StudentSignup } from './schema'; const steps = [ { @@ -84,8 +52,8 @@ export default function StudentRegistrationForm() { const [currentStep, setCurrentStep] = useState(1); const [isSubmitting, setIsSubmitting] = useState(false); - const form = useForm({ - resolver: zodResolver(formSchema), + const form = useForm({ + resolver: zodResolver(studentSignupSchema), defaultValues: { firstName: '', lastName: '', @@ -625,4 +593,4 @@ function AdditionalDetailsStep({ form }: { form: any }) { />
); -} +} \ No newline at end of file diff --git a/apps/student/app/signup/schema.ts b/apps/student/app/signup/schema.ts new file mode 100644 index 0000000..59d50cf --- /dev/null +++ b/apps/student/app/signup/schema.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +export const sgpiSchema = z.object({ + sem: z.number().min(1).max(8), + sgpi: z.number().min(0).max(10), + kt: z.boolean(), + ktDead: z.boolean(), +}); + +export const internshipSchema = z.object({ + title: z.string(), + company: z.string(), + description: z.string(), + location: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), +}); + +export const resumeSchema = z.object({ + title: z.string(), + link: z.string().url(), +}); + +export const studentSignupSchema = z.object({ + rollNumber: z.string().max(12), + firstName: z.string().max(255), + middleName: z.string().max(255), + lastName: z.string().max(255), + mothersName: z.string().max(255), + gender: z.string().max(10), + dob: z.coerce.date(), + personalGmail: z.string().email(), + phoneNumber: z.string().max(10), + address: z.string(), + degree: z.string(), + branch: z.string(), + year: z.string(), + skills: z.array(z.string()), + linkedin: z.string(), + github: z.string(), + ssc: z.coerce.number(), + hsc: z.coerce.number(), + isDiploma: z.boolean(), + sgpi: z.array(sgpiSchema), + internships: z.array(internshipSchema).optional(), + resume: z.array(resumeSchema).optional(), +}); + +export type StudentSignup = z.infer; +export type Internship = z.infer; +export type Resume = z.infer; +export type SGPI = z.infer; \ No newline at end of file diff --git a/apps/student/auth.ts b/apps/student/auth.ts index 83e44c2..c020c61 100644 --- a/apps/student/auth.ts +++ b/apps/student/auth.ts @@ -10,6 +10,7 @@ declare module 'next-auth' { role?: 'ADMIN' | 'USER'; adminId?: number; studentId?: number; + completedProfile?: boolean; [key: string]: any; } & DefaultSession["user"]; } @@ -18,6 +19,7 @@ declare module 'next-auth' { role?: 'ADMIN' | 'USER'; adminId?: number; studentId?: number; + completedProfile?: boolean; } } @@ -30,13 +32,14 @@ declare module 'next/server' { const authConfig: NextAuthConfig = { providers: [Google], callbacks: { - async jwt({ token, account, user, profile }) { + async jwt({ token, account, user }) { // 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; + token.completedProfile = true; } else { token.role = 'USER'; const student = await db @@ -46,6 +49,7 @@ const authConfig: NextAuthConfig = { .limit(1); if (student.length > 0 && student[0]) { token.studentId = student[0].id; + token.completedProfile = student[0].rollNumber ? true : false; } else { const nameParts = user.name?.split(' ') ?? []; const firstName = nameParts[0] || ''; @@ -61,6 +65,7 @@ const authConfig: NextAuthConfig = { .returning({ id: students.id }); if (newStudent[0]) { token.studentId = newStudent[0].id; + token.completedProfile = false; } } } @@ -77,6 +82,9 @@ const authConfig: NextAuthConfig = { if (token?.studentId) { session.user.studentId = token.studentId as number; } + if (token?.completedProfile !== undefined) { + session.user.completedProfile = token.completedProfile as boolean; + } return session; }, }, diff --git a/apps/student/middleware.ts b/apps/student/middleware.ts index 9b5d8b5..836e69c 100644 --- a/apps/student/middleware.ts +++ b/apps/student/middleware.ts @@ -7,6 +7,11 @@ export default auth((req: NextRequest) => { } if (req.auth.user?.role === 'USER') { + if (!req.auth.user?.completedProfile && !req.nextUrl.pathname.startsWith('/signup')) { + const signupUrl = process.env.STUDENT_PROFILE_URL ?? 'http://localhost:3000/signup'; + return NextResponse.redirect(new URL(signupUrl, req.url)); + } + return NextResponse.next(); }