diff --git a/README.md b/README.md index d5aa256c..a22efee5 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,4 @@ npm run dev ```bash npm run test ``` + diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 00000000..f0335080 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { TriangleAlert } from "lucide-react"; +import { useEffect } from "react"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service like Sentry + console.error(error); + }, [error]); + + return ( +
+ + Error +
+ + Something went wrong. +
+ + Sorry for the inconvenience. Please{" "} + + contact us for support + + . + + {error && ( +
+ + Error: {error.message} + +
+ )} +
+ +
+
+
+ ); +} diff --git a/app/hire/authctx.tsx b/app/hire/authctx.tsx index 073e9301..3c17054f 100644 --- a/app/hire/authctx.tsx +++ b/app/hire/authctx.tsx @@ -7,7 +7,6 @@ import { EmployerAuthService } from "@/lib/api/hire.api"; import { getFullName } from "@/lib/profile"; import { FetchResponse } from "@/lib/api/use-fetch"; import { useQueryClient } from "@tanstack/react-query"; -import { usePocketbase } from "@/lib/pocketbase"; import { EmployerService } from "@/lib/api/services"; import { useRef } from "react"; @@ -30,7 +29,7 @@ interface IAuthContext { ) => Promise<{ existing_user: boolean; verified_user: boolean }>; logout: () => Promise; isAuthenticated: () => boolean; - refreshAuthentication: () => void; + refreshAuthentication: () => Promise | null>; redirectIfNotLoggedIn: () => void; redirectIfLoggedIn: () => void; } @@ -54,12 +53,11 @@ export const AuthContextProvider = ({ const [loading, setLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); const queryClient = useQueryClient(); - const pocketbase = usePocketbase(); const [god, setGod] = useState(false); const [user, setUser] = useState | null>(() => { if (typeof window === "undefined") return null; const user = sessionStorage.getItem("user"); - return user ? JSON.parse(user) : null; + return user ? (JSON.parse(user) as PublicEmployerUser) : null; }); // Whenever user is updated, cache in localStorage @@ -87,7 +85,8 @@ export const AuthContextProvider = ({ setUser(response.user as PublicEmployerUser); - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error if (response.god) setGod(true); setIsAuthenticated(true); @@ -112,6 +111,7 @@ export const AuthContextProvider = ({ setUser(response.user as PublicEmployerUser); setIsAuthenticated(true); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error if (response.god) setGod(true); @@ -125,7 +125,6 @@ export const AuthContextProvider = ({ return null; } - await pocketbase.refresh(); await queryClient.invalidateQueries({ queryKey: ["my-employer-profile"] }); setProxy(getFullName(response.user)); setUser(response.user); @@ -138,7 +137,6 @@ export const AuthContextProvider = ({ }; const logout = async () => { - await pocketbase.logout(); await EmployerAuthService.logout(); await queryClient.invalidateQueries({ queryKey: ["my-employer-profile"] }); @@ -162,17 +160,17 @@ export const AuthContextProvider = ({ useEffect(() => { if (effectRan.current && !loading && isAuthenticated) { - router.push("/dashboard") - }; + router.push("/dashboard"); + } if (!loading) { effectRan.current = true; } }, [isAuthenticated, loading]); - } + }; useEffect(() => { - refreshAuthentication(); + void refreshAuthentication(); }, []); return ( @@ -181,9 +179,11 @@ export const AuthContextProvider = ({ user, god, proxy, - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error register, - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error login, loginAs, loading, diff --git a/app/hire/conversations/page.tsx b/app/hire/conversations/page.tsx deleted file mode 100644 index 42a29ad7..00000000 --- a/app/hire/conversations/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { ConversationPage } from "@/components/features/hire/chat/ConversationPage"; -import { useDbRefs } from "@/lib/db/use-refs"; -import { useSearchParams } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import ContentLayout from "@/components/features/hire/content-layout"; - -function ConversationsPage() { - const searchParams = useSearchParams(); - const conversationId = searchParams.get("conversationId") - const router = useRouter(); - - return( - -
- -
-
- ) -}; - -export default function Conversations() { - return( - - - - ); -} diff --git a/app/hire/dashboard/applicant/page.tsx b/app/hire/dashboard/applicant/page.tsx index 58f16505..f34531af 100644 --- a/app/hire/dashboard/applicant/page.tsx +++ b/app/hire/dashboard/applicant/page.tsx @@ -6,7 +6,6 @@ import { ApplicantPage } from "@/components/features/hire/dashboard/ApplicantPag import { type ActionItem } from "@/components/ui/action-item"; import { useEmployerApplications } from "@/hooks/use-employer-api"; import { UserService } from "@/lib/api/services"; -import { EmployerApplication } from "@/lib/db/db.types"; import { useDbRefs } from "@/lib/db/use-refs"; import { useSearchParams } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; @@ -14,76 +13,23 @@ import useApplicationActions from "@/hooks/use-application-actions"; function ApplicantPageContent() { const searchParams = useSearchParams(); - const userId = searchParams.get("userId"); - const isDummyProfile = searchParams.get("dummy") === "1"; + const applicationId = searchParams.get("applicationId"); const [loading, setLoading] = useState(true); const applications = useEmployerApplications(); const { app_statuses } = useDbRefs(); const { triggerAction } = useApplicationActions(applications.review); - const dummyApplication: EmployerApplication = { - id: "dummy-super-application", - job_id: jobId ?? "dummy-super-job", - status: 0, - applied_at: "2026-03-09T00:00:00.000Z", - cover_letter: - "I am excited to apply and contribute with thoughtful solutions.", - challenge_submission: - "This is a sample challenge submission used for previewing super listing flows.", - job: { - title: "Super Listing - Sample Role", - internship_preferences: { - require_cover_letter: true, - }, - }, - user: { - id: "dummy-super-user", - first_name: "Sample", - last_name: "Applicant", - phone_number: "+63 900 000 0000", - edu_verification_email: "sample.applicant@school.edu", - degree: "BS Computer Science", - bio: "I build practical products and enjoy solving product and UX problems.", - github_link: "https://github.com/sample-applicant", - portfolio_link: "https://sample-applicant.dev", - linkedin_link: "https://linkedin.com/in/sample-applicant", - internship_preferences: { - internship_type: "credited", - expected_start_date: 1751328000000, - expected_duration_hours: 400, - }, - expected_graduation_date: 1767225600000, - }, - }; - - let userApplication = applications?.employer_applications.find( - (a) => userId === a.user_id, + const userApplication = applications?.employer_applications.find( + (a) => applicationId === a.id, ); - let otherApplications = applications?.employer_applications.filter( - (a) => userId === a.user_id, + const otherApplications = applications?.employer_applications.filter( + (a) => a.user_id === userApplication?.user_id, ); - - if (jobId) { - userApplication = applications?.employer_applications.find( - (a) => userId === a.user_id && a.job_id === jobId, - ); - otherApplications = applications?.employer_applications.filter( - (a) => userId === a.user_id && a.id !== userApplication?.id, - ); - } - - if (isDummyProfile) { - userApplication = dummyApplication; - otherApplications = []; - } + const userId = userApplication?.user_id; useEffect(() => { const fetchUserData = async () => { - if (isDummyProfile) { - setLoading(false); - return; - } if (!userId) { setLoading(false); return; @@ -97,12 +43,14 @@ function ApplicantPageContent() { setLoading(false); } }; - fetchUserData(); - }, [isDummyProfile, userId]); + void fetchUserData(); + }, [userId]); if (!app_statuses) return null; - const unique_app_statuses = app_statuses.reduce( + const unique_app_statuses = ( + app_statuses as { id: number; name: string }[] + ).reduce( (acc: { id: number; name: string }[], cur: { id: number; name: string }) => acc.find((a) => a.name === cur.name) ? acc : [...acc, cur], [], @@ -135,10 +83,7 @@ function ApplicantPageContent() {
{ if (!userApplication) return; diff --git a/app/hire/god/students/page.tsx b/app/hire/god/students/page.tsx index a9dea493..1c163b26 100644 --- a/app/hire/god/students/page.tsx +++ b/app/hire/god/students/page.tsx @@ -1,7 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; +import { useMemo, useState } from "react"; import { Autocomplete } from "@/components/ui/autocomplete"; import { Button } from "@/components/ui/button"; import { @@ -9,8 +8,6 @@ import { RowCard, Meta, LastLogin, - useLocalTagMap, - EditableTags, ListSummary, } from "@/components/features/hire/god/ui"; import { BoolBadge, Badge } from "@/components/ui/badge"; @@ -24,12 +21,9 @@ import { import { useDbRefs } from "@/lib/db/use-refs"; import { useModal } from "@/hooks/use-modal"; import { ApplicantModalContent } from "@/components/shared/applicant-modal"; -import { PDFPreview } from "@/components/shared/pdf-preview"; import { UserService } from "@/lib/api/services"; import { useModalRegistry } from "@/components/modals/modal-registry"; -import { useFile } from "@/hooks/use-file"; import { - FileText, Filter as FilterIcon, ChevronDown, Check, @@ -37,24 +31,15 @@ import { CheckSquare, Square, } from "lucide-react"; -import { Separator } from "@/components/ui/separator"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; -import { - Command, - CommandList, - CommandItem, - CommandGroup, - CommandInput, - CommandEmpty, -} from "@/components/ui/command"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { useQueryClient } from "@tanstack/react-query"; -import { useJobsData } from "@/lib/api/student.data.api"; +import { Employer, EmployerApplication, PublicUser } from "@/lib/db/db.types"; const STUDENT_ORIGIN = process.env.NEXT_PUBLIC_CLIENT_URL || "http://localhost:3000"; @@ -73,30 +58,6 @@ const RowCheck = ({ visible }: { visible: boolean }) => ( ); -const Chip = ({ - children, - active = false, - onClick, -}: { - children: React.ReactNode; - active?: boolean; - onClick?: () => void; -}) => ( - -); - -/* ---------- Page ---------- */ - export default function StudentsPage() { const { users, isFetching } = useUsers(); const queryClient = useQueryClient(); @@ -104,10 +65,6 @@ export default function StudentsPage() { const refs = useDbRefs(); const modalRegistry = useModalRegistry(); - // local tags (by user.id) - const { tagMap, addTag, removeTag, allTags } = - useLocalTagMap("god-tags:students"); - // Mass apply selection state const [selectedStudentIds, setSelectedStudentIds] = useState>( new Set(), @@ -134,33 +91,12 @@ export default function StudentsPage() { const [hideNoApps, setHideNoApps] = useState(false); // selection / modals - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); const { open: openApplicantModal, close: closeApplicantModal, Modal: ApplicantModal, } = useModal("applicant-modal"); - const { - open: openResumeModal, - close: closeResumeModal, - Modal: ResumeModal, - } = useModal("resume-modal"); - - // Signed resume URL for the currently selected user - const { url: resumeURL, sync: syncResumeURL } = useFile({ - fetcher: useCallback( - async () => await UserService.getUserResumeURL(selectedUser?.id ?? ""), - [selectedUser], - ), - route: selectedUser ? `/users/${selectedUser?.id}/resume` : "", - }); - - // Prefetch signed URL whenever a new student is selected (simple + effective) - useEffect(() => { - if (selectedUser?.id) { - syncResumeURL(); - } - }, [selectedUser?.id, syncResumeURL]); // Student impersonation const { impersonate } = useStudentImpersonation(); @@ -190,7 +126,8 @@ export default function StudentsPage() { const toggleStudentSelection = (studentId: string) => { setSelectedStudentIds((prev) => { const next = new Set(prev); - next.has(studentId) ? next.delete(studentId) : next.add(studentId); + if (next.has(studentId)) next.delete(studentId); + else next.add(studentId); return next; }); }; @@ -199,55 +136,52 @@ export default function StudentsPage() { const selectAllFiltered = () => { const next = new Set(selectedStudentIds); - filtered.forEach((u: any) => u.id && next.add(u.id)); + filtered.forEach((u: PublicUser) => u.id && next.add(u.id)); setSelectedStudentIds(next); }; const unselectAllFiltered = () => { const next = new Set(selectedStudentIds); - filtered.forEach((u: any) => u.id && next.delete(u.id)); + filtered.forEach((u: PublicUser) => u.id && next.delete(u.id)); setSelectedStudentIds(next); }; const applications = useMemo(() => { - const apps: any[] = []; - employers.data.forEach((e: any) => - e?.applications?.map((a: any) => apps.push(a)), + const apps: EmployerApplication[] = []; + const employersWithApplications = employers.data as (Employer & { + applications: EmployerApplication[]; + })[]; + + employersWithApplications?.forEach( + (e: Employer & { applications: EmployerApplication[] }) => + e?.applications?.map((a: EmployerApplication) => apps.push(a)), ); return apps; }, [employers.data]); - /* ---------- Options for dropdowns ---------- */ - - const positionOptions = useMemo( - () => refs.job_categories.map((c) => ({ id: c.id, label: c.name })), - [refs.job_categories], - ); - /* ---------- Filter helpers ---------- */ - - const matchesCollege = (u: any) => { + const matchesCollege = (u: PublicUser) => { if (activeColleges.length === 0) return true; return activeColleges.includes(u.college); }; - const matchesApplyForMe = (u: any) => { + const matchesApplyForMe = (u: PublicUser) => { if (filterApplyForMe === null) return true; return u.apply_for_me === filterApplyForMe; }; - const matchesWorkModes = (u: any) => { + const matchesWorkModes = (u: PublicUser) => { if (activeWorkModes.length === 0) return true; const modes = u.internship_preferences?.job_setup_ids ?? []; - return activeWorkModes.some((mode) => modes.includes(String(mode) as any)); + return activeWorkModes.some((mode) => modes.includes(String(mode))); }; - const matchesWorkTypes = (u: any) => { + const matchesWorkTypes = (u: PublicUser) => { if (activeWorkTypes.length === 0) return true; const types = u.internship_preferences?.job_commitment_ids ?? []; - return activeWorkTypes.some((type) => types.includes(String(type) as any)); + return activeWorkTypes.some((type) => types.includes(String(type))); }; - const matchesCreditedInternship = (u: any) => { + const matchesCreditedInternship = (u: PublicUser) => { if (filterCreditedInternship === null) return true; const isCredited = u.internship_preferences?.internship_type === "credited"; return isCredited === filterCreditedInternship; @@ -256,12 +190,12 @@ export default function StudentsPage() { /* ---------- Main filtered list ---------- */ const filtered = users .filter( - (u: any) => + (u: PublicUser) => !hideNoApps || applications.some((a) => a.user_id === u.id) || u.id === search, ) - .filter((u: any) => + .filter((u) => `${getFullName(u)} ${u.email} ${refs.to_college_name(u.college)} ${ u.degree }` @@ -274,55 +208,21 @@ export default function StudentsPage() { .filter(matchesWorkTypes) .filter(matchesCreditedInternship) .toSorted( - (a: any, b: any) => + (a, b) => new Date(b.created_at ?? "").getTime() - new Date(a.created_at ?? "").getTime(), ); - /* ---------- ID(s) → text helpers ---------- */ - - const toSepText = ( - idsOrId: ID[] | ID | null | undefined, - toName: (id: ID) => string | null, - sep = ", ", - ) => { - const ids: ID[] = Array.isArray(idsOrId) - ? idsOrId - : idsOrId != null - ? [idsOrId] - : []; - return ids - .map((x) => toName(x) ?? "") - .filter(Boolean) - .join(sep); - }; - /* ---------- Rows ---------- */ - - const rows = filtered.map((u: any, index: number) => { + const rows = filtered.map((u, index) => { const userApplications = applications.filter((a) => a.user_id === u.id); const isRowPending = impersonate.isPending && impUserId === u.id; const isSelected = selectedStudentIds.has(u.id); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const lastTs = u?.last_session?.timestamp - ? new Date(u.last_session.timestamp).getTime() + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + new Date(u.last_session.timestamp).getTime() : undefined; - const rowTags = tagMap[u.id] ?? []; - - const modeName = (id: number) => refs.to_job_mode_name(id); - const typeName = (id: number) => refs.to_job_type_name(id); - const categoryName = (id: string) => refs.to_job_category_name(id, ""); - - const modeTxt = toSepText( - u.job_mode_ids ?? u.job_mode, - modeName, - " · ", - ); - const typeTxt = toSepText( - u.job_type_ids ?? u.job_type, - typeName, - " · ", - ); - const posTxt = toSepText(u.job_category_ids, categoryName, " · "); return (
{(() => { - const ids = (u.internship_preferences?.job_category_ids ?? - []) as string[]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const ids: string[] = + u.internship_preferences?.job_category_ids ?? []; const items = ids .map((id) => { const c = refs.job_categories.find((x) => x.id === id); @@ -494,7 +395,7 @@ export default function StudentsPage() { disabled={isRowPending} onClick={(ev) => { ev.stopPropagation(); - viewAsStudent(u.id); + void viewAsStudent(u.id); }} > {isRowPending ? "Starting..." : "View"} @@ -530,10 +431,6 @@ export default function StudentsPage() { openApplicantModal(); } }} - className={cn( - "transition-colors", - massApplyMode && isSelected && "bg-blue-50 border-blue-200", - )} /> ); }); @@ -551,11 +448,13 @@ export default function StudentsPage() { // Get unique colleges for filter const collegeOptions = useMemo(() => { const colleges = new Set(); - users.forEach((u: any) => { + users.forEach((u: PublicUser) => { if (u.college) colleges.add(u.college); }); - return Array.from(colleges).sort((a, b) => - refs.to_college_name(a).localeCompare(refs.to_college_name(b)), + return Array.from(colleges).sort( + (a, b) => + refs.to_college_name(a)?.localeCompare(refs.to_college_name(b) ?? "") ?? + 0, ); }, [users, refs]); @@ -577,7 +476,7 @@ export default function StudentsPage() { ({ + options={users.map((u) => ({ id: u.id, name: getFullName(u) ?? "", }))} @@ -628,7 +527,6 @@ export default function StudentsPage() { setActiveColleges([]); setActiveWorkModes([]); setActiveWorkTypes([]); - setActivePositions([]); }} > Reset all @@ -862,7 +760,6 @@ export default function StudentsPage() { setActiveColleges([]); setActiveWorkModes([]); setActiveWorkTypes([]); - setActivePositions([]); }} > @@ -950,42 +847,16 @@ export default function StudentsPage() { } pfp_route={`/users/${selectedUser?.id}/pic`} applicant={selectedUser ?? undefined} - resume_url={resumeURL} - open_calendar={async () => { + open_calendar={() => { closeApplicantModal(); window?.open(selectedUser?.calendar_link ?? "", "_blank")?.focus(); }} - open_resume={async () => { + open_resume={() => { closeApplicantModal(); - await syncResumeURL(); - openResumeModal(); }} job={{}} /> - - - {resumeURL ? ( -
-

- {getFullName(selectedUser)} - Resume -

- -
- ) : ( -
-
- -

- No Resume Available -

-
- This applicant has not uploaded a resume yet. -
-
-
- )} -
); } diff --git a/app/hire/layout.tsx b/app/hire/layout.tsx index 0f445d62..bd929c1c 100644 --- a/app/hire/layout.tsx +++ b/app/hire/layout.tsx @@ -5,14 +5,13 @@ import { RefsContextProvider } from "@/lib/db/use-refs"; import { AppContextProvider } from "@/lib/ctx-app"; import { TooltipProvider } from "@/components/ui/tooltip"; import { BIMoaContextProvider } from "@/lib/db/use-bi-moa"; +import { getRefsData } from "@/lib/db/use-refs-backend"; +import { getBiMoaData } from "@/lib/db/use-bi-moa-backend"; import { PostHogProvider } from "../posthog-provider"; import TanstackProvider from "../tanstack-provider"; import Head from "next/head"; import AllowLanding from "./allowLanding"; -import { ConversationsContextProvider } from "@/hooks/use-conversation"; -import { PocketbaseProvider, usePocketbase } from "@/lib/pocketbase"; import { ModalProvider } from "@/components/providers/modal-provider/ModalProvider"; -import { NotificationListener } from "./notification-listener"; import { SonnerToaster } from "@/components/ui/sonner-toast"; const baseUrl = @@ -55,14 +54,19 @@ export const metadata: Metadata = { * * @component */ -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const [refsData, biMoaData] = await Promise.all([ + getRefsData(), + getBiMoaData(), + ]); + return ( - - + + {children} @@ -85,37 +89,32 @@ const HTMLContent = ({ return ( - - - - - - - - - - - - -
-
- - {children} -
-
-
-
- - - -
-
-
-
-
+ + + + + + + + + + +
+
+ {children} +
+
+
+
+ + + +
+
+
); }; diff --git a/app/hire/notification-listener.tsx b/app/hire/notification-listener.tsx deleted file mode 100644 index a6acf554..00000000 --- a/app/hire/notification-listener.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; -import { useEffect } from "react"; -import { usePocketbase } from "@/lib/pocketbase"; -import { sendNotification, checkNotificationSupport } from "@/lib/notification-service"; - -export function NotificationListener() { - const { user, pb } = usePocketbase(); - useEffect(() => { - if (!user?.id) return; - - pb.collection("conversations").subscribe("*", (e) => { - const convoId = e.record.id; - const newMessages = e.record.contents; - const lastMessage = newMessages[newMessages.length - 1]; - - if (lastMessage?.sender_id !== user.id) { - if (checkNotificationSupport()) { - const n = sendNotification(`New message - BetterInternship`, { - body: lastMessage.message.substring(0, 100), - tag: lastMessage.sender_id, - }); - - if (n) { - n.onclick = () => { - window.focus(); - const target = `/conversations?userId=${lastMessage.sender_id}`; - - if (window.location.pathname + window.location.search !== target) { - window.location.href = target; - } - } - } - } - } - }, { expand: "subscribers" }); - - return () => { - pb.collection("conversations").unsubscribe("*") - }; - }, [user?.id, pb]); - - return null; -} \ No newline at end of file diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 00000000..5adcbcc6 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useEffect } from "react"; + +export default function NotFound() { + useEffect(() => { + const previousHtmlOverflow = document.documentElement.style.overflow; + const previousBodyOverflow = document.body.style.overflow; + + document.documentElement.style.overflow = "hidden"; + document.body.style.overflow = "hidden"; + + return () => { + document.documentElement.style.overflow = previousHtmlOverflow; + document.body.style.overflow = previousBodyOverflow; + }; + }, []); + + return ( +
+ Page not found +
+ ); +} diff --git a/app/student/%5F%5F/page.tsx b/app/student/%5F%5F/page.tsx index 9a9523b9..837d0e62 100644 --- a/app/student/%5F%5F/page.tsx +++ b/app/student/%5F%5F/page.tsx @@ -19,6 +19,7 @@ const InternalSetupPage = () => { await queryClient.invalidateQueries({ queryKey: ["my-forms"] }); await queryClient.invalidateQueries({ queryKey: ["my-form-templates"] }); await queryClient.invalidateQueries({ queryKey: ["my-form-template"] }); + await queryClient.invalidateQueries({ queryKey: ["my-resumes"] }); router.push("/search"); })(); }); diff --git a/app/student/__/page.tsx b/app/student/__/page.tsx index 9a9523b9..837d0e62 100644 --- a/app/student/__/page.tsx +++ b/app/student/__/page.tsx @@ -19,6 +19,7 @@ const InternalSetupPage = () => { await queryClient.invalidateQueries({ queryKey: ["my-forms"] }); await queryClient.invalidateQueries({ queryKey: ["my-form-templates"] }); await queryClient.invalidateQueries({ queryKey: ["my-form-template"] }); + await queryClient.invalidateQueries({ queryKey: ["my-resumes"] }); router.push("/search"); })(); }); diff --git a/app/student/applications/page.tsx b/app/student/applications/page.tsx index d1bdb613..294c2c0f 100644 --- a/app/student/applications/page.tsx +++ b/app/student/applications/page.tsx @@ -3,7 +3,8 @@ // React imports import React from "react"; import Link from "next/link"; -import { ArrowUpRight, BookA } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { ArrowUpRight, BookA, Eye } from "lucide-react"; // UI components import { Button } from "@/components/ui/button"; @@ -22,10 +23,21 @@ import { HeaderText, HeaderIcon } from "@/components/ui/text"; import { Separator } from "@/components/ui/separator"; import { PageError } from "@/components/ui/error"; import { cn } from "@/lib/utils"; +import { useFile } from "@/hooks/use-file"; +import { UserService } from "@/lib/api/services"; +import { useModal } from "@/hooks/use-modal"; +import { PDFPreview } from "@/components/shared/pdf-preview"; export default function ApplicationsPage() { const { redirectIfNotLoggedIn } = useAuthContext(); const rawApplications = useApplicationsData(); + const { url: resumeURL, sync: syncResumeURL } = useFile({ + fetcher: (resumeId: string) => UserService.getMyResumeURL(resumeId), + route: (resumeId: string) => `/users/me/resume/${resumeId}`, + }); + const { open: openResumeModal, Modal: ResumeModal } = useModal( + "application-resume-modal", + ); const applications = React.useMemo( () => ({ @@ -40,6 +52,11 @@ export default function ApplicationsPage() { ); redirectIfNotLoggedIn(); + const openResumePreview = async (resumeId: string) => { + await syncResumeURL(resumeId); + openResumeModal(); + }; + return (
@@ -87,16 +104,31 @@ export default function ApplicationsPage() { ) : (
{applications?.data.map((application) => ( - + ))}
)} + + + +
); } -const ApplicationCard = ({ application }: { application: UserApplication }) => { +const ApplicationCard = ({ + application, + onPreviewResume, +}: { + application: UserApplication; + onPreviewResume: (resumeId: string) => void | Promise; +}) => { + const router = useRouter(); const { to_app_status_name } = useDbRefs(); const job = application.job ?? application.jobs; const jobRecord = job as Record | undefined; @@ -115,13 +147,29 @@ const ApplicationCard = ({ application }: { application: UserApplication }) => { else if (statusLabel === "Accepted" || statusLabel === "Hired") statusBadgeType = "supportive"; else statusBadgeType = "warning"; + const resumeId = application.resume_id; + const listingHref = `/search/${job?.id}`; + const openListing = () => { + if (canOpenListing) router.push(listingHref); + }; - const cardContent = ( + return ( { + if (!canOpenListing) return; + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + openListing(); + } + }} className={cn( + "rounded-[0.16em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40", canOpenListing && !isSuperListing && - "cursor-pointer transition-colors hover:bg-primary/5 hover:border-primary/20", + "cursor-pointer transition-colors hover:bg-gray-50 hover:border-primary/20", canOpenListing && isSuperListing && "cursor-pointer hover:ring-1 hover:ring-primary/20", @@ -129,10 +177,13 @@ const ApplicationCard = ({ application }: { application: UserApplication }) => { "super-card relative isolate overflow-hidden bg-[radial-gradient(ellipse_at_top_left,rgba(254,240,138,0.5),transparent_40%),radial-gradient(ellipse_at_bottom_right,rgba(251,146,60,0.18),transparent_35%),linear-gradient(150deg,rgba(255,251,235,1)_0%,rgba(255,255,255,1)_45%,rgba(254,243,199,0.98)_100%)]", )} > -
+
{statusLabel} + + Applied {formatTimeAgo(application.applied_at ?? "")} + {isSuperListing && }
{canOpenListing ? ( @@ -146,23 +197,24 @@ const ApplicationCard = ({ application }: { application: UserApplication }) => { )}
-
- - Applied {formatTimeAgo(application.applied_at ?? "")} - +
+
); - - if (!canOpenListing) return cardContent; - - return ( - - {cardContent} - - ); }; diff --git a/app/student/companies/brain/page.tsx b/app/student/companies/brain/page.tsx new file mode 100644 index 00000000..26f1e1ec --- /dev/null +++ b/app/student/companies/brain/page.tsx @@ -0,0 +1,661 @@ +"use client"; + +import { + ArrowRight, + Bot, + Brain as BrainIcon, + CalendarDays, + Check, + CirclePlay, + Database, + File, + FileSearch, + Folder, + Github, + Home, + Inbox, + ListChecks, + Mail, + MessageCircle, + Network, + PanelsTopLeft, + Search, + Send, + Slack, + Sparkles, + Table2, + Workflow, +} from "lucide-react"; + +const updates = [ + ["Q2 Planning", "Google Docs", "Document", "2m ago"], + ["Product Roadmap", "Notion", "Page", "15m ago"], + ["Customer Research", "Google Sheets", "Database", "1h ago"], + ["Design System", "Figma", "File", "3h ago"], + ["Interview Notes", "Notion", "Page", "5h ago"], + ["Usage Metrics", "Google Sheets", "Database", "1d ago"], +]; + +const sidebarItems = [ + ["Home", Home], + ["Pages", File], + ["Databases", Database], + ["Files", Folder], + ["Agents", Bot], + ["Tasks", ListChecks], + ["Connections", Network], +]; + +const agents = [ + ["Research Agent", "Finds what you need", Search], + ["Data Analyst", "Answers with your data", Table2], + ["Write Assistant", "Drafts and improves", File], + ["Summarizer", "Condenses and explains", PanelsTopLeft], + ["Task Builder", "Turns ideas into tasks", ListChecks], + ["File Finder", "Searches across everything", FileSearch], +]; + +const integrations = [ + ["Slack", Slack, "text-[#36C5F0]"], + ["Gmail", Mail, "text-[#D93025]"], + ["Google Drive", Folder, "text-[#1A73E8]"], + ["Notion", File, "text-[#111111]"], + ["Figma", Workflow, "text-[#A259FF]"], + ["Asana", Check, "text-[#F06A6A]"], + ["HubSpot", Network, "text-[#FF7A59]"], + ["GitHub", Github, "text-[#111111]"], + ["Dropbox", Folder, "text-[#0061FF]"], +]; + +const primitiveCards = [ + { + title: "Docs", + body: "Bring your docs, wikis, and notes into one place.", + icon: File, + className: "bg-white", + art: "stacked pages", + }, + { + title: "Databases", + body: "Connect and organize structured data.", + icon: Database, + className: "bg-[#FBFBF5]", + art: "soft grid", + }, + { + title: "Files", + body: "Find and organize every file.", + icon: Folder, + className: "bg-white", + art: "folder cluster", + }, + { + title: "Agents", + body: "Let AI agents search, update, and act for you.", + icon: Bot, + className: "bg-[#F7FAF1]", + art: "node map", + }, +]; + +const timeline = [ + ["Capture", "Bring docs, files, data, and chats into Brain."], + ["Connect", "Brain understands and keeps things in sync."], + ["Act", "Brain and agents take action across your context."], + ["Move forward", "Spend less time maintaining systems."], +]; + +function Logo({ className = "" }: { className?: string }) { + return ( +
+ + Brain +
+ ); +} + +function DottedCurve({ flip = false }: { flip?: boolean }) { + return ( + + ); +} + +function HeroDoodles() { + return ( + <> +
+ + +
+
+
+ + {[["left-0 top-10"], ["right-0 top-10"], ["left-10 top-0"], ["left-10 bottom-0"]].map( + ([pos]) => ( + + ), + )} +
+ +
+ + ); +} + +function ProductMockup() { + return ( +
+
+
+ + +
+
+ {["Page", "Overview", "Files", "Activity"].map((tab, index) => ( + + {tab} + + ))} +
+
+
+
+

+ Recent updates +

+ + Live + +
+

+ A snapshot of your work — everything you've added, + created, or connected across your workspace. +

+
+
+
+ {["All updates", "All sources", "All workspaces"].map((filter) => ( + + ))} +
+
+
+ Item + Source + Type + Updated +
+ {updates.map(([item, source, type, time]) => ( +
+ {item} + {source} + {type} + {time} +
+ ))} +
+
+ {[ + ["Meeting Notes", "Transcribed", Inbox], + ["Q2 Forecast", "Updated", CalendarDays], + ["Budget Tracker", "Synced", Database], + ].map(([title, meta, Icon]) => ( +
+ +
+

+ {title as string} +

+

{meta as string}

+
+
+ ))} +
+
+ + +
+
+
+ ); +} + +function ConnectorDiagram() { + return ( +
+
+ {[ + ["Emails", Mail], + ["Chats", MessageCircle], + ["Calendar", CalendarDays], + ].map(([label, Icon]) => ( +
+ + {label as string} + +
+ ))} +
+
+ +
+
+ {[ + ["Docs", File], + ["Files", Folder], + ["Tools", Workflow], + ].map(([label, Icon]) => ( +
+ + + {label as string} +
+ ))} +
+
+ ); +} + +function PrimitiveArt({ art }: { art: string }) { + if (art === "soft grid") { + return
; + } + + if (art === "folder cluster") { + return ( +
+ + + +
+ ); + } + + if (art === "node map") { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ ); +} + +export default function BrainLandingPage() { + return ( +
+
+ +
+ +
+ +
+
+ + AI-native workspace +
+

+ One Brain for everything you know — and{" "} + everything your agents do. +

+

+ Brain brings docs, databases, files, and agents into one connected + workspace — so your context is organized, remembered, and ready to + act on. +

+
+ + +
+
+
+ + + +
+

+ Works with the tools you already use +

+
+ {integrations.map(([name, Icon, color]) => ( +
+ + {name as string} +
+ ))} +
+
+ +
+
+
+

+ Your context is everywhere. Brain brings it together. +

+

+ Emails, chats, docs, files, and tools each hold a piece of the + picture. Brain connects the dots so your context becomes useful. +

+
+ +
+
+ +
+
+ {primitiveCards.map((card) => ( + + ))} +
+
+ +
+
+
+

+ Ask Brain to do the work around your work. +

+
+ {[ + "Summarize my unread emails", + "Find decisions from last quarter", + "Pull research for a new project", + "Draft a brief from my recent notes", + ].map((item) => ( +
+ + {item} +
+ ))} +
+
+
+
+
+
+

+ Q2 Planning Summary +

+

+ Generated by Research Agent +

+
+ + Complete + +
+
+ {[ + ["Key decisions", "12"], + ["Open items", "3"], + ["Risks identified", "3"], + ].map(([label, value]) => ( +
+

{label}

+

+ {value} +

+
+ ))} +
+
+

Summary

+

+ Q2 execution is on track across core initiatives. Priorities: + launch, adoption, and platform stability. +

+
+
+
+
+
+ +
+
+

+ From context to action. +

+
+ {timeline.map(([title, body], index) => ( +
+ {index < timeline.length - 1 && ( + + )} +
+ {index + 1} +
+

+ {title} +

+

+ {body} +

+
+ ))} +
+
+
+ +
+
+
+
+
+

+ Start with one Brain. +

+

+ Your context. Your space. Ready for action. +

+
+ + +
+
+
+
+ {["Docs", "Databases", "Files"].map((label) => ( +
+ + {label} + + +
+ ))} +
+
+ +
+
+ {["Agents", "Tasks", "Pages"].map((label) => ( +
+ + + {label} + +
+ ))} +
+
+
+
+
+ +
+
+
+ +

+ The AI-native workspace for everything you know and everything + your agents do. +

+
+
+ {[ + ["Product", "Docs", "Databases", "Files", "Agents"], + ["Use cases", "Personal brain", "Team hub", "Research", "Ops"], + ["Resources", "Guides", "Templates", "Changelog", "Support"], + ["Company", "About", "Careers", "Privacy", "Terms"], + ].map(([heading, ...links]) => ( +
+

{heading}

+
+ {links.map((link) => ( + + {link} + + ))} +
+
+ ))} +
+
+

Stay in sync

+
+ + +
+
+
+
+
+ ); +} diff --git a/app/student/complete-profile/page.tsx b/app/student/complete-profile/page.tsx deleted file mode 100644 index 77e2d7ae..00000000 --- a/app/student/complete-profile/page.tsx +++ /dev/null @@ -1,716 +0,0 @@ -"use client"; - -import React, { RefObject, useEffect, useMemo, useRef, useState } from "react"; -import { - Upload, - UserCheck, - FileText, - AlertTriangle, - Repeat, - User, -} from "lucide-react"; -import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { useAnalyzeResume } from "@/hooks/use-register"; -import ResumeUpload from "@/components/features/student/resume-parser/ResumeUpload"; -import { FormDropdown, FormInput } from "@/components/EditForm"; -import { UserService } from "@/lib/api/services"; -import { useProfileData } from "@/lib/api/student.data.api"; -import { Stepper } from "@/components/stepper/stepper"; -import { isProfileResume, isProfileBaseComplete } from "../../../lib/profile"; -import { ModalHandle } from "@/hooks/use-modal"; -import { isValidPHNumber } from "@/lib/utils"; -import { useDbRefs } from "@/lib/db/use-refs"; -import { Job } from "@/lib/db/db.types"; -import { isValidRequiredUserName } from "@/lib/utils/name-utils"; -import { DropdownGroup } from "@/components/ui/dropdown"; - -/* ============================== Modal shell ============================== */ - -export default function IncompleteProfileContent({ - onFinish, -}: { - onFinish: () => void; - applySuccessModalRef?: RefObject; - job?: Job | null; -}) { - return ( -
-
-
- -
-

- Let's finish setting up your profile -

-
- - -
- ); -} - -/* ============================== Types ============================== */ - -type ProfileDraft = { - firstName?: string; - middleName?: string; - lastName?: string; - phone?: string; - university?: string; - college?: string; - department?: string; - degree?: string; -}; - -type ResumeParsedUserSnake = { - first_name?: string; - middle_name?: string; - last_name?: string; - phone_number?: string; - university?: string; - degree?: string; -}; - -function snakeToDraft(u: ResumeParsedUserSnake): Partial { - return { - firstName: u.first_name, - middleName: u.middle_name, - lastName: u.last_name, - phone: u.phone_number, - university: u.university, - degree: u.degree, - }; -} - -/* ============================== Main Stepper ============================== */ - -function CompleteProfileStepper({ onFinish }: { onFinish: () => void }) { - const existingProfile = useProfileData(); - const [step, setStep] = useState(0); - const [showComplete, setShowComplete] = useState(false); - - // profile being edited - const [profile, setProfile] = useState(() => ({ - firstName: existingProfile.data?.first_name ?? "", - middleName: existingProfile.data?.middle_name ?? "", - lastName: existingProfile.data?.last_name ?? "", - phone: existingProfile.data?.phone_number ?? "", - university: existingProfile.data?.university ?? "", - college: existingProfile.data?.college ?? "", - department: existingProfile.data?.department ?? "", - degree: existingProfile.data?.degree ?? "", - })); - - const [autoApply, setAutoApply] = useState(true); - - // parsing/upload states - const [file, setFile] = useState(null); - const [isParsing, setIsParsing] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const [parsedReady, setParsedReady] = useState(false); - const [parseError, setParseError] = useState(null); - - // analyze - const { upload, fileInputRef, response } = useAnalyzeResume(file); - const handledResponseRef = useRef(null); - - // upload on file - useEffect(() => { - if (!file) return; - setParseError(null); - setParsedReady(false); - setIsParsing(true); - void upload(file); - }, [file]); - - // hydrate once per promise - useEffect(() => { - if (!response || handledResponseRef.current === response) return; - handledResponseRef.current = response; - - const { extractedUser, message, success } = response as { - success?: boolean; - message?: string; - extractedUser?: ResumeParsedUserSnake; - }; - - if (!success && message) { - setParseError(message || "Failed to analyze resume."); - setIsParsing(false); - setParsedReady(false); - return; - } - - if (extractedUser) { - const patch = snakeToDraft(extractedUser); - setProfile((p) => ({ ...p, ...patch })); - } - - setParsedReady(true); - setIsParsing(false); - setStep(1); - }, [response]); - - const steps = useMemo(() => { - const s: Array<{ - id: "resume" | "base" | "auto-apply"; - title: string; - subtitle?: string; - icon: any; - canNext: () => boolean; - component: React.ReactNode; - }> = []; - - if (!isProfileResume(existingProfile.data)) { - s.push({ - id: "resume", - title: "Upload your resume", - subtitle: "Upload a PDF and we'll auto-fill what we can.", - icon: Upload, - canNext: () => !!file && !isParsing, - component: ( - setFile(f)} - fileInputRef={ - fileInputRef as unknown as RefObject - } - response={response ?? null} - /> - ), - }); - } - - if (!isProfileBaseComplete(existingProfile.data)) { - s.push({ - id: "base", - title: "Basic details", - icon: UserCheck, - canNext: () => { - const phoneValid = isValidPHNumber(profile.phone); - return ( - !!profile.firstName && - isValidRequiredUserName(profile.firstName) && - !!profile.lastName && - isValidRequiredUserName(profile.lastName) && - !isUpdating && - phoneValid && - !!profile.degree && - !!profile.college && - !!profile.department - ); - }, - component: , - }); - } - - if (existingProfile.data?.acknowledged_auto_apply === false) { - s.push({ - id: "auto-apply", - title: "Auto-apply settings", - icon: Repeat, - canNext: () => autoApply !== null && !isUpdating, - component: , - }); - } - - return s; - }, [ - isParsing, - parsedReady, - parseError, - response, - existingProfile.data, - profile, - isUpdating, - autoApply, - ]); - - useEffect(() => { - if (steps.length === 0) { - setShowComplete(true); - } - }, [steps.length]); - - // Next behavior - const onNext = async () => { - const current = steps[step]; - - console.log(current); - - if (!current) return; - - if (current.id === "resume") { - setStep(step + 1); - return; - } - - if (current.id === "base") { - setIsUpdating(true); - const fullName = [ - profile?.firstName ?? "", - profile?.middleName ?? "", - profile?.lastName ?? "", - ] - .filter(Boolean) - .join(" "); - await UserService.updateMyProfile({ - first_name: profile.firstName ?? "", - middle_name: profile.middleName ?? "", - last_name: profile.lastName ?? "", - phone_number: profile.phone ?? "", - university: profile.university ?? "", - degree: profile.degree ?? "", - college: profile.college ?? "", - department: profile.department ?? "", - internship_moa_fields: { - student: { - "student-degree": profile.degree ?? "", - "student-college": profile.college ?? "", - "student-full-name": fullName, - "student-first-name": profile.firstName ?? "", - "student-middle-name": profile.middleName ?? "", - "student-last-name": profile.lastName ?? "", - "student-department": profile.department ?? "", - "student-university": profile.university ?? "", - "student-phone-number": profile.phone ?? "", - }, - }, - }) - .then(() => { - setIsUpdating(false); - const isLast = step + 1 >= steps.length; - if (isLast) setShowComplete(true); - else setStep(step + 1); - }) - .catch((e) => console.log(e)); - return; - } - - if (current.id === "auto-apply") { - if (autoApply === null) return; - - setIsUpdating(true); - await UserService.updateMyProfile({ - acknowledged_auto_apply: true, - apply_for_me: autoApply, - }).then(() => { - setIsUpdating(false); - const isLast = step + 1 >= steps.length; - if (isLast) setShowComplete(true); - else setStep(step + 1); - }); - return; - } - }; - - if (showComplete) { - return ; - } - - return ( - - void onNext()} - onBack={() => setStep(Math.max(0, step - 1))} - /> - - ); -} - -/* ---------------------- Step: Resume Upload ---------------------- */ - -function StepResume({ - file, - isParsing, - parsedReady, - parseError, - onPick, - fileInputRef, - response, -}: { - file: File | null; - isParsing: boolean; - parsedReady: boolean; - parseError: string | null; - onPick: (file: File) => void; - fileInputRef: React.RefObject; - response: Promise | null; -}) { - return ( -
- onPick(f)} - onComplete={() => {}} - /> - - {file && ( -
-
- -
-
{file.name}
-
- {(file.size / 1024 / 1024).toFixed(2)} MB -
-
-
- {parsedReady ? ( - Parsed - ) : isParsing ? ( - Parsing... - ) : ( - Waiting... - )} -
- )} - - {parseError && ( -
- - {parseError} -
- )} -
- ); -} - -/* ---------------------- Step: Basic Identity ---------------------- */ - -function StepBasicIdentity({ - value, - onChange, -}: { - value: ProfileDraft; - onChange: (v: ProfileDraft) => void; -}) { - const { - colleges, - departments, - get_departments_by_college, - to_department_name, - to_university_name, - get_colleges_by_university, - to_college_name, - } = useDbRefs(); - - const phoneInvalid = useMemo( - () => !!value.phone && !isValidPHNumber(value.phone), - [value.phone], - ); - - const firstNameInvalid = useMemo( - () => !!value.firstName && !isValidRequiredUserName(value.firstName), - [value.firstName], - ); - - const lastNameInvalid = useMemo( - () => !!value.lastName && !isValidRequiredUserName(value.lastName), - [value.lastName], - ); - - const [departmentOptions, setDepartmentOptions] = - useState<{ id: string; name: string }[]>(departments); - - const [collegesOptions, setCollegesOptions] = - useState< - { id: string; name: string; short_name: string; university_id: string }[] - >(colleges); - - // for realtime updating the department based on the college - useEffect(() => { - const collegeId = value.college; - - if (collegeId) { - const list = get_departments_by_college?.(collegeId); - setDepartmentOptions( - list.map((d) => ({ - id: d, - name: to_department_name(d) ?? "", - })), - ); - } else { - // no college selected -> empty department options - setDepartmentOptions( - departments.map((d) => ({ id: d.id, name: d.name })), - ); - if (value.department) { - onChange({ ...value, department: undefined }); - } - } - }, [ - value.college, - value.department, - departments, - get_departments_by_college, - to_department_name, - onChange, - ]); - - // for realtime updating the college based on the university - useEffect(() => { - const universityId = value.university; - - if (universityId) { - const list = get_colleges_by_university?.(universityId) ?? []; - const mapped = list.map((d) => ({ - id: d, - name: to_college_name(d) ?? "", - short_name: "", - university_id: universityId, - })); - setCollegesOptions(mapped); - - // If the currently selected college is not in the new mapped list, clear it (and department) - if (value.college && !mapped.some((c) => c.id === value.college)) { - onChange({ ...value, college: undefined, department: undefined }); - } - } else { - // no university selected -> show all colleges and clear college/department - setCollegesOptions( - colleges.map((d) => ({ - id: d.id, - name: d.name, - short_name: d.short_name, - university_id: d.university_id, - })), - ); - - // Clear selected college and department because no university is chosen - if (value.college || value.department) { - onChange({ ...value, college: undefined, department: undefined }); - } - } - }, [ - value.university, - value.college, - value.department, - colleges, - get_colleges_by_university, - to_college_name, - onChange, - ]); - - return ( -
- {/* Personal */} -
-

- Personal information -

-
-
- onChange({ ...value, firstName: v })} - /> - - onChange({ ...value, middleName: v })} - /> - onChange({ ...value, lastName: v })} - /> -
- - {firstNameInvalid && ( -

- Please enter a first name. Special characters are not allowed. -

- )} - {lastNameInvalid && ( -

- Please enter a last name. Special characters are not allowed. -

- )} - - onChange({ ...value, phone: v })} - /> - {phoneInvalid && ( -

- Please enter a valid mobile number (e.g., 639XXXXXXXXX). -

- )} -
-
- - {/* Education */} -
-

- Educational background -

-
-
- onChange({ ...value, college: val.toString() })} - options={collegesOptions} - placeholder="Select college…" - /> -
- -
- - onChange({ ...value, department: val.toString() }) - } - options={departmentOptions} - placeholder="Select department…" - /> -
-
- onChange({ ...value, degree: val.toString() })} - placeholder="Select degree / program…" - /> -
-
-
-
- ); -} - -/* ---------------------- Step: Auto-Apply Acknowledge ---------------------- */ - -function StepAutoApply({ - value, - onChange, -}: { - value: boolean | null; - onChange: (v: boolean) => void; -}) { - return ( -
- -
-
-

Turn on Auto-Apply?

- - Recommended - -
- -

- We’ll auto-submit matching roles using your saved details.{" "} -

- - {/* Native select to avoid extra deps; replace with shadcn Select if you prefer */} -
- - -

- You can change this anytime in your profile. -

-
-
-
-
- ); -} - -/* ---------------------- Completion (rendered OUTSIDE stepper) ---------------------- */ - -function StepComplete({ onDone }: { onDone: () => void }) { - useEffect(() => { - const t = setTimeout(() => onDone(), 1400); - return () => clearTimeout(t); - }, [onDone]); - - return ( -
- {/* Check animation */} -
-
- - - -
-
- -

Profile complete

-

- You’re all set. Nice work! -

- - -
- ); -} - -/* ============================== Exports ============================== */ -export { CompleteProfileStepper }; diff --git a/app/student/conversations/page.tsx b/app/student/conversations/page.tsx deleted file mode 100644 index c5e84505..00000000 --- a/app/student/conversations/page.tsx +++ /dev/null @@ -1,397 +0,0 @@ -"use client"; -import { useConversation, useConversations } from "@/hooks/use-conversation"; -import { useAuthContext } from "@/lib/ctx-auth"; -import { Card } from "@/components/ui/card"; -import { EmployerPfp } from "@/components/shared/pfp"; -import { ChevronLeft, ChevronRight, SendHorizonal } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useAppContext } from "@/lib/ctx-app"; -import { Ref, useEffect, useMemo, useRef, useState } from "react"; -import { Textarea } from "@/components/ui/textarea"; -import { Message } from "@/components/ui/messages"; -import { Button } from "@/components/ui/button"; -import { UserConversationService } from "@/lib/api/services"; -import { useProfileData } from "@/lib/api/student.data.api"; -import { Loader } from "@/components/ui/loader"; -import { useEmployerName } from "@/hooks/use-employer-api"; -import { Badge } from "@/components/ui/badge"; - -export default function ConversationsPage() { - const { redirectIfNotLoggedIn } = useAuthContext(); - const profile = useProfileData(); - const conversations = useConversations(); - const { isMobile } = useAppContext(); - - // selection + message composing state - const [conversationId, setConversationId] = useState(""); - const conversation = useConversation("user", conversationId); - const [message, setMessage] = useState(""); - const [sending, setSending] = useState(false); - - // mobile "router": list or chat - const [mobileView, setMobileView] = useState<"list" | "chat">("list"); - - // anchor for autoscroll - const chatAnchorRef = useRef(null); - - redirectIfNotLoggedIn(); - - // auto-switch to chat view on mobile when a conversation is selected - useEffect(() => { - if (isMobile && conversationId) setMobileView("chat"); - }, [isMobile, conversationId]); - - // unsubscribe on conversation change (keeps your original behavior) - useEffect(() => { - conversation.unsubscribe(); - }, [conversationId]); - - const sortedConvos = useMemo( - () => - (conversations.data ?? []).toSorted( - (a, b) => - (b.last_unread?.timestamp ?? 0) - (a.last_unread?.timestamp ?? 0), - ), - [conversations.data], - ); - - const endSend = () => { - setMessage(""); - setSending(false); - chatAnchorRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - const handleMessage = async (employerId: string | undefined, msg: string) => { - if (!msg.trim() || !employerId) return; - setSending(true); - - const employerConversation = conversations.data?.find((c) => - c?.subscribers?.includes(employerId), - ); - - if (!employerConversation) return endSend(); - - await UserConversationService.sendToEmployer( - employerConversation.id, - msg, - ).catch(endSend); - - endSend(); - }; - - // loading state for initial fetch - if (conversations.loading) { - return Loading your conversations...; - } - - const hasConversations = (conversations.data?.length ?? 0) > 0; - - return ( -
- {hasConversations ? ( - <> - {/* ===== Left: List (Desktop always visible; Mobile only when in "list" view) ===== */} - - - {/* ===== Right: Chat (Desktop always visible; Mobile only when in "chat" view) ===== */} -
- {/* Mobile top bar */} - {isMobile && ( -
-
- - -
-
- )} - - {/* Chat body */} - {conversation?.loading ? ( -
- Loading conversation... -
- ) : conversationId ? ( - <> - - handleMessage(conversation.senderId, message)} - /> - - ) : ( - - )} -
- - ) : ( - - )} -
- ); -} - -/* ======================= Subcomponents ======================= */ - -function ConversationList({ - conversations, - profileId, - onPick, -}: { - conversations: any[]; - profileId?: string; - onPick: (id: string) => void; -}) { - const { unreads } = useConversations(); - - return ( -
- {conversations.map((c) => ( - - u.subscribers.some((s: string) => c.subscribers?.includes(s)), - ) - } - /> - ))} -
- ); -} - -function ChatHeaderTitle({ conversationId }: { conversationId?: string }) { - const { senderId } = useConversation("user", conversationId || ""); - const { employerName } = useEmployerName(senderId || ""); - return ( -
-
- {employerName || "Conversations"} -
-
Chat
-
- ); -} - -function ComposerBar({ - value, - onChange, - onSend, - disabled, -}: { - value: string; - onChange: (v: string) => void; - onSend: () => void; - disabled?: boolean; -}) { - return ( -
-
-