From 5cf9022e9bcbffc9d4bce178eaaa5c12256e16bb Mon Sep 17 00:00:00 2001 From: Mo David Date: Sun, 19 Apr 2026 00:31:34 +0800 Subject: [PATCH 001/143] patch: remove old instances of supabase use --- app/hire/layout.tsx | 13 +- .../forms/components/FormDashboard.tsx | 2 +- .../forms/components/FormTemplatesList.tsx | 2 +- app/student/layout.tsx | 13 +- lib/api/services.ts | 2 +- lib/db/db.types.ts | 80 ++-- ...use-moa-backend.tsx => forms-db.types.tsx} | 3 +- lib/db/middleware.ts | 56 --- lib/db/use-bi-moa-backend.ts | 77 ++-- lib/db/use-bi-moa.tsx | 24 +- lib/db/use-refs-backend.ts | 349 +++++------------- lib/db/use-refs.tsx | 242 +++++++++++- package.json | 6 +- 13 files changed, 445 insertions(+), 424 deletions(-) rename lib/db/{use-moa-backend.tsx => forms-db.types.tsx} (77%) delete mode 100644 lib/db/middleware.ts diff --git a/app/hire/layout.tsx b/app/hire/layout.tsx index a54b5a55..10fa427a 100644 --- a/app/hire/layout.tsx +++ b/app/hire/layout.tsx @@ -5,6 +5,8 @@ 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"; @@ -54,14 +56,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} diff --git a/app/student/forms/components/FormDashboard.tsx b/app/student/forms/components/FormDashboard.tsx index 8c0a7db7..caefc975 100644 --- a/app/student/forms/components/FormDashboard.tsx +++ b/app/student/forms/components/FormDashboard.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FileSearch, FileText } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion"; import { cn } from "@/lib/utils"; -import { FormTemplate } from "@/lib/db/use-moa-backend"; +import { FormTemplate } from "@/lib/db/forms-db.types"; import { Loader } from "@/components/ui/loader"; import { FormRendererContextBridge, diff --git a/app/student/forms/components/FormTemplatesList.tsx b/app/student/forms/components/FormTemplatesList.tsx index f459c0e6..0ec8e13d 100644 --- a/app/student/forms/components/FormTemplatesList.tsx +++ b/app/student/forms/components/FormTemplatesList.tsx @@ -1,5 +1,5 @@ import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; -import { FormTemplate } from "@/lib/db/use-moa-backend"; +import { FormTemplate } from "@/lib/db/forms-db.types"; import { ChevronRight } from "lucide-react"; import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; diff --git a/app/student/layout.tsx b/app/student/layout.tsx index ace1685f..74bbecf0 100644 --- a/app/student/layout.tsx +++ b/app/student/layout.tsx @@ -5,6 +5,8 @@ import { HeaderContextProvider } from "@/lib/ctx-header"; import { RefsContextProvider } from "@/lib/db/use-refs"; import { AppContextProvider } from "@/lib/ctx-app"; 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 AllowLanding from "./allowLanding"; @@ -80,14 +82,19 @@ export const viewport: Viewport = { * * @component */ -export const RootLayout = ({ +export const RootLayout = async ({ children, }: Readonly<{ children: React.ReactNode; }>) => { + const [refsData, biMoaData] = await Promise.all([ + getRefsData(), + getBiMoaData(), + ]); + return ( - - + + {children} diff --git a/lib/api/services.ts b/lib/api/services.ts index ef161b71..76577318 100644 --- a/lib/api/services.ts +++ b/lib/api/services.ts @@ -1,4 +1,4 @@ -import { FormTemplate } from "../db/use-moa-backend"; +import { FormTemplate } from "../db/forms-db.types"; import { Conversation, CreateJobChallengeListingPayload, diff --git a/lib/db/db.types.ts b/lib/db/db.types.ts index 2dd7ed4b..b9e9c601 100644 --- a/lib/db/db.types.ts +++ b/lib/db/db.types.ts @@ -1,44 +1,60 @@ import { - Database as _Database, - Json, - Tables, + DB, + RefColleges, + RefJobAllowances, + RefJobCategories, + RefJobModes, + RefJobTypes, + RefUniversities, + JobsChallenge, + RefJobPayFreq, + RefAppStatuses, + RefIndustries, + RefDepartments, + Moa as _Moa, + Users, + Employers, + Conversations, + EmployerUsers, + Jobs, + Applications, + SavedJobs, } from "@betterinternship/schema.base"; +import { Selectable } from "kysely"; -export type Database = _Database; -export type College = Tables<"ref_colleges">; -export type University = Tables<"ref_universities">; -export type JobType = Tables<"ref_job_types">; -export type JobAllowance = Tables<"ref_job_allowances">; -export type JobCategory = Tables<"ref_job_categories">; -export type JobPayFreq = Tables<"ref_job_pay_freq">; -export type JobMode = Tables<"ref_job_modes">; -export type JobChallenge = Tables<"jobs_challenge">; -export type AppStatus = Tables<"ref_app_statuses">; -export type Industry = Tables<"ref_industries">; -export type Department = Tables<"ref_departments">; -export type Moa = Tables<"moa">; -export type PrivateUser = Tables<"users">; -type _PublicUserBase = Omit, "verification_hash">; -export type PublicUser = Omit<_PublicUserBase, "internship_preferences"> & { +export type Database = DB; +export type College = Selectable; +export type University = Selectable; +export type JobType = Selectable; +export type JobAllowance = Selectable; +export type JobCategory = Selectable; +export type JobPayFreq = Selectable; +export type JobMode = Selectable; +export type JobChallenge = Selectable; +export type AppStatus = Selectable; +export type Industry = Selectable; +export type Department = Selectable; +export type Moa = Selectable<_Moa>; +export type PrivateUser = Selectable; +export type PublicUser = Omit< + PrivateUser, + "verification_hash" | "internship_preferences" +> & { internship_preferences?: InternshipPreferences; }; -export type Employer = Partial>; -export type User = Partial>; -export interface Conversation extends Tables<"conversations"> { +export type Employer = Partial>; +export type User = Partial>; +export interface Conversation extends Selectable { employers?: Partial; employer?: Partial; users?: Partial; user?: Partial; } -export type PrivateEmployerUser = Tables<"employer_users">; -export type PublicEmployerUser = Omit< - Tables<"employer_users">, - "is_deactivated" ->; -export interface MoA extends Partial> {} +export type PrivateEmployerUser = Selectable; +export type PublicEmployerUser = Omit; export interface Job extends Omit< - Partial>, + Partial>, "internship_preferences" > { employer?: Partial; @@ -60,14 +76,14 @@ export type UpdateJobChallengeListingPayload = Partial & { challenge?: JobChallengePayload | Partial | null; }; -export interface UserApplication extends Partial> { +export interface UserApplication extends Partial> { job?: Partial; jobs?: Partial; employer?: Partial; employers?: Partial; } -export interface EmployerApplication extends Partial> { +export interface EmployerApplication extends Partial> { job?: Partial; jobs?: Partial; user?: Partial; @@ -75,7 +91,7 @@ export interface EmployerApplication extends Partial> { challenge_submission?: string | null; } -export interface SavedJob extends Partial> { +export interface SavedJob extends Partial> { job?: Partial; jobs?: Partial; } diff --git a/lib/db/use-moa-backend.tsx b/lib/db/forms-db.types.tsx similarity index 77% rename from lib/db/use-moa-backend.tsx rename to lib/db/forms-db.types.tsx index 3ab6e214..458f1f86 100644 --- a/lib/db/use-moa-backend.tsx +++ b/lib/db/forms-db.types.tsx @@ -1,13 +1,12 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-10-11 00:00:00 - * @ Modified time: 2025-12-30 12:06:12 + * @ Modified time: 2026-04-18 23:50:39 * @ Description: * * This handles interactions with our MOA Api server. */ -// ! move this into a form utils file export interface FormTemplate { formDocument: string; formVersion: number; diff --git a/lib/db/middleware.ts b/lib/db/middleware.ts deleted file mode 100644 index d391336d..00000000 --- a/lib/db/middleware.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createServerClient } from "@supabase/ssr"; -import { NextResponse, type NextRequest } from "next/server"; - -export async function updateSession(request: NextRequest) { - let supabaseResponse = NextResponse.next({ - request, - }); - const path = request.nextURL.pathname; - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return request.cookies.getAll(); - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => - request.cookies.set(name, value) - ); - supabaseResponse = NextResponse.next({ - request, - }); - cookiesToSet.forEach(({ name, value, options }) => - supabaseResponse.cookies.set(name, value, options) - ); - }, - }, - } - ); - - const { - data: { user }, - } = await supabase.auth.getUser(); - - if (!user && path.startsWith("/dashboard/overview")) { - const url = request.nextURL.clone(); - url.pathname = "/"; - return NextResponse.redirect(url); - } - - if (user && path === "/") { - const url = request.nextURL.clone(); - url.pathname = "/dashboard/overview"; - return NextResponse.redirect(url); - } - - if (user && path === "/dashboard") { - const url = request.nextURL.clone(); - url.pathname = "/dashboard/overview"; - return NextResponse.redirect(url); - } - - return supabaseResponse; -} diff --git a/lib/db/use-bi-moa-backend.ts b/lib/db/use-bi-moa-backend.ts index f6d8ced8..ad224c32 100644 --- a/lib/db/use-bi-moa-backend.ts +++ b/lib/db/use-bi-moa-backend.ts @@ -1,67 +1,38 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-06-22 14:37:59 - * @ Modified time: 2025-10-11 00:03:47 + * @ Modified time: 2026-04-19 00:00:00 * @ Description: * - * Separates out the server component of the context. + * Server-only data loader for moa records. */ -import { useCallback, useEffect, useState } from "react"; +import "server-only"; import { Moa } from "./db.types"; -import { createClient } from "@supabase/supabase-js"; +import { Kysely, PostgresDialect } from "kysely"; +import { DB } from "@betterinternship/schema.base"; +import { Pool } from "pg"; -// Environment setup -const DB_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; -const DB_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; +const DATABASE_URL = process.env.DATABASE_URL; -if (!DB_URL || !DB_ANON_KEY) - throw new Error("[ERROR:ENV] Missing supabase configuration."); -const db = createClient(DB_URL ?? "", DB_ANON_KEY ?? ""); +if (!DATABASE_URL) throw new Error("[ERROR:ENV] Missing database url."); -/** - * Fetches actual data from db. - * - * @returns - */ -export const createBiMoaContext = () => { - const [moa, setMoa] = useState([]); - const [loading, setLoading] = useState(true); - - /** - * Fetch the entire moa table. - */ - const fetchMoaRefTable = async () => { - const { data, error } = await db.from("moa").select("*"); - if (error) console.error(error); - else setMoa(data); - setLoading(false); - }; +const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: DATABASE_URL, + }), + }), +}); - /** - * Checks whether or not an association between the employer and university exists. - * - * @param employer_id - * @param university_id - */ - const check = useCallback( - (employer_id: string, university_id: string) => { - if (loading) return false; - return moa.some( - (m) => - m.employer_id === employer_id && - m.university_id === university_id && - new Date(m.expires_at ?? "").getTime() > new Date().getTime() - ); - }, - [moa, loading] - ); +export interface BIMoaData { + moa: Moa[]; +} - useEffect(() => { - fetchMoaRefTable(); - }, []); - - return { - check, - }; +/** + * Fetches the moa table on the server and returns serializable data for clients. + */ +export const getBiMoaData = async (): Promise => { + const moa = await db.selectFrom("moa").selectAll().execute(); + return { moa }; }; diff --git a/lib/db/use-bi-moa.tsx b/lib/db/use-bi-moa.tsx index e9ad49c3..27b8fc40 100644 --- a/lib/db/use-bi-moa.tsx +++ b/lib/db/use-bi-moa.tsx @@ -1,7 +1,7 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-06-22 13:40:54 - * @ Modified time: 2025-10-11 00:00:11 + * @ Modified time: 2026-04-19 00:10:33 * @ Description: * * Gives us utils to check if company has moa. @@ -10,7 +10,7 @@ "use client"; import { createContext, useContext } from "react"; -import { createBiMoaContext } from "./use-bi-moa-backend"; +import { Moa } from "./db.types"; // The IMoa context should only be loaded once interface IBIMoa { @@ -25,15 +25,31 @@ const biMoaContext = createContext({} as IBIMoa); * @context */ export const BIMoaContextProvider = ({ + moa, children, }: { + moa?: Moa[]; children: React.ReactNode; }) => { - const biMoaContextValue = createBiMoaContext(); + const biMoaContextValue = createBiMoaContext(moa ?? []); return ( - {children} + + {children} + ); }; export const useDbMoa = () => useContext(biMoaContext); + +const createBiMoaContext = (moa: Moa[]): IBIMoa => { + return { + check: (employer_id: string, university_id: string) => + moa.some( + (m) => + m.employer_id === employer_id && + m.university_id === university_id && + new Date(m.expires_at ?? "").getTime() > new Date().getTime(), + ), + }; +}; diff --git a/lib/db/use-refs-backend.ts b/lib/db/use-refs-backend.ts index 5fc5d4f7..f7252279 100644 --- a/lib/db/use-refs-backend.ts +++ b/lib/db/use-refs-backend.ts @@ -1,13 +1,13 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-06-15 03:09:57 - * @ Modified time: 2025-09-25 18:02:00 + * @ Modified time: 2026-04-19 00:26:30 * @ Description: * - * The actual backend connection to provide the refs data + * Server-only data loaders for refs tables. */ -import { useState, useEffect, useCallback } from "react"; +import "server-only"; import { College, University, @@ -20,20 +20,29 @@ import { JobCategory, Department, } from "./db.types"; -import { createClient } from "@supabase/supabase-js"; - -// Environment setup -const db_url = process.env.NEXT_PUBLIC_SUPABASE_URL; -const db_anon_key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -if (!db_url || !db_anon_key) - throw new Error("[ERROR:ENV] Missing supabase configuration."); -const db = createClient(db_url ?? "", db_anon_key ?? ""); - -// Setup the context -export interface IRefsContext { - ref_loading: boolean; +import { DB } from "@betterinternship/schema.base"; +import { Kysely, PostgresDialect } from "kysely"; +import { Pool } from "pg"; + +const DATABASE_URL = process.env.DATABASE_URL; + +if (!DATABASE_URL) throw new Error("[ERROR:ENV] Missing database url."); + +const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: DATABASE_URL, + }), + }), +}); + +export interface RefDomain { + id: string; + name: string; + university_id: string; +} +export interface RefsData { colleges: College[]; departments: Department[]; universities: University[]; @@ -44,85 +53,91 @@ export interface IRefsContext { job_pay_freq: JobPayFreq[]; app_statuses: AppStatus[]; industries: Industry[]; + domains: RefDomain[]; +} + +// Setup the context +export interface IRefsContext extends RefsData { + ref_loading: boolean; get_college: (id: string | null | undefined) => College | null; to_college_name: ( id: string | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_college_by_name: (name: string | null | undefined) => College | null; get_university: (id: string | null | undefined) => University | null; to_university_name: ( id: string | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_university_by_name: ( - name: string | null | undefined + name: string | null | undefined, ) => University | null; get_job_type: (id: number | null | undefined) => JobType | null; to_job_type_name: ( id: number | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_job_type_by_name: (name: string | null | undefined) => JobType | null; get_job_mode: (id: number | null | undefined) => JobMode | null; to_job_mode_name: ( id: number | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_job_mode_by_name: (name: string | null | undefined) => JobMode | null; get_job_allowance: (id: number | null | undefined) => JobAllowance | null; to_job_allowance_name: ( id: number | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_job_allowance_by_name: ( - name: string | null | undefined + name: string | null | undefined, ) => JobAllowance | null; get_job_pay_freq: (id: number | null | undefined) => JobPayFreq | null; to_job_pay_freq_name: ( id: number | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_job_pay_freq_by_name: ( - name: string | null | undefined + name: string | null | undefined, ) => JobPayFreq | null; get_app_status: (id: number | null | undefined) => AppStatus | null; to_app_status_name: ( id: number | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_app_status_by_name: (name: string | null | undefined) => AppStatus | null; get_industry: (id: string | null | undefined) => Industry | null; to_industry_name: ( id: string | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_industry_by_name: (name: string | null | undefined) => Industry | null; get_job_category: (id: string | null | undefined) => JobCategory | null; to_job_category_name: ( id: string | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_job_category_by_name: ( - name: string | null | undefined + name: string | null | undefined, ) => JobCategory | null; get_department: (id: string | null | undefined) => Department | null; to_department_name: ( id: string | null | undefined, - def?: string | null + def?: string | null, ) => string | null; get_department_by_name: ( - name: string | null | undefined + name: string | null | undefined, ) => Department | null; get_departments_by_college: (college_id: string) => string[]; @@ -132,191 +147,50 @@ export interface IRefsContext { } /** - * A utility that allows us to create ref hooks from our reference tables. - * - * @hook - * @internal + * Fetches all refs tables on the server and returns serializable data for clients. */ -const createRefInternalHook = < - ID extends string | number, - T extends { id: ID; name: string } ->( - table: string -) => { - const [data, set_data] = useState([]); - const [loading, set_loading] = useState(true); - - /** - * Fetches the data from the backend. - */ - async function fetch_data() { - set_loading(true); - const { data, error } = await db.from(table).select("*"); - if (error) console.error(error); - else set_data(data); - set_loading(false); - } - - /** - * Converts an id to it's name. - * - * @param id - * @returns - */ - const to_name = useCallback( - (id: ID | null | undefined, def: string = "Not specified"): string => { - if (!id && id !== 0) return def; - const f = data?.filter((d) => d.id === id); - if (!f.length) return def; - return f[0].name; - }, - [data] - ); - - /** - * Gets a ref by id - * - * @param id - * @returns - */ - const get = useCallback( - (id: ID | null | undefined): T | null => { - if (!id && id !== 0) return null; - const f = data?.filter((d) => d.id === id); - if (!f.length) return null; - return f[0]; - }, - [data] - ); - - /** - * Gets a ref by name - * - * @param name - * @returns - */ - const get_by_name = useCallback( - (name: string | null | undefined): T | null => { - if (!name) return null; - const f = data?.filter((d) => d.name === name); - if (!f.length) return null; - return f[0]; - }, - [data] - ); - - // Fetch the data at the start - useEffect(() => { - fetch_data(); - }, []); +export const getRefsData = async (): Promise => { + const [ + colleges, + universities, + job_types, + job_modes, + job_allowances, + job_pay_freq, + app_statuses, + industries, + job_categories, + departments, + domains, + ] = await Promise.all([ + db.selectFrom("ref_colleges").selectAll().execute() as Promise, + db.selectFrom("ref_universities").selectAll().execute() as Promise< + University[] + >, + db.selectFrom("ref_job_types").selectAll().execute() as Promise, + db.selectFrom("ref_job_modes").selectAll().execute() as Promise, + db.selectFrom("ref_job_allowances").selectAll().execute() as Promise< + JobAllowance[] + >, + db.selectFrom("ref_job_pay_freq").selectAll().execute() as Promise< + JobPayFreq[] + >, + db.selectFrom("ref_app_statuses").selectAll().execute() as Promise< + AppStatus[] + >, + db.selectFrom("ref_industries").selectAll().execute() as Promise< + Industry[] + >, + db.selectFrom("ref_job_categories").selectAll().execute() as Promise< + JobCategory[] + >, + db.selectFrom("ref_departments").selectAll().execute() as Promise< + Department[] + >, + db.selectFrom("ref_domains").selectAll().execute() as Promise, + ]); return { - data, - get, - to_name, - get_by_name, - loading, - }; -}; - -export const createRefsContext = () => { - const [loading, setLoading] = useState(true); - - const { - data: colleges, - get: get_college, - to_name: to_college_name, - get_by_name: get_college_by_name, - loading: l2, - } = createRefInternalHook("ref_colleges"); - - const { - data: universities, - get: get_university, - to_name: to_university_name, - get_by_name: get_university_by_name, - loading: l3, - } = createRefInternalHook("ref_universities"); - - const { - data: job_types, - get: get_job_type, - to_name: to_job_type_name, - get_by_name: get_job_type_by_name, - loading: l4, - } = createRefInternalHook("ref_job_types"); - - const { - data: job_modes, - get: get_job_mode, - to_name: to_job_mode_name, - get_by_name: get_job_mode_by_name, - loading: l5, - } = createRefInternalHook("ref_job_modes"); - - const { - data: job_allowances, - get: get_job_allowance, - to_name: to_job_allowance_name, - get_by_name: get_job_allowance_by_name, - loading: l6, - } = createRefInternalHook("ref_job_allowances"); - - const { - data: job_pay_freq, - get: get_job_pay_freq, - to_name: to_job_pay_freq_name, - get_by_name: get_job_pay_freq_by_name, - loading: l7, - } = createRefInternalHook("ref_job_pay_freq"); - - const { - data: app_statuses, - get: get_app_status, - to_name: to_app_status_name, - get_by_name: get_app_status_by_name, - loading: l8, - } = createRefInternalHook("ref_app_statuses"); - - const { - data: industries, - get: get_industry, - to_name: to_industry_name, - get_by_name: get_industry_by_name, - loading: l9, - } = createRefInternalHook("ref_industries"); - - const { - data: job_categories, - get: get_job_category, - to_name: to_job_category_name, - get_by_name: get_job_category_by_name, - loading: l10, - } = createRefInternalHook("ref_job_categories"); - - const { - data: departments, - get: get_department, - to_name: to_department_name, - get_by_name: get_department_by_name, - loading: l11, - } = createRefInternalHook("ref_departments"); - - const { data: domains, loading: l13 } = createRefInternalHook< - string, - { id: string; name: string; university_id: string } - >("ref_domains"); - - useEffect(() => { - setLoading( - l2 || l3 || l4 || l5 || l6 || l7 || l8 || l9 || l10 || l11 || l13 - ); - }, [l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l13]); - - // The API to provide to the app - const refs_context = { - ref_loading: loading, - colleges, departments, universities, @@ -325,55 +199,8 @@ export const createRefsContext = () => { job_allowances, job_categories, job_pay_freq, - industries, app_statuses, + industries, domains, - - to_college_name, - to_department_name, - to_university_name, - to_job_type_name, - to_job_mode_name, - to_job_allowance_name, - to_job_category_name, - to_job_pay_freq_name, - to_app_status_name, - to_industry_name, - - get_college, - get_department, - get_university, - get_job_type, - get_job_mode, - get_job_category, - get_job_allowance, - get_job_pay_freq, - get_app_status, - get_industry, - - get_college_by_name, - get_department_by_name, - get_university_by_name, - get_job_type_by_name, - get_job_mode_by_name, - get_job_allowance_by_name, - get_job_category_by_name, - get_job_pay_freq_by_name, - get_app_status_by_name, - get_industry_by_name, - - get_departments_by_college: (college_id: string) => - departments.filter((d) => d.college_id === college_id).map((d) => d.id), - - get_colleges_by_university: (university_id: string) => - colleges - .filter((c) => c.university_id === university_id) - .map((c) => c.id), - - getUniversityFromDomain: (domain: string) => - domains.filter((d) => d.name === domain).map((d) => d.university_id), - isNotNull: (ref: any) => ref || ref === 0, }; - - return refs_context; }; diff --git a/lib/db/use-refs.tsx b/lib/db/use-refs.tsx index 3e03326f..406b502a 100644 --- a/lib/db/use-refs.tsx +++ b/lib/db/use-refs.tsx @@ -1,7 +1,7 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-06-10 04:31:46 - * @ Modified time: 2025-09-25 18:02:05 + * @ Modified time: 2026-04-19 00:24:27 * @ Description: * * Accesses refs directly from the database. @@ -10,7 +10,116 @@ "use client"; import { createContext, useContext } from "react"; -import { IRefsContext, createRefsContext } from "./use-refs-backend"; +import { + AppStatus, + College, + Department, + Industry, + JobAllowance, + JobCategory, + JobMode, + JobPayFreq, + JobType, + University, +} from "./db.types"; + +interface RefDomain { + id: string; + name: string; + university_id: string; +} + +interface RefsData { + colleges: College[]; + departments: Department[]; + universities: University[]; + job_types: JobType[]; + job_modes: JobMode[]; + job_allowances: JobAllowance[]; + job_categories: JobCategory[]; + job_pay_freq: JobPayFreq[]; + app_statuses: AppStatus[]; + industries: Industry[]; + domains: RefDomain[]; +} + +interface IRefsContext extends RefsData { + ref_loading: boolean; + get_college: (id: string | null | undefined) => College | null; + to_college_name: ( + id: string | null | undefined, + def?: string | null, + ) => string | null; + get_college_by_name: (name: string | null | undefined) => College | null; + get_university: (id: string | null | undefined) => University | null; + to_university_name: ( + id: string | null | undefined, + def?: string | null, + ) => string | null; + get_university_by_name: ( + name: string | null | undefined, + ) => University | null; + get_job_type: (id: number | null | undefined) => JobType | null; + to_job_type_name: ( + id: number | null | undefined, + def?: string | null, + ) => string | null; + get_job_type_by_name: (name: string | null | undefined) => JobType | null; + get_job_mode: (id: number | null | undefined) => JobMode | null; + to_job_mode_name: ( + id: number | null | undefined, + def?: string | null, + ) => string | null; + get_job_mode_by_name: (name: string | null | undefined) => JobMode | null; + get_job_allowance: (id: number | null | undefined) => JobAllowance | null; + to_job_allowance_name: ( + id: number | null | undefined, + def?: string | null, + ) => string | null; + get_job_allowance_by_name: ( + name: string | null | undefined, + ) => JobAllowance | null; + get_job_pay_freq: (id: number | null | undefined) => JobPayFreq | null; + to_job_pay_freq_name: ( + id: number | null | undefined, + def?: string | null, + ) => string | null; + get_job_pay_freq_by_name: ( + name: string | null | undefined, + ) => JobPayFreq | null; + get_app_status: (id: number | null | undefined) => AppStatus | null; + to_app_status_name: ( + id: number | null | undefined, + def?: string | null, + ) => string | null; + get_app_status_by_name: (name: string | null | undefined) => AppStatus | null; + get_industry: (id: string | null | undefined) => Industry | null; + to_industry_name: ( + id: string | null | undefined, + def?: string | null, + ) => string | null; + get_industry_by_name: (name: string | null | undefined) => Industry | null; + get_job_category: (id: string | null | undefined) => JobCategory | null; + to_job_category_name: ( + id: string | null | undefined, + def?: string | null, + ) => string | null; + get_job_category_by_name: ( + name: string | null | undefined, + ) => JobCategory | null; + get_department: (id: string | null | undefined) => Department | null; + to_department_name: ( + id: string | null | undefined, + def?: string | null, + ) => string | null; + get_department_by_name: ( + name: string | null | undefined, + ) => Department | null; + get_departments_by_college: (college_id: string) => string[]; + get_colleges_by_university: (university_id: string) => string[]; + getUniversityFromDomain: (domain: string) => string[]; + isNotNull: (ref: any) => boolean; +} // The context template const RefsContext = createContext({} as IRefsContext); @@ -21,21 +130,144 @@ const RefsContext = createContext({} as IRefsContext); * @component */ export const RefsContextProvider = ({ + data, children, }: { + data?: RefsData; children: React.ReactNode; }) => { - const refs_context = createRefsContext(); + const refsContext = createRefsContext(data ?? emptyRefsData); return ( - {children} + {children} ); }; /** - * Allows using the refs table we have in supabase as a hook. + * Allows using the refs table. * * @hook */ export const useDbRefs = (): IRefsContext => { return useContext(RefsContext); }; + +const createRefHelpers = < + ID extends string | number, + T extends { id: ID; name: string }, +>( + data: T[], +) => { + const get = (id: ID | null | undefined): T | null => { + if (!id && id !== 0) return null; + const found = data.find((d) => d.id === id); + return found ?? null; + }; + + const toName = ( + id: ID | null | undefined, + def: string | null | undefined = "Not specified", + ): string => { + if (!id && id !== 0) return def ?? ""; + const found = data.find((d) => d.id === id); + return found?.name ?? def ?? ""; + }; + + const getByName = (name: string | null | undefined): T | null => { + if (!name) return null; + const found = data.find((d) => d.name === name); + return found ?? null; + }; + + return { + get, + toName, + getByName, + }; +}; + +const createRefsContext = (data: RefsData): IRefsContext => { + const collegeHelpers = createRefHelpers(data.colleges); + const universityHelpers = createRefHelpers( + data.universities, + ); + const jobTypeHelpers = createRefHelpers(data.job_types); + const jobModeHelpers = createRefHelpers(data.job_modes); + const jobAllowanceHelpers = createRefHelpers( + data.job_allowances, + ); + const jobPayFreqHelpers = createRefHelpers( + data.job_pay_freq, + ); + const appStatusHelpers = createRefHelpers( + data.app_statuses, + ); + const industryHelpers = createRefHelpers(data.industries); + const jobCategoryHelpers = createRefHelpers( + data.job_categories, + ); + const departmentHelpers = createRefHelpers( + data.departments, + ); + + return { + ref_loading: false, + ...data, + get_college: collegeHelpers.get, + to_college_name: collegeHelpers.toName, + get_college_by_name: collegeHelpers.getByName, + get_university: universityHelpers.get, + to_university_name: universityHelpers.toName, + get_university_by_name: universityHelpers.getByName, + get_job_type: jobTypeHelpers.get, + to_job_type_name: jobTypeHelpers.toName, + get_job_type_by_name: jobTypeHelpers.getByName, + get_job_mode: jobModeHelpers.get, + to_job_mode_name: jobModeHelpers.toName, + get_job_mode_by_name: jobModeHelpers.getByName, + get_job_allowance: jobAllowanceHelpers.get, + to_job_allowance_name: jobAllowanceHelpers.toName, + get_job_allowance_by_name: jobAllowanceHelpers.getByName, + get_job_pay_freq: jobPayFreqHelpers.get, + to_job_pay_freq_name: jobPayFreqHelpers.toName, + get_job_pay_freq_by_name: jobPayFreqHelpers.getByName, + get_app_status: appStatusHelpers.get, + to_app_status_name: appStatusHelpers.toName, + get_app_status_by_name: appStatusHelpers.getByName, + get_industry: industryHelpers.get, + to_industry_name: industryHelpers.toName, + get_industry_by_name: industryHelpers.getByName, + get_job_category: jobCategoryHelpers.get, + to_job_category_name: jobCategoryHelpers.toName, + get_job_category_by_name: jobCategoryHelpers.getByName, + get_department: departmentHelpers.get, + to_department_name: departmentHelpers.toName, + get_department_by_name: departmentHelpers.getByName, + get_departments_by_college: (collegeId: string) => + data.departments + .filter((d) => d.college_id === collegeId) + .map((d) => d.id), + get_colleges_by_university: (universityId: string) => + data.colleges + .filter((c) => c.university_id === universityId) + .map((c) => c.id), + getUniversityFromDomain: (domain: string) => + data.domains + .filter((d: RefDomain) => d.name === domain) + .map((d: RefDomain) => d.university_id), + isNotNull: (ref: any) => !!(ref || ref === 0), + }; +}; + +const emptyRefsData: RefsData = { + colleges: [], + departments: [], + universities: [], + job_types: [], + job_modes: [], + job_allowances: [], + job_categories: [], + job_pay_freq: [], + app_statuses: [], + industries: [], + domains: [], +}; diff --git a/package.json b/package.json index 6d3d85c2..1663f12a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "@betterinternship/components": "1.5.28", "@betterinternship/core": "^2.8.10", - "@betterinternship/schema.base": "2.1.2", + "@betterinternship/schema.base": "3.0.1", "@betterinternship/schema.moa": "^1.5.15", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -51,7 +51,6 @@ "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.2.8", "@react-native-async-storage/async-storage": "^2.2.0", - "@supabase/supabase-js": "^2.50.0", "@tailwindcss/typography": "^0.5.16", "@tanstack/query-async-storage-persister": "^5.81.5", "@tanstack/react-query": "^5.81.5", @@ -76,12 +75,14 @@ "jspdf-autotable": "^5.0.2", "jszip": "^3.10.1", "knuth-shuffle-seeded": "^1.0.6", + "kysely": "^0.28.16", "lenis": "^1.3.21", "lucide-react": "^0.454.0", "luxon": "^3.6.1", "next": "^15.2.6", "next-themes": "^0.4.4", "number-to-words": "^1.2.4", + "pg": "^8.20.0", "pocketbase": "^0.26.1", "posthog-js": "^1.255.1", "qrcode.react": "^4.2.0", @@ -118,6 +119,7 @@ "@types/knuth-shuffle-seeded": "^1.0.2", "@types/node": "^22", "@types/number-to-words": "^1.2.3", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "cypress": "^15.6.0", From 664d2bfc81be7aedb544cd6e52dce97fc368b080 Mon Sep 17 00:00:00 2001 From: Jana Marie Bantolino Date: Sun, 26 Apr 2026 17:38:53 +0800 Subject: [PATCH 002/143] chore: fix out copywrite of PCC --- app/hire/dashboard/applicant/page.tsx | 2 - .../pcc/components/OverviewPanel.tsx | 88 ++++++++++++++++--- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/app/hire/dashboard/applicant/page.tsx b/app/hire/dashboard/applicant/page.tsx index 528359c7..58f16505 100644 --- a/app/hire/dashboard/applicant/page.tsx +++ b/app/hire/dashboard/applicant/page.tsx @@ -15,7 +15,6 @@ import useApplicationActions from "@/hooks/use-application-actions"; function ApplicantPageContent() { const searchParams = useSearchParams(); const userId = searchParams.get("userId"); - const jobId = searchParams.get("jobId"); const isDummyProfile = searchParams.get("dummy") === "1"; const [loading, setLoading] = useState(true); const applications = useEmployerApplications(); @@ -25,7 +24,6 @@ function ApplicantPageContent() { const dummyApplication: EmployerApplication = { id: "dummy-super-application", - user_id: "", job_id: jobId ?? "dummy-super-job", status: 0, applied_at: "2026-03-09T00:00:00.000Z", diff --git a/app/student/super-listing/pcc/components/OverviewPanel.tsx b/app/student/super-listing/pcc/components/OverviewPanel.tsx index ec8549cf..47a40e92 100644 --- a/app/student/super-listing/pcc/components/OverviewPanel.tsx +++ b/app/student/super-listing/pcc/components/OverviewPanel.tsx @@ -47,13 +47,6 @@ const HOW_TO_APPLY_STEPS = [ "Submit one clear link and your details. Include a short walkthrough showing how your flow reduces friction from signup to introduction.", ]; -const INTERNSHIP_OVERVIEW_PARAGRAPHS = [ - "This internship places you inside a real PCC ecosystem problem where member data exists but network value is still hard to unlock at speed.", - "Each company should have a meaningful profile, be discoverable by practical criteria, and be routed toward relevant members without relying on manual back-and-forth.", - "Working on PCC gives you real ecosystem exposure: you solve a live business-network problem, design for multi-stakeholder behavior, and ship work that can directly improve how companies find opportunities through the chamber.", - "Your system should reduce onboarding friction, improve profile quality, and speed up partner discovery through clear group-based pathways and incentives.", -]; - const INTERNSHIP_OKRS = [ "A company can complete onboarding and profile setup quickly in one clear flow.", "Members can identify and request relevant introductions without slow manual routing.", @@ -91,9 +84,84 @@ export function OverviewPanel({
- {INTERNSHIP_OVERVIEW_PARAGRAPHS.map((paragraph) => ( -

{paragraph}

- ))} +

This internship will be like no other.

+

+ You are going to build a platform that will solve{" "} + one of PCCI's biggest problems. +

+

+ It's like LinkedIn, but localized for Philippine + businesses. Your initial users will be us, PCCI, the{" "} + + largest business organisation in the Philippines with 80,000 + member companies. + +

+

+ And yes, this sounds like too big of a project to give to an + intern, but we're doing it anyway. +

+

+ Most internships give you grunt work. Here, you'll be + given a lot of resources, freedom, and mentorship to bring this + dream to reality. You'll be given tasks that you are not + qualified to do, but that means{" "} + you'll grow faster. +

+

+ This project is the perfect opportunity to give you industry + exposure. Not only are you shipping a solution to a live problem + that involves real business owners, but you are also placed + within close proximity to the businesses involved. +

+

+ By the end of it, you'll have a project you can show, and + a story that will make any employer want to hire you. +

+

+ And BTW,{" "} + we don't look at grades or resumes. Check + the challenge for more details. +

+
+

+ Project details +

+
+

+ PCCI has 80,000 businesses in its network.{" "} + But it's hard for these businesses to talk to each + other. +

+

+ Businesses like these usually try to connect with each other + to find the best partnerships that optimize their line of + work. +

+

+ But in a sea of thousands of members with no centralized + contact list, it's almost impossible for businesses to + find each other in this community. +

+

+ This internship involves working closely with members of + PCCI and their data. The goal is to create a platform that + makes it easier for businesses to find each other, and to + network when they do. +

+

+ But most important of all,{" "} + + the platform should incentivize businesses to connect. + {" "} + Sometimes, businesses within PCCI aren't even held + back by the lack of a platform: some businesses just prefer + not to respond to requests because they're used to + their usual sets of connections and don't bother + looking for more. +

+
+

What Success Looks Like From b44f315bfac5abbd88f7d4f6ee3d75625613f700 Mon Sep 17 00:00:00 2001 From: Jana Marie Bantolino Date: Sun, 26 Apr 2026 19:07:10 +0800 Subject: [PATCH 003/143] feat: sofi ai company profile + super listing --- app/student/allowLanding.tsx | 7 +- app/student/companies/sofi-ai/data.ts | 148 +++ app/student/companies/sofi-ai/page.tsx | 939 ++++++++++++++++++ app/student/super-listing/og-config.ts | 7 + .../sofi-ai/components/ApplyPanel.tsx | 344 +++++++ .../sofi-ai/components/HeroPanel.tsx | 33 + .../sofi-ai/components/HowToApplyPanel.tsx | 153 +++ .../sofi-ai/components/JobDetailsRail.tsx | 30 + .../sofi-ai/components/OverviewPanel.tsx | 243 +++++ .../super-listing/sofi-ai/components/types.ts | 24 + app/student/super-listing/sofi-ai/layout.tsx | 22 + app/student/super-listing/sofi-ai/logo.png | Bin 0 -> 28555 bytes app/student/super-listing/sofi-ai/page.tsx | 411 ++++++++ 13 files changed, 2356 insertions(+), 5 deletions(-) create mode 100644 app/student/companies/sofi-ai/data.ts create mode 100644 app/student/companies/sofi-ai/page.tsx create mode 100644 app/student/super-listing/sofi-ai/components/ApplyPanel.tsx create mode 100644 app/student/super-listing/sofi-ai/components/HeroPanel.tsx create mode 100644 app/student/super-listing/sofi-ai/components/HowToApplyPanel.tsx create mode 100644 app/student/super-listing/sofi-ai/components/JobDetailsRail.tsx create mode 100644 app/student/super-listing/sofi-ai/components/OverviewPanel.tsx create mode 100644 app/student/super-listing/sofi-ai/components/types.ts create mode 100644 app/student/super-listing/sofi-ai/layout.tsx create mode 100644 app/student/super-listing/sofi-ai/logo.png create mode 100644 app/student/super-listing/sofi-ai/page.tsx diff --git a/app/student/allowLanding.tsx b/app/student/allowLanding.tsx index 3765db0d..3db85152 100644 --- a/app/student/allowLanding.tsx +++ b/app/student/allowLanding.tsx @@ -3,6 +3,7 @@ import { usePathname } from "next/navigation"; import Header from "@/components/features/student/header"; import { Suspense } from "react"; +import { startsWith } from "zod"; export default function AllowLanding({ children, @@ -12,11 +13,7 @@ export default function AllowLanding({ const pathname = usePathname(); const isStudentLanding = pathname === "/"; const hideSharedHeader = - isStudentLanding || - pathname === "/companies/cebu-pacific" || - pathname === "/student/companies/cebu-pacific" || - pathname === "/companies/pcc" || - pathname === "/student/companies/pcc"; + isStudentLanding || startsWith(pathname, "/companies/"); return (

diff --git a/app/student/companies/sofi-ai/data.ts b/app/student/companies/sofi-ai/data.ts new file mode 100644 index 00000000..d0424d9f --- /dev/null +++ b/app/student/companies/sofi-ai/data.ts @@ -0,0 +1,148 @@ +import type { Job } from "@/lib/db/db.types"; + +export type CompanySection = { + eyebrow: string; + title: string; + body: string; +}; + +export type CompanyTestimonial = { + quote: string; + name: string; + role: string; +}; + +export type CompanyDetailItem = { + label: string; + value: string; +}; + +export type SofiAiProfile = { + slug: string; + name: string; + websiteUrl: string; + location: string; + headline: string; + subheadline: string; + rotatingPhrases: string[]; + heroStats: Array<{ + label: string; + value: string; + }>; + about: CompanySection; + internCulture: CompanySection; + testimonials: CompanyTestimonial[]; + jobDetails: CompanyDetailItem[]; + roleOverview: string[]; + whySkipResume: string[]; + listings: { + super: Job[]; + normal: Job[]; + }; +}; + +export const sofiAiPrimaryListing: Job = { + id: "pilot-sofi-ai-super-listing", + title: "Frontend Product Engineering Intern", + location: "Metro Manila / Hybrid", + description: + "Build product interfaces for a fast-growing applied AI startup, starting with a frontend for a TikTok hook-analysis backend.", + employer: { + name: "Sofi AI", + }, + challenge: { + title: "Open Challenge", + description: + "Build a polished interface for analyzing TikTok hooks, showing scores, retention risks, rewrite suggestions, and comparison states.", + }, + internship_preferences: { + job_setup_ids: [], + job_commitment_ids: [], + }, +}; + +export const sofiAiNormalListing: Job = { + id: "pilot-sofi-ai-normal-listing", + title: "AI Product Operations Intern", + location: "Metro Manila / Hybrid", + description: + "Support customer automation workflows, product testing, and internal systems for a fast-growing applied AI startup.", + employer: { + name: "Sofi AI", + }, + internship_preferences: { + job_setup_ids: [], + job_commitment_ids: [], + }, +}; + +export const sofiAiProfile: SofiAiProfile = { + slug: "sofi-ai", + name: "Sofi AI", + websiteUrl: "https://sofitech.ai/", + location: "Metro Manila, Philippines", + headline: "Build AI products businesses actually use.", + subheadline: + "Sofi AI is a fast-growing applied AI startup building tools that help businesses automate customer interactions, streamline operations, and scale with practical real-world AI.", + rotatingPhrases: [ + "for customer conversations", + "for business automation", + "for real AI products", + ], + heroStats: [ + { label: "Platform reach", value: "Millions of users" }, + { label: "Business model", value: "Revenue-generating" }, + { label: "Startup programs", value: "Google + NVIDIA" }, + ], + about: { + eyebrow: "About Sofi AI", + title: "Applied AI with real customers, real traction, and real pressure.", + body: "Sofi AI, also known as Sofitech AI, builds AI assistants, customer support automation, and workflow tools that help businesses automate customer interactions and streamline operations. The company is already operating with millions of users across its platforms, consistent revenue generation, and recognition from global startup programs like Google for Startups and NVIDIA.", + }, + internCulture: { + eyebrow: "Founder-led Culture", + title: "Move fast, build in public, and focus on execution.", + body: "Sofi AI is led by Sophia Nicole Sy, a young Filipino founder known for building in public and actively sharing her journey in tech and startups. Her leadership gives the company a strong identity: transparent, hands-on, active in the Women in Tech and startup community, and focused on building things that actually work in the real world.", + }, + testimonials: [ + { + quote: + "The team moves quickly and expects you to make the product clearer every week, not just complete tasks.", + name: "Mika T.", + role: "Former Product Intern", + }, + { + quote: + "Working close to a founder taught me how much execution matters. Good ideas only count when users can actually use them.", + name: "Andre L.", + role: "Former Startup Intern", + }, + { + quote: + "The feedback loop was direct, practical, and fast. I learned to design for business outcomes, not just screens.", + name: "Patricia C.", + role: "Former Product Design Intern", + }, + ], + jobDetails: [ + { label: "Work setup", value: "Hybrid collaboration" }, + { label: "Location", value: "Metro Manila" }, + ], + roleOverview: [ + "This role is for builders who want to work inside a traction-driven AI startup, not a simulated school project.", + "The challenge-first format helps Sofi AI evaluate product taste, interface judgment, and execution better than resume-only screening.", + "Strong applicants show they can turn AI capabilities into clear, useful interfaces that real businesses and users can understand.", + ], + whySkipResume: [ + "A resume can say you know frontend development, but it does not show whether you can make AI usable for real customers.", + "Sofi AI prioritizes demonstrated product thinking: clean flows, useful states, strong copy, and interfaces that make applied AI feel simple.", + ], + listings: { + super: [sofiAiPrimaryListing], + normal: [sofiAiNormalListing], + }, +}; + + + + diff --git a/app/student/companies/sofi-ai/page.tsx b/app/student/companies/sofi-ai/page.tsx new file mode 100644 index 00000000..d242334a --- /dev/null +++ b/app/student/companies/sofi-ai/page.tsx @@ -0,0 +1,939 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { JetBrains_Mono, Open_Sans, Space_Grotesk } from "next/font/google"; +import { + useEffect, + useLayoutEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { motion, useReducedMotion, type Variants } from "framer-motion"; +import { ArrowRight, ArrowUpRight } from "lucide-react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { Button } from "@/components/ui/button"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import useModalRegistry from "@/components/modals/modal-registry"; +import { cn } from "@/lib/utils"; +import { sofiAiPrimaryListing, sofiAiProfile } from "./data"; + +const headingFont = Space_Grotesk({ + subsets: ["latin"], + weight: ["500", "700"], + variable: "--font-paraluman-heading", +}); + +const monoFont = JetBrains_Mono({ + subsets: ["latin"], + weight: ["400", "600"], + variable: "--font-paraluman-mono", +}); + +const bodyFont = Open_Sans({ + subsets: ["latin"], + weight: ["400", "600", "700"], + variable: "--font-paraluman-body", +}); + +const TEXT_GUTTER = "px-6 sm:px-10 lg:px-16 xl:px-24"; +const FEATURE_HEADING_CLASS = + "[font-family:var(--font-paraluman-heading)] text-[clamp(1.95rem,3.6vw,3.15rem)] font-black leading-[0.96] tracking-[-0.055em]"; +const BODY_COPY_CLASS = + "[font-family:var(--font-paraluman-body)] max-w-[60ch] text-base leading-7 text-[#184d45]/82 sm:text-lg sm:leading-[1.72]"; +const SECTION_REVEAL_VARIANTS: Variants = { + hidden: { opacity: 0, y: 28 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.62, ease: [0.22, 1, 0.36, 1] }, + }, +}; +const STAGGER_CONTAINER_VARIANTS: Variants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.13, delayChildren: 0.04 }, + }, +}; +const STAGGER_ITEM_VARIANTS: Variants = { + hidden: { opacity: 0, y: 22 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] }, + }, +}; +type InViewMotionProps = { + initial?: "hidden"; + whileInView?: "visible"; + viewport?: { once: true; amount: number }; +}; +const SOFI_AI_LOGO_URL = + "https://sofitech.ai/_next/static/media/sofi-ai-chat-support-automation-logo-vector.80ec9e4e.png"; +const SOFI_AI_HERO_VIDEO_URL = + "https://www.sofitech.ai/videos/landscape_english_wcaption.mp4"; +const FOUNDER_PROFILE = { + name: "Sophia Nicole Sy", + role: "Founder of Sofi AI\nYoung Filipino builder leading a real, revenue-generating AI startup focused on practical execution.", + profileUrl: "https://www.linkedin.com/in/sophia-nicole-sy/", + image: + "https://media.licdn.com/dms/image/v2/D5603AQHOdvGO-2aSBg/profile-displayphoto-shrink_400_400/B56ZW3MvE4GoAk-/0/1742535326021?e=1778716800&v=beta&t=alkNMb4zoeKaxYFqFDC2jwRqMM2zFwmRF2SLl0oIPpw", +}; +const LISTING_CARDS = [ + { + id: "core", + title: sofiAiPrimaryListing.title, + summary: + "Build practical frontend experiences for Sofi AI, starting with a TikTok hook-analysis product interface", + metrics: [ + "Support link, caption, script, and hook-text inputs", + "Show scores, retention risk, clarity, niche fit, and rewrites", + "Deliver a product-ready flow with loading, empty, failed, and comparison states", + ], + supporting: + "Before internship onboarding, you complete a challenge that mirrors the kind of applied AI product work Sofi AI ships.", + accent: "#07C4A7", + }, + { + id: "digital", + eyebrow: "Product Track", + title: "AI Product Operations Intern", + summary: + "Support product testing, customer automation workflows, and operating systems for a fast-growing applied AI team.", + metrics: [ + "Clarify one customer-facing workflow", + "Improve a repetitive AI-assisted process", + "Ship one measurable product or operations improvement", + ], + supporting: + "You will work close to real product and customer workflows, not simulated tasks.", + accent: "#35e3ca", + }, + { + id: "ops", + eyebrow: "Growth Track", + title: "Startup Growth Intern", + summary: + "Help translate Sofi AI's founder-led momentum, product wins, and customer outcomes into clearer growth systems.", + metrics: [ + "Map one growth or onboarding funnel", + "Improve customer-facing messaging clarity", + "Produce one implementation-ready growth experiment", + ], + supporting: + "Your scope focuses on practical outputs that help the team move faster.", + accent: "#8cf5e4", + }, +] as const; + +function getInViewMotionProps( + reduceMotion: boolean, + amount: number, +): InViewMotionProps { + if (reduceMotion) return {}; + return { + initial: "hidden", + whileInView: "visible", + viewport: { once: true, amount }, + }; +} + +function RevealBlock({ + children, + className, + variants = SECTION_REVEAL_VARIANTS, + inView, +}: { + children: ReactNode; + className?: string; + variants?: Variants; + inView: InViewMotionProps; +}) { + return ( + + {children} + + ); +} + +function SectionShell({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +function SectionInner({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return
{children}
; +} + +function MagneticButton({ + children, + className = "", +}: { + children: ReactNode; + className?: string; +}) { + const prefersReduce = useReducedMotion(); + const ref = useRef(null); + const [tx, setTx] = useState(0); + const [ty, setTy] = useState(0); + + const max = 6; + + const onMove = (e: React.MouseEvent) => { + if (prefersReduce) return; + const el = ref.current; + if (!el) return; + const r = el.getBoundingClientRect(); + const x = (e.clientX - r.left) / r.width - 0.5; + const y = (e.clientY - r.top) / r.height - 0.5; + setTx(x * max * 2); + setTy(y * max * 2); + }; + + const onLeave = () => { + setTx(0); + setTy(0); + }; + + return ( + + {children} + + ); +} + +function ListingsCTA({ + onClick, + className, + label = "See listings", + size = "default", +}: { + onClick: () => void; + className?: string; + label?: string; + size?: "default" | "hero"; +}) { + return ( + + + + ); +} + +function HeroMainContent({ + reduceMotion, + onJumpToListings, +}: { + reduceMotion: boolean; + onJumpToListings: () => void; +}) { + return ( + + + + Sofi AI logo + + + +

+ + + Build the AI Workflows Companies Will Run On. + + +

+ + + + +
+ ); +} + +function MeaningfulWorkScrollScene({ + reduceMotion, + onJumpToListings, +}: { + reduceMotion: boolean; + onJumpToListings: () => void; +}) { + const getScrollParent = (node: HTMLElement | null): HTMLElement | Window => { + let parent = node?.parentElement ?? null; + while (parent) { + const style = window.getComputedStyle(parent); + const overflowY = style.overflowY; + if ( + /(auto|scroll|overlay)/.test(overflowY) && + parent.scrollHeight > parent.clientHeight + 1 + ) { + return parent; + } + parent = parent.parentElement; + } + return window; + }; + + const sectionRef = useRef(null); + const eyebrowRef = useRef(null); + const lineOneRef = useRef(null); + const lineTwoRef = useRef(null); + const lineThreeRef = useRef(null); + const ctaRef = useRef(null); + + useLayoutEffect(() => { + if (reduceMotion) return; + if (!sectionRef.current) return; + + gsap.registerPlugin(ScrollTrigger); + + const ctx = gsap.context(() => { + const scroller = getScrollParent(sectionRef.current); + const triggerScroller = + scroller === window ? undefined : (scroller as HTMLElement); + + const targets = [ + eyebrowRef.current, + lineOneRef.current, + lineTwoRef.current, + lineThreeRef.current, + ].filter(Boolean); + + gsap.set(targets, { autoAlpha: 0, y: 24 }); + if (ctaRef.current) { + gsap.set(ctaRef.current, { + autoAlpha: 0, + y: 20, + scale: 0.96, + }); + } + + const sharedTimeline = gsap.timeline({ + scrollTrigger: { + trigger: sectionRef.current, + scroller: triggerScroller, + start: "top 82%", + end: "bottom 38%", + toggleActions: "play none none reverse", + invalidateOnRefresh: true, + }, + defaults: { + ease: "power2.out", + duration: 0.72, + }, + }); + + if (eyebrowRef.current) { + sharedTimeline.to(eyebrowRef.current, { autoAlpha: 0.65, y: 0 }, 0); + } + if (lineOneRef.current) { + sharedTimeline.to(lineOneRef.current, { autoAlpha: 1, y: 0 }, 0.16); + } + if (lineTwoRef.current) { + sharedTimeline.to(lineTwoRef.current, { autoAlpha: 1, y: 0 }, 0.38); + } + if (lineThreeRef.current) { + sharedTimeline.to(lineThreeRef.current, { autoAlpha: 1, y: 0 }, 0.62); + } + if (ctaRef.current) { + sharedTimeline.to( + ctaRef.current, + { autoAlpha: 1, y: 0, scale: 1 }, + 0.88, + ); + } + + ScrollTrigger.refresh(); + }, sectionRef); + + return () => { + ctx.revert(); + }; + }, [reduceMotion]); + + return ( +
+
+
+

+ Imagine an internship where... +

+ +
+

+ You work the way you want +

+

+ Your skills matter more + than experience +

+

+ You do work that matters. + No grunt work +

+
+ +
+ +
+
+
+
+ ); +} + +function HeroPanel({ + reduceMotion, + onJumpToListings, +}: { + reduceMotion: boolean; + onJumpToListings: () => void; +}) { + const sharedHeroBackground = + "pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_14%_18%,rgba(7,196,167,0.22),transparent_24%),radial-gradient(circle_at_78%_76%,rgba(7,196,167,0.1),transparent_28%)]"; + const sharedHeroBottomFade = + "pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-[linear-gradient(180deg,rgba(255,255,255,0)_0%,rgba(231,255,250,0.8)_100%)]"; + + return ( +
+
+ + BetterInternship + +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ ); +} + +function WorkWithFounder() { + return ( +
+

+ Work with +

+
+
+
+ Sofia Nicole Sy portrait +
+ +
+ + + {FOUNDER_PROFILE.name} + + + +

+ {FOUNDER_PROFILE.role} +

+

+ Sophia is known for building in public, sharing the startup + journey, and leading Sofi AI with a bias toward products that + actually work for real users. +

+
+
+
+
+ ); +} + +function ListingCard({ + card, + onSelect, +}: { + card: (typeof LISTING_CARDS)[number]; + onSelect: () => void; +}) { + const isPrimary = card.id === "core"; + + return ( + +
+
+
+ +
+
+
+
+ +
+
+

+ {card.eyebrow} +

+

+ {card.title} +

+
+ +

+ {card.summary} +

+
+ +
+ View this role + +
+ + ); +} + +function ListingModalContent({ + card, + onApply, +}: { + card: (typeof LISTING_CARDS)[number]; + onApply: () => void; +}) { + return ( +
+
+

+ Your internship shall: +

+

+ Build a practical frontend for TikTok hook analysis that turns AI + output into clear product decisions +

+
+ +
+

+ Your internship is a success if you can: +

+
+
+ +

+ Let users submit a TikTok link, caption, script, or hook text in + one clear flow +

+
+
+ +

+ Show hook score, retention risk, clarity, emotional pull, niche + fit, and suggested rewrites +

+
+
+ +

+ Include loading, empty, failed, and original-versus-improved + comparison states +

+
+
+
+ +
+

+ Exciting? But before you can start the internship, you need to pass + our challenge. +

+
+ +
+ +
+
+ ); +} +export default function SofiAiCompanyProfilePage() { + const shouldReduceMotion = useReducedMotion(); + const sectionRevealMotion = getInViewMotionProps(shouldReduceMotion, 0.24); + const sectionStaggerMotion = getInViewMotionProps(shouldReduceMotion, 0.18); + const modalRegistry = useModalRegistry(); + const listingsRef = useRef(null); + + const scrollToListings = () => { + listingsRef.current?.scrollIntoView({ + behavior: shouldReduceMotion ? "auto" : "smooth", + block: "start", + }); + }; + + return ( +
+
+
+ +
+ +
+ + +
+ +
+ + + + + + + + + + +
+
+
+ + +

+ Better internships start here. +

+
+ + + { + modalRegistry.centeredDetails.open({ + title: ( + + {LISTING_CARDS[0].title} + + ), + content: ( + modalRegistry.centeredDetails.close()} + /> + ), + showHeaderDivider: false, + closeOnBackdropClick: true, + closeOnEscapeKey: true, + showCloseButton: true, + }); + }} + /> + + +
+ + + + + +

+ FAQs +

+
+ + + + + + Do I need a resume to apply? + + + No. Sofi AI reviews your challenge output first. Product + judgment, practicality, and execution matter most. + + + + + + How fast will I hear back? + + + Our target is to respond within 24 hours so strong + applicants can move forward quickly. + + + + + + What kind of work will interns do? + + + Real product and startup work. You'll help turn AI + output into interfaces, workflows, and decisions that real + businesses can use. + + + + + + Which listings should I apply to? + + + Choose the role where your skills are strongest, then submit + a high-quality challenge response for that listing. + + + + +
+
+
+
+ ); +} diff --git a/app/student/super-listing/og-config.ts b/app/student/super-listing/og-config.ts index 488a1ba3..d8e93d28 100644 --- a/app/student/super-listing/og-config.ts +++ b/app/student/super-listing/og-config.ts @@ -46,6 +46,13 @@ const SUPER_LISTING_OG_OVERRIDES: Record< accent: "#0f766e", glow: "#34d399", }, + "sofi-ai": { + company: "Sofi AI", + role: "Frontend AI Product Challenge", + tagline: "Build a practical frontend for TikTok hook analysis.", + accent: "#07C4A7", + glow: "#35e3ca", + }, paraluman: { company: "Paraluman News", role: "Multilingual News Delivery Challenge", diff --git a/app/student/super-listing/sofi-ai/components/ApplyPanel.tsx b/app/student/super-listing/sofi-ai/components/ApplyPanel.tsx new file mode 100644 index 00000000..6aa97961 --- /dev/null +++ b/app/student/super-listing/sofi-ai/components/ApplyPanel.tsx @@ -0,0 +1,344 @@ +"use client"; + +import { type ChangeEvent, type FormEvent, useEffect, useRef } from "react"; +import { + ArrowLeft, + FileText, + FolderOpen, + Globe, + Loader2, + Video, +} from "lucide-react"; +import { Turnstile } from "@marsidev/react-turnstile"; +import confetti from "canvas-confetti"; +import { motion, useReducedMotion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Loader } from "@/components/ui/loader"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { SofiAiSubmissionForm, SubmissionStep } from "./types"; + +type ApplyPanelProps = { + form: SofiAiSubmissionForm; + submissionStep: SubmissionStep; + hasSubmitted: boolean; + submittedEmail: string; + isSubmitting: boolean; + isError: boolean; + resultMessage: string; + isDevelopment: boolean; + token: string; + tokenFail: boolean; + turnstileSiteKey?: string; + onFieldChange: (field: keyof SofiAiSubmissionForm, value: string) => void; + onNextStep: () => void; + onBackStep: () => void; + onSubmit: (event: FormEvent) => void; + onBackToOverview: () => void; + onTokenSuccess: (token: string) => void; + onTokenError: () => void; +}; + +export function ApplyPanel({ + form, + submissionStep, + hasSubmitted, + submittedEmail, + isSubmitting, + isError, + resultMessage, + isDevelopment, + token, + tokenFail, + turnstileSiteKey, + onFieldChange, + onNextStep, + onBackStep, + onSubmit, + onBackToOverview, + onTokenSuccess, + onTokenError, +}: ApplyPanelProps) { + const prefersReduce = useReducedMotion(); + const hasCelebratedRef = useRef(false); + + useEffect(() => { + if (!hasSubmitted) { + hasCelebratedRef.current = false; + return; + } + + if (hasCelebratedRef.current || prefersReduce) return; + if (typeof window === "undefined") return; + + hasCelebratedRef.current = true; + + type ConfettiFn = ( + options?: Record, + ) => Promise | null; + const fireConfetti = confetti as unknown as ConfettiFn; + + void fireConfetti({ + particleCount: 90, + spread: 74, + startVelocity: 34, + origin: { y: 0.65 }, + colors: ["#07C4A7", "#35e3ca", "#8cf5e4", "#ffffff"], + }); + + window.setTimeout(() => { + void fireConfetti({ + particleCount: 60, + spread: 60, + startVelocity: 28, + origin: { x: 0.75, y: 0.68 }, + colors: ["#07C4A7", "#8cf5e4", "#ffffff"], + }); + }, 180); + }, [hasSubmitted, prefersReduce]); + + const updateField = + (field: keyof SofiAiSubmissionForm) => + ( + event: ChangeEvent | ChangeEvent, + ) => { + onFieldChange(field, event.target.value); + }; + + return ( +
+
+

+ Submit your challenge output. +

+

+ Again,{" "} + + no resume needed. Response within 24 hours. + +

+
+ +
+
+ {hasSubmitted ? ( + +
+

+ Submission sent +

+

+ You're in. +

+

+ Thank you for applying. We sent a confirmation to{" "} + + {submittedEmail || "your email"} + + . You will receive a response within 24 hours. +

+
+ +
+
+
+ ) : ( +
void onSubmit(e)}> +

+ {submissionStep === 1 + ? "Step 1/2: Paste the single best link to your prototype, video, or document." + : "Step 2/2: Add your contact details so the team can review and reply quickly."} +

+ + {submissionStep === 1 && ( + <> +
+ + +
+ Accepted Links: + + + Google Drive, + + + + Google Docs, + + + + Live Demo, + + + +
+
+ +
+ +