From 0b3f84d8360e9563017997a5387a1bec8b0465a7 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Tue, 17 Feb 2026 04:39:21 +0800 Subject: [PATCH 01/97] patch: package update + client for rabbitmq tasks --- app/student/forms/[name]/page.tsx | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/student/forms/[name]/page.tsx b/app/student/forms/[name]/page.tsx index 4ccb2d94..b2b7fa0c 100644 --- a/app/student/forms/[name]/page.tsx +++ b/app/student/forms/[name]/page.tsx @@ -6,6 +6,14 @@ import { useFormsLayout } from "../layout"; import { useParams } from "next/navigation"; import { useEffect } from "react"; import { toast } from "sonner"; +import { + config, + configure, + useAsyncProcess, +} from "@betterinternship/components"; + +// ! move this to the topmost client component you can find +configure({ orchestratorApi: "https://orca.betterinternship.com/process" }); /** * The individual form page. @@ -14,8 +22,15 @@ import { toast } from "sonner"; export default function FormPage() { const params = useParams(); const form = useFormRendererContext(); + const { setCurrentFormName, setCurrentFormLabel } = useFormsLayout(); + // ? We will be using this to keep track of async processes on any client we have + const test = useAsyncProcess({ + processId: "00000000-0000-0000-0000-000000000000", + }); + console.log("TEST", test, config); + // Show mobile notice toast on mount useEffect(() => { const isMobile = window.innerWidth < 640; // sm breakpoint diff --git a/package.json b/package.json index c95295ba..29726b39 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test": "npx jest --no-color --passWithNoTests >jest.log 2>&1" }, "dependencies": { - "@betterinternship/components": "^1.0.1", + "@betterinternship/components": "1.3.5", "@betterinternship/core": "^1.9.4", "@betterinternship/schema.base": "^1.2.1", "@betterinternship/schema.moa": "^1.5.15", From e17780b50cc2a21553f73ddc8becca73cc2164c4 Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Tue, 17 Feb 2026 19:06:17 +0800 Subject: [PATCH 02/97] fix: forms ui doesn't wait for cache to be invalidated --- app/student/profile/page.tsx | 1 - lib/api/student.actions.api.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/student/profile/page.tsx b/app/student/profile/page.tsx index d09bc397..901eef13 100644 --- a/app/student/profile/page.tsx +++ b/app/student/profile/page.tsx @@ -789,7 +789,6 @@ const ProfileEditor = forwardRef< }, }; updateProfile(updatedProfile); - await qc.refetchQueries({ queryKey: ["my-profile"] }); return true; }, })); diff --git a/lib/api/student.actions.api.ts b/lib/api/student.actions.api.ts index 5a0d4334..e114fc7b 100644 --- a/lib/api/student.actions.api.ts +++ b/lib/api/student.actions.api.ts @@ -68,7 +68,7 @@ export const useProfileActions = () => { update: useMutation({ mutationFn: UserService.updateMyProfile, onSettled: () => - queryClient.invalidateQueries({ queryKey: ["my-profile"] }), + queryClient.invalidateQueries({ queryKey: ["my-profile", "my-form-templates"] }), }), }; From dc129a075a4ce6faa61fbf38b07a067218536bba Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Tue, 17 Feb 2026 19:17:42 +0800 Subject: [PATCH 03/97] fix: editing profile doesn't save changes --- lib/api/student.actions.api.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/api/student.actions.api.ts b/lib/api/student.actions.api.ts index e114fc7b..9a48b7a5 100644 --- a/lib/api/student.actions.api.ts +++ b/lib/api/student.actions.api.ts @@ -67,8 +67,10 @@ export const useProfileActions = () => { const actions = { update: useMutation({ mutationFn: UserService.updateMyProfile, - onSettled: () => - queryClient.invalidateQueries({ queryKey: ["my-profile", "my-form-templates"] }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["my-profile"] }); + queryClient.invalidateQueries({ queryKey: ["my-form-templates"] }); + } }), }; From 3d0f88f3383ce3db2a196faded2c9b4831089511 Mon Sep 17 00:00:00 2001 From: Jana Marie Bantolino Date: Wed, 18 Feb 2026 19:41:46 +0800 Subject: [PATCH 04/97] chore: the `rounded-[0.33em]` disease --- app/hire/login/page.tsx | 72 ++++++++++++++++++---------- app/hire/register/page.tsx | 97 +++++++++++++++++++++++++------------- 2 files changed, 112 insertions(+), 57 deletions(-) diff --git a/app/hire/login/page.tsx b/app/hire/login/page.tsx index 4d392434..a36bb8b6 100644 --- a/app/hire/login/page.tsx +++ b/app/hire/login/page.tsx @@ -7,9 +7,7 @@ import { useAuthContext } from "../authctx"; import { cn } from "@/lib/utils"; import { useAppContext } from "@/lib/ctx-app"; -import { - FormInput, -} from "@/components/EditForm"; +import { FormInput } from "@/components/EditForm"; import { Card } from "@/components/ui/card"; import { MailCheck, TriangleAlert, User } from "lucide-react"; @@ -22,7 +20,7 @@ export default function LoginPage() { Loading login...}> - ) + ); } function LoginContent() { @@ -92,18 +90,15 @@ function LoginContent() { } }; - return ( -
@@ -115,10 +110,12 @@ function LoginContent() {
{/* Error Message */} {error && ( -
+
{error}
@@ -126,12 +123,17 @@ function LoginContent() { {/* check email message on successful register */} {status === "success" && !error && ( -
+
- Registration successful. Please check your email for the password. + + Registration successful. Please check your email for the + password. +
)} @@ -154,19 +156,41 @@ function LoginContent() { required />
- Forgot password? -
- Don't have an account? Register here. + Don't have an account?{" "} + + Register here. + - Need help? Contact us at 0927 660 4999 or on Viber. + Need help? Contact us at{" "} + + 0927 660 4999 + {" "} + or on{" "} + + Viber + + .
diff --git a/app/hire/register/page.tsx b/app/hire/register/page.tsx index 9b57a3b6..3ade6096 100644 --- a/app/hire/register/page.tsx +++ b/app/hire/register/page.tsx @@ -6,11 +6,7 @@ import { useAuthContext } from "../authctx"; import { useRouter } from "next/navigation"; import { isValidRequiredURL, toURL } from "@/lib/utils/url-utils"; import { Employer } from "@/lib/db/db.types"; -import { - createEditForm, - FormCheckbox, - FormInput, -} from "@/components/EditForm"; +import { createEditForm, FormCheckbox, FormInput } from "@/components/EditForm"; import { Card } from "@/components/ui/card"; import { ErrorLabel } from "@/components/ui/labels"; import { Button } from "@/components/ui/button"; @@ -39,12 +35,12 @@ export default function RegisterPage() { const { isMobile } = useAppContext(); return ( -
+
@@ -79,7 +75,7 @@ const EmployerEditor = ({ const { industries, universities, get_university_by_name } = useDbRefs(); const [isRegistering, setIsRegistering] = useState(false); const [additionalFields, setAdditionalFields] = useState( - {} as AdditionalFields + {} as AdditionalFields, ); const [missingFields, setMissingFields] = useState([]); @@ -87,7 +83,10 @@ const EmployerEditor = ({ // Validate required fields before submitting const newMissing: string[] = []; - if (!formData.legal_entity_name || formData.legal_entity_name.trim().length < 3) { + if ( + !formData.legal_entity_name || + formData.legal_entity_name.trim().length < 3 + ) { newMissing.push("Legal entity name"); } if ( @@ -158,25 +157,25 @@ const EmployerEditor = ({ useEffect(() => { addValidator( "name", - (name: string) => name && name.length < 3 && `Company Name is not valid.` + (name: string) => name && name.length < 3 && `Company Name is not valid.`, ); addValidator( "website", (link: string) => - link && !isValidRequiredURL(link) && "Invalid website link." + link && !isValidRequiredURL(link) && "Invalid website link.", ); addValidator( "phone_number", (number: string) => - number && !isValidPHNumber(number) && "Invalid PH number." + number && !isValidPHNumber(number) && "Invalid PH number.", ); addValidator( "email", - (email: string) => email && !isValidEmail(email) && "Invalid email." + (email: string) => email && !isValidEmail(email) && "Invalid email.", ); addValidator( "location", - (location: string) => !location && `Provide your main office's location.` + (location: string) => !location && `Provide your main office's location.`, ); }, []); @@ -195,18 +194,18 @@ const EmployerEditor = ({ Register
{missingFields.length > 0 && ( -
+
You need to provide values for these fields:
    {missingFields.map((field) => ( -
  • - {field} -
  • +
  • {field}
  • ))}
@@ -227,7 +226,9 @@ const EmployerEditor = ({ }) } className={cn( - missingFields.find((field) => field === "Contact name") ? "border-destructive" : "" + missingFields.find((field) => field === "Contact name") + ? "border-destructive" + : "", )} />
@@ -236,7 +237,9 @@ const EmployerEditor = ({ value={formData.phone_number ?? ""} setter={fieldSetter("phone_number")} className={cn( - missingFields.find((field) => field === "Phone number") ? "border-destructive" : "" + missingFields.find((field) => field === "Phone number") + ? "border-destructive" + : "", )} /> @@ -247,7 +250,9 @@ const EmployerEditor = ({ value={formData.email ?? ""} setter={fieldSetter("email")} className={cn( - missingFields.find((field) => field === "Contact email") ? "border-destructive" : "" + missingFields.find((field) => field === "Contact email") + ? "border-destructive" + : "", )} /> @@ -258,7 +263,9 @@ const EmployerEditor = ({ value={formData.website ?? ""} setter={fieldSetter("website")} // invalid type className={cn( - missingFields.find((field) => field === "Company website") ? "border-destructive" : "" + missingFields.find((field) => field === "Company website") + ? "border-destructive" + : "", )} />
@@ -275,7 +282,9 @@ const EmployerEditor = ({ required={true} maxLength={100} className={cn( - missingFields.find((field) => field === "Legal entity name") ? "border-destructive" : "" + missingFields.find((field) => field === "Legal entity name") + ? "border-destructive" + : "", )} /> @@ -295,7 +304,9 @@ const EmployerEditor = ({ setter={fieldSetter("location")} maxLength={100} className={cn( - missingFields.find((field) => field === "Main office city") ? "border-destructive" : "" + missingFields.find((field) => field === "Main office city") + ? "border-destructive" + : "", )} />
@@ -337,7 +348,13 @@ const EmployerEditor = ({
- Already have an account? Log in here. + Already have an account?{" "} + + Log in here. +
- Need help? Contact us at 0927 660 4999 or on Viber. + Need help? Contact us at{" "} + + 0927 660 4999 + {" "} + or on{" "} + + Viber + + . From 9d6c22e25ae4dcf86aca06eeb7ef8d68a88dce4d Mon Sep 17 00:00:00 2001 From: neue-dev Date: Thu, 19 Feb 2026 02:14:44 +0800 Subject: [PATCH 05/97] fix: formDocument -> formTemplate and temp fix for useAsyncProcess --- app/student/forms/[name]/page.tsx | 8 ++++---- components/features/student/forms/form-renderer.ctx.tsx | 6 +++--- lib/api/services.ts | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/student/forms/[name]/page.tsx b/app/student/forms/[name]/page.tsx index b2b7fa0c..0713c431 100644 --- a/app/student/forms/[name]/page.tsx +++ b/app/student/forms/[name]/page.tsx @@ -26,10 +26,10 @@ export default function FormPage() { const { setCurrentFormName, setCurrentFormLabel } = useFormsLayout(); // ? We will be using this to keep track of async processes on any client we have - const test = useAsyncProcess({ - processId: "00000000-0000-0000-0000-000000000000", - }); - console.log("TEST", test, config); + // const test = useAsyncProcess({ + // processId: "00000000-0000-0000-0000-000000000000", + // }); + // console.log("TEST", test, config); // Show mobile notice toast on mount useEffect(() => { diff --git a/components/features/student/forms/form-renderer.ctx.tsx b/components/features/student/forms/form-renderer.ctx.tsx index 3960fe80..7789d905 100644 --- a/components/features/student/forms/form-renderer.ctx.tsx +++ b/components/features/student/forms/form-renderer.ctx.tsx @@ -1,7 +1,7 @@ /** * @ Author: BetterInternship * @ Create Time: 2025-11-09 03:19:04 - * @ Modified time: 2026-01-06 19:31:46 + * @ Modified time: 2026-02-19 01:22:55 * @ Description: * * We can move this out later on so it becomes reusable in other places. @@ -162,14 +162,14 @@ export const FormRendererContextProvider = ({ const fm = new FormMetadata(form.formMetadata); const newFormName = form.formMetadata.name; const newFormLabel = form.formMetadata.label; - const newFormVersion = form.formDocument.version; + const newFormVersion = form.formTemplate.version; // Only update form if it's new setFormMetadata(fm); setFormName(newFormName); setFormLabel(newFormLabel); setFormVersion(newFormVersion); - setDocumentName(form.formDocument.name); + setDocumentName(form.formTemplate.name); setDocumentUrl(form.documentUrl); const loadedFields = fm.getFieldsForClientService("initiator"); diff --git a/lib/api/services.ts b/lib/api/services.ts index 6aef87f3..96c13d74 100644 --- a/lib/api/services.ts +++ b/lib/api/services.ts @@ -227,7 +227,7 @@ export const FormService = { async getForm(formName: string) { const form = await APIClient.get< { - formDocument: { + formTemplate: { name: string; label: string; version: number; @@ -332,7 +332,6 @@ export const UserService = { APIRouteBuilder("users").r(userId).build(), ); }, - }; // Job Services From 09aa2652e0ce2ee909dd3984c281fc6592b80b77 Mon Sep 17 00:00:00 2001 From: anaj00 Date: Fri, 20 Feb 2026 01:17:21 +0800 Subject: [PATCH 06/97] chore: no wonder shiny text was black --- app/globals.css | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/globals.css b/app/globals.css index 04a3e253..85254047 100644 --- a/app/globals.css +++ b/app/globals.css @@ -26,22 +26,6 @@ } } -.shiny-text { - background: linear-gradient( - 90deg, - #ffd700 0%, - #fff44f 25%, - #ffeb3b 50%, - #fff44f 75%, - #ffd700 100% - ); - background-size: 200% auto; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: shiny 3s linear infinite; -} - @layer base { :root { --background: 0 0% 100%; From 37a5a69e06104a6322aa969353979ba03c669f56 Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Mon, 23 Feb 2026 17:14:32 +0800 Subject: [PATCH 07/97] style: make contact options more obvious on forms page with no forms --- components/forms/FormGenerateView.tsx | 84 +++++++++++++++------------ components/forms/FormHistoryView.tsx | 8 +-- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/components/forms/FormGenerateView.tsx b/components/forms/FormGenerateView.tsx index 02f4987c..bd6c4385 100644 --- a/components/forms/FormGenerateView.tsx +++ b/components/forms/FormGenerateView.tsx @@ -1,13 +1,15 @@ "use client"; import { HeaderIcon, HeaderText } from "@/components/ui/text"; -import { Newspaper, MessageSquare } from "lucide-react"; +import { Newspaper, MessageSquare, HelpCircle, Facebook, Mail } from "lucide-react"; import FormTemplateCard from "@/components/features/student/forms/FormGenerateCard"; import { useRouter } from "next/navigation"; import { Loader } from "@/components/ui/loader"; import { cn } from "@/lib/utils"; import { Separator } from "@/components/ui/separator"; import { HorizontalCollapsible } from "@/components/ui/horizontal-collapse"; +import { Button } from "../ui/button"; +import Link from "next/link"; /** * Generate Forms View @@ -25,44 +27,27 @@ export function FormGenerateView({
-
+
Internship Forms
-
-
- -

- Need help? Contact us via{" "} - - Facebook - {" "} - or{" "} - - email - -

+ {formTemplates?.length && +
+ +
+ +
+
- -
- -
-
-
+ }
@@ -71,8 +56,35 @@ export function FormGenerateView({ {isLoading && Loading latest forms...}
{!isLoading && (formTemplates?.length ?? 0) === 0 && ( -
-

There are no forms available yet for your department.

+
+

We're currently adding forms for this department.

+

+ In the meantime, please contact us and we'll personally assist you. +

+
+ + + + + + +
)}
+
-
+
Form History
@@ -42,9 +42,9 @@ export function FormHistoryView({ forms }: FormHistoryViewProps) {
{forms.length === 0 ? ( -
+

No forms yet

-

+

You haven't generated any forms yet. Create your first form to get started!

From d672f1b1014f4707443e463c0d8b67d4216a8af3 Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Mon, 23 Feb 2026 17:53:15 +0800 Subject: [PATCH 08/97] style: add link to form generation when no forms are in history --- components/forms/FormGenerateView.tsx | 7 ++++++- components/forms/FormHistoryView.tsx | 26 +++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/components/forms/FormGenerateView.tsx b/components/forms/FormGenerateView.tsx index bd6c4385..3f0c1c12 100644 --- a/components/forms/FormGenerateView.tsx +++ b/components/forms/FormGenerateView.tsx @@ -10,6 +10,7 @@ import { Separator } from "@/components/ui/separator"; import { HorizontalCollapsible } from "@/components/ui/horizontal-collapse"; import { Button } from "../ui/button"; import Link from "next/link"; +import { useAppContext } from "@/lib/ctx-app"; /** * Generate Forms View @@ -22,6 +23,7 @@ export function FormGenerateView({ isLoading: boolean; }) { const router = useRouter(); + const { isMobile } = useAppContext(); return (
@@ -61,7 +63,10 @@ export function FormGenerateView({

In the meantime, please contact us and we'll personally assist you.

-
+
diff --git a/components/forms/FormHistoryView.tsx b/components/forms/FormHistoryView.tsx index a7f02ca1..e63a8c70 100644 --- a/components/forms/FormHistoryView.tsx +++ b/components/forms/FormHistoryView.tsx @@ -1,13 +1,17 @@ "use client"; import { HeaderIcon, HeaderText } from "@/components/ui/text"; -import { Newspaper } from "lucide-react"; +import { Mail, MessageSquare, Newspaper, Plus, PlusCircle } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { FormLog } from "./FormLog"; import { IFormSigningParty } from "@betterinternship/core/forms"; -import { formatDate } from "@/lib/utils"; +import { cn, formatDate } from "@/lib/utils"; +import Link from "next/link"; +import { Button } from "../ui/button"; +import { useFormsLayout } from "@/app/student/forms/layout"; +import { useAppContext } from "@/lib/ctx-app"; interface FormHistoryViewProps { forms: Array<{ @@ -28,6 +32,9 @@ interface FormHistoryViewProps { * Form History View */ export function FormHistoryView({ forms }: FormHistoryViewProps) { + const { setActiveView } = useFormsLayout(); + const { isMobile } = useAppContext(); + return (
@@ -45,9 +52,22 @@ export function FormHistoryView({ forms }: FormHistoryViewProps) {

No forms yet

- You haven't generated any forms yet. Create your first form to + You haven't generated any forms yet. Generate your first form to get started!

+
+ +
) : ( forms From 2ca05ec5f5f43eb4494f1ae93dca5a6517a15b85 Mon Sep 17 00:00:00 2001 From: chua-e <159678059+chua-e@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:19:21 +0800 Subject: [PATCH 09/97] fix case sensitive email and success messages in forgot pw --- app/hire/forgot-password/page.tsx | 9 +++++---- lib/api/api-client.ts | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/hire/forgot-password/page.tsx b/app/hire/forgot-password/page.tsx index 84e8dc71..caf472af 100644 --- a/app/hire/forgot-password/page.tsx +++ b/app/hire/forgot-password/page.tsx @@ -49,12 +49,13 @@ const ForgotPasswordForm = ({}) => { setMessage(""); try { - const r = await EmployerUserService.requestPasswordReset(email); + const r = await EmployerUserService.requestPasswordReset(email.toLowerCase()); // @ts-ignore setMessage(r.message); } catch (err: any) { - setError(err.message ?? "Something went wrong. Please try again later."); + console.log(err); + setError(err.response?.data?.message ?? err.message ?? "Something went wrong. Please try again later."); } finally { setIsLoading(false); } @@ -75,12 +76,12 @@ const ForgotPasswordForm = ({}) => { Reset password
{error && ( -
+

{error}

)} {message && ( -
+

{message}

)} diff --git a/lib/api/api-client.ts b/lib/api/api-client.ts index af4a6f57..c6961302 100644 --- a/lib/api/api-client.ts +++ b/lib/api/api-client.ts @@ -84,8 +84,9 @@ class FetchClient { const response = await fetch(url, config); if (!response.ok && response.status !== 304) { const errorData = await response.json().catch(() => ({})); - console.warn(`${url}: ${errorData.message || response.status}`); - return { error: errorData.message } as T; + // console.warn(`${url}: ${errorData.message || response.status}`); + // return { error: errorData.message } as T; + throw new Error(errorData.message || "Something went wrong."); } const contentType = response.headers.get("content-type"); From 188551ca20e346d4f74a4ae6b8e7642c7fec630e Mon Sep 17 00:00:00 2001 From: chua-e <159678059+chua-e@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:34:25 +0800 Subject: [PATCH 10/97] forgot to remove console log --- app/hire/forgot-password/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/hire/forgot-password/page.tsx b/app/hire/forgot-password/page.tsx index caf472af..b1109abe 100644 --- a/app/hire/forgot-password/page.tsx +++ b/app/hire/forgot-password/page.tsx @@ -54,7 +54,6 @@ const ForgotPasswordForm = ({}) => { // @ts-ignore setMessage(r.message); } catch (err: any) { - console.log(err); setError(err.response?.data?.message ?? err.message ?? "Something went wrong. Please try again later."); } finally { setIsLoading(false); From 71f6b547ce1c4940ecc9bf1fb628c19019899dc4 Mon Sep 17 00:00:00 2001 From: Jana Marie Bantolino Date: Tue, 24 Feb 2026 00:50:46 +0800 Subject: [PATCH 11/97] chore (temp): disable ADMU for now --- app/student/register/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/student/register/page.tsx b/app/student/register/page.tsx index db8d2b1d..ea94dfe4 100644 --- a/app/student/register/page.tsx +++ b/app/student/register/page.tsx @@ -223,6 +223,9 @@ export default function RegisterPage() { } }, [internshipType, regForm.getValues()]); + // !TEMP -- disable ateneo + const universityOptions = refs.universities?.filter((u) => u.name !== "ADMU"); + return (
@@ -295,7 +298,7 @@ export default function RegisterPage() {
regForm.setValue("university", value + "") } From d0bf8497979933a8b54b18ed29b38ba7f65a0e4d Mon Sep 17 00:00:00 2001 From: Jana Marie Bantolino Date: Tue, 24 Feb 2026 01:12:31 +0800 Subject: [PATCH 12/97] chore: remove bottom nav bar for mobile verification pages --- components/shared/mobile-nav-wrapper.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/shared/mobile-nav-wrapper.tsx b/components/shared/mobile-nav-wrapper.tsx index 58953bd1..bd62210d 100644 --- a/components/shared/mobile-nav-wrapper.tsx +++ b/components/shared/mobile-nav-wrapper.tsx @@ -15,7 +15,9 @@ export default function MobileNavWrapper() { pathname === "/" || pathname === "/student" || pathname.startsWith("/forms/") || - pathname === "/miro"; + pathname === "/miro" || + pathname === "/register" || + pathname === "/register/verify"; if (!isMobile || hide) { return null; From e07c16547de968891fe13bfc2089ad82082cf6f0 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Tue, 24 Feb 2026 04:21:23 +0800 Subject: [PATCH 13/97] feat: pdf gen (manual sign) works with rabbitmq --- app/student/forms/[name]/page.tsx | 12 +-------- app/student/forms/layout.tsx | 2 -- app/student/layout.tsx | 23 ++++++++++------- .../student/forms/FormActionButtons.tsx | 22 ++++++++++++++-- lib/api/services.ts | 25 +++++++++++++------ package.json | 2 +- 6 files changed, 53 insertions(+), 33 deletions(-) diff --git a/app/student/forms/[name]/page.tsx b/app/student/forms/[name]/page.tsx index 0713c431..35e30587 100644 --- a/app/student/forms/[name]/page.tsx +++ b/app/student/forms/[name]/page.tsx @@ -6,11 +6,7 @@ import { useFormsLayout } from "../layout"; import { useParams } from "next/navigation"; import { useEffect } from "react"; import { toast } from "sonner"; -import { - config, - configure, - useAsyncProcess, -} from "@betterinternship/components"; +import { configure } from "@betterinternship/components"; // ! move this to the topmost client component you can find configure({ orchestratorApi: "https://orca.betterinternship.com/process" }); @@ -25,12 +21,6 @@ export default function FormPage() { const { setCurrentFormName, setCurrentFormLabel } = useFormsLayout(); - // ? We will be using this to keep track of async processes on any client we have - // const test = useAsyncProcess({ - // processId: "00000000-0000-0000-0000-000000000000", - // }); - // console.log("TEST", test, config); - // Show mobile notice toast on mount useEffect(() => { const isMobile = window.innerWidth < 640; // sm breakpoint diff --git a/app/student/forms/layout.tsx b/app/student/forms/layout.tsx index 715cdfe5..96d01890 100644 --- a/app/student/forms/layout.tsx +++ b/app/student/forms/layout.tsx @@ -16,7 +16,6 @@ import { FormFillerContextProvider } from "@/components/features/student/forms/f import { FormsNavigation } from "@/components/features/student/forms/FormsNavigation"; import { useMyForms } from "./myforms.ctx"; import { SignContextProvider } from "@/components/providers/sign.ctx"; -import { SonnerToaster } from "@/components/ui/sonner-toast"; import { useMobile } from "@/hooks/use-mobile"; import { useHeaderContext, MobileAddonConfig } from "@/lib/ctx-header"; @@ -173,7 +172,6 @@ const FormsLayout = ({ children }: { children: React.ReactNode }) => { {children} - diff --git a/app/student/layout.tsx b/app/student/layout.tsx index f054d63d..dc70da1a 100644 --- a/app/student/layout.tsx +++ b/app/student/layout.tsx @@ -12,6 +12,8 @@ import { ConversationsContextProvider } from "@/hooks/use-conversation"; import { PocketbaseProvider } from "@/lib/pocketbase"; import { ModalProvider } from "@/components/providers/ModalProvider"; import MobileNavWrapper from "@/components/shared/mobile-nav-wrapper"; +import { SonnerToaster } from "@/components/ui/sonner-toast"; +import { ClientProcessesProvider } from "@betterinternship/components"; const baseUrl = process.env.NEXT_PUBLIC_CLIENT_URL || "https://betterinternship.com"; @@ -93,16 +95,19 @@ const HTMLContent = ({ - - -
-
- {children} + + + +
+
+ {children} +
+
- -
- - + + + + diff --git a/components/features/student/forms/FormActionButtons.tsx b/components/features/student/forms/FormActionButtons.tsx index 98363a1f..e15f4cf8 100644 --- a/components/features/student/forms/FormActionButtons.tsx +++ b/components/features/student/forms/FormActionButtons.tsx @@ -9,7 +9,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useFormRendererContext } from "./form-renderer.ctx"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useFormFiller } from "./form-filler.ctx"; import { useMyAutofillUpdate, useMyAutofill } from "@/hooks/use-my-autofill"; import { useProfileData } from "@/lib/api/student.data.api"; @@ -22,6 +22,7 @@ import { toast } from "sonner"; import { toastPresets } from "@/components/ui/sonner-toast"; import { useSignContext } from "@/components/providers/sign.ctx"; import { CircleHelp } from "lucide-react"; +import { useClientProcess } from "@betterinternship/components"; export function FormActionButtons() { const form = useFormRendererContext(); @@ -32,6 +33,12 @@ export function FormActionButtons() { const updateAutofill = useMyAutofillUpdate(); const signContext = useSignContext(); const queryClient = useQueryClient(); + const filloutFormRequest = useClientProcess({ + caller: FormService.filloutForm.bind(FormService), + loadingMessage: `Generating ${form.formLabel}...`, + successMessage: `Successfully generated ${form.formLabel}!`, + failureMessage: `Could not generate ${form.formLabel}.`, + }); const noEsign = !form.formMetadata.mayInvolveEsign(); const initiateFormLabel = "Sign via BetterInternship"; @@ -116,7 +123,7 @@ export function FormActionButtons() { // Just fill out form } else { - const response = await FormService.filloutForm({ + const response = await filloutFormRequest.run({ formName: form.formName, formVersion: form.formVersion, values: finalValues, @@ -142,6 +149,17 @@ export function FormActionButtons() { } }; + useEffect(() => { + if (filloutFormRequest.status === "handled") + void queryClient.invalidateQueries({ queryKey: ["my-forms"] }); + + console.log( + "FILLOUT FORM REQUEST", + filloutFormRequest.result, + filloutFormRequest.status, + ); + }, [filloutFormRequest]); + return (
diff --git a/lib/api/services.ts b/lib/api/services.ts index 96c13d74..fe86d1eb 100644 --- a/lib/api/services.ts +++ b/lib/api/services.ts @@ -12,12 +12,24 @@ import { import { APIClient, APIRouteBuilder } from "./api-client"; import { FetchResponse } from "@/lib/api/use-fetch"; import { IFormMetadata, IFormSigningParty } from "@betterinternship/core/forms"; -import { Tables } from "@betterinternship/schema.base"; interface EmployerResponse extends FetchResponse { employer: Partial; } +export interface ProcessCallbackDto { + processId: string; + processName: string; +} + +export interface ProcessResponse { + processId: string; + processName: string; + processCallbackUrl: string; + success: boolean; + message?: string; +} + export const EmployerService = { async getMyProfile() { return APIClient.get( @@ -180,13 +192,10 @@ export const FormService = { values: Record; disableEsign?: boolean; }) { - return APIClient.post<{ - formProcessId: string; - documentId?: string; - documentUrl?: string; - success?: boolean; - message?: string; - }>(APIRouteBuilder("users").r("me/fillout-form").build(), data); + return APIClient.post( + APIRouteBuilder("users").r("me/fillout-form").build(), + data, + ); }, async getMyFormTemplates() { diff --git a/package.json b/package.json index 29726b39..01e8f559 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test": "npx jest --no-color --passWithNoTests >jest.log 2>&1" }, "dependencies": { - "@betterinternship/components": "1.3.5", + "@betterinternship/components": "1.5.3", "@betterinternship/core": "^1.9.4", "@betterinternship/schema.base": "^1.2.1", "@betterinternship/schema.moa": "^1.5.15", From 50515e6bb2b1883a6b4407d93a5490c6f10b0344 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Wed, 25 Feb 2026 13:39:06 +0800 Subject: [PATCH 14/97] fix: rm thrown error on fetch client --- lib/api/api-client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/api/api-client.ts b/lib/api/api-client.ts index c6961302..91e861ed 100644 --- a/lib/api/api-client.ts +++ b/lib/api/api-client.ts @@ -84,9 +84,9 @@ class FetchClient { const response = await fetch(url, config); if (!response.ok && response.status !== 304) { const errorData = await response.json().catch(() => ({})); - // console.warn(`${url}: ${errorData.message || response.status}`); - // return { error: errorData.message } as T; - throw new Error(errorData.message || "Something went wrong."); + console.warn(`${url}: ${errorData.message || response.status}`); + return { error: errorData.message } as T; + // throw new Error(errorData.message || "Something went wrong."); } const contentType = response.headers.get("content-type"); From b839b640923999bd1a84e4649e253d82dd583619 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Wed, 25 Feb 2026 18:42:42 +0800 Subject: [PATCH 15/97] feat: form fillout process client impl for rabbitmq --- app/student/forms/page.tsx | 25 ++++++++-- app/tanstack-provider.tsx | 2 +- .../student/forms/FormActionButtons.tsx | 48 +++++++++++-------- components/forms/FormHistoryView.tsx | 17 ++++--- components/forms/FormLog.tsx | 40 ++++++++++++++-- package.json | 2 +- 6 files changed, 96 insertions(+), 38 deletions(-) diff --git a/app/student/forms/page.tsx b/app/student/forms/page.tsx index 3f89ae69..1a4e1c74 100644 --- a/app/student/forms/page.tsx +++ b/app/student/forms/page.tsx @@ -3,7 +3,7 @@ import { useProfileData } from "@/lib/api/student.data.api"; import { useRouter } from "next/navigation"; import { FormService } from "@/lib/api/services"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMyForms } from "./myforms.ctx"; import { FormGenerateView } from "../../../components/forms/FormGenerateView"; import { FormHistoryView } from "../../../components/forms/FormHistoryView"; @@ -12,8 +12,9 @@ import { FORM_TEMPLATES_STALE_TIME, FORM_TEMPLATES_GC_TIME, } from "@/lib/consts/cache"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useAuthContext } from "@/lib/ctx-auth"; +import { useClientProcess } from "@betterinternship/components"; /** * The forms page component - shows either history or generate based on form count @@ -24,8 +25,11 @@ export default function FormsPage() { const profile = useProfileData(); const router = useRouter(); const myForms = useMyForms(); + const queryClient = useQueryClient(); + const { activeView } = useFormsLayout(); const { redirectIfNotLoggedIn, isAuthenticated } = useAuthContext(); + const [formHistory, setFormHistory] = useState([]); // Auth redirect at body level (runs first) redirectIfNotLoggedIn(); @@ -62,11 +66,26 @@ export default function FormsPage() { // enabled: !!updateInfo, // Only fetch after we have update info }); + // Heeheehaw + const formFilloutProcess = useClientProcess({ filterKey: "form-fillout" }); + const pendingForms = formFilloutProcess + .getAllPending() + .map((pendingForm) => ({ + label: pendingForm.metadata?.metadata?.label ?? "", + timestamp: pendingForm.metadata?.metadata?.timestamp ?? "", + pending: true, + })); + + // Refresh when processes become handled + useEffect(() => { + void queryClient.invalidateQueries({ queryKey: ["my-forms"] }); + }, [formFilloutProcess.getAllHandled()]); + return ( <> {/* Show the active view */} {activeView === "history" ? ( - + ) : ( )} diff --git a/app/tanstack-provider.tsx b/app/tanstack-provider.tsx index 4b5b35c8..3081cee0 100644 --- a/app/tanstack-provider.tsx +++ b/app/tanstack-provider.tsx @@ -21,7 +21,7 @@ const asyncStoragePersister = createAsyncStoragePersister({ storage: typeof window === "undefined" ? undefined : AsyncStorage, }); -persistQueryClient({ +void persistQueryClient({ queryClient, persister: asyncStoragePersister, maxAge: 24 * 60 * 60 * 1000, diff --git a/components/features/student/forms/FormActionButtons.tsx b/components/features/student/forms/FormActionButtons.tsx index e15f4cf8..d0584d48 100644 --- a/components/features/student/forms/FormActionButtons.tsx +++ b/components/features/student/forms/FormActionButtons.tsx @@ -33,11 +33,24 @@ export function FormActionButtons() { const updateAutofill = useMyAutofillUpdate(); const signContext = useSignContext(); const queryClient = useQueryClient(); - const filloutFormRequest = useClientProcess({ + const formFilloutProcess = useClientProcess({ + filterKey: "form-fillout", caller: FormService.filloutForm.bind(FormService), - loadingMessage: `Generating ${form.formLabel}...`, - successMessage: `Successfully generated ${form.formLabel}!`, - failureMessage: `Could not generate ${form.formLabel}.`, + onLoading: (processId) => { + toast.loading(`Generating ${form.formLabel}...`, { id: processId }); + }, + onSuccess: (processId, _processName, result) => { + toast.success(`Generated ${form.formLabel}!`, { id: processId }); + setTimeout(() => toast.dismiss(processId), 2000); + console.log("RESULT: ", result); + }, + onFailure: (processId, _processName, error) => { + toast.error(`Could not generate ${form.formLabel}: ${error}`, { + id: processId, + }); + setTimeout(() => toast.dismiss(processId), 2000); + console.log("ERROR: ", error); + }, }); const noEsign = !form.formMetadata.mayInvolveEsign(); @@ -123,11 +136,17 @@ export function FormActionButtons() { // Just fill out form } else { - const response = await filloutFormRequest.run({ - formName: form.formName, - formVersion: form.formVersion, - values: finalValues, - }); + const response = await formFilloutProcess.run( + { + formName: form.formName, + formVersion: form.formVersion, + values: finalValues, + }, + { + label: form.formLabel, + timestamp: new Date().toISOString(), + }, + ); if (!response.success) { setBusy(false); @@ -149,17 +168,6 @@ export function FormActionButtons() { } }; - useEffect(() => { - if (filloutFormRequest.status === "handled") - void queryClient.invalidateQueries({ queryKey: ["my-forms"] }); - - console.log( - "FILLOUT FORM REQUEST", - filloutFormRequest.result, - filloutFormRequest.status, - ); - }, [filloutFormRequest]); - return (
diff --git a/components/forms/FormHistoryView.tsx b/components/forms/FormHistoryView.tsx index e63a8c70..6cabc076 100644 --- a/components/forms/FormHistoryView.tsx +++ b/components/forms/FormHistoryView.tsx @@ -1,21 +1,20 @@ "use client"; import { HeaderIcon, HeaderText } from "@/components/ui/text"; -import { Mail, MessageSquare, Newspaper, Plus, PlusCircle } from "lucide-react"; +import { Newspaper, Plus } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { FormLog } from "./FormLog"; import { IFormSigningParty } from "@betterinternship/core/forms"; import { cn, formatDate } from "@/lib/utils"; -import Link from "next/link"; import { Button } from "../ui/button"; import { useFormsLayout } from "@/app/student/forms/layout"; import { useAppContext } from "@/lib/ctx-app"; interface FormHistoryViewProps { forms: Array<{ - form_process_id: string; + form_process_id?: string; label: string; prefilled_document_id?: string | null; pending_document_id?: string | null; @@ -25,6 +24,7 @@ interface FormHistoryViewProps { signing_parties?: IFormSigningParty[]; status?: string | null; rejection_reason?: string; + pending?: boolean; }>; } @@ -55,14 +55,11 @@ export function FormHistoryView({ forms }: FormHistoryViewProps) { You haven't generated any forms yet. Generate your first form to get started!

-
+
)} - {documentId ? ( + {status === "done" ? ( ) : ( @@ -207,7 +212,7 @@ export const FormLog = ({ {/* Mobile chevron/download */}
- {documentId ? ( + {status === "done" ? ( ) : ( @@ -239,7 +248,7 @@ export const FormLog = ({
{/* Mobile action buttons */} - {!rejectionReason && !documentId && ( + {!rejectionReason && status !== "done" && (
+ ); + })} +
+
+ + +
+
+
+
+

+ {selectedTemplate?.title} +

+ +
+
+ These people will receive a copy of this form, in this order: +
+ + {recipients.map((recipient, index) => ( + + {recipient.title} + + } + subtitle={ + (index === 1 || index === 4) && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + +
+
+ + +
+ +
+
+
+
+
+
+ ); +} diff --git a/components/ui/timeline.tsx b/components/ui/timeline.tsx index f1bcbff2..aa6f1edb 100644 --- a/components/ui/timeline.tsx +++ b/components/ui/timeline.tsx @@ -23,29 +23,26 @@ export const TimelineItem = ({ const isCheckmark = number === -1; return ( -
- {/* Timeline connector */} -
- {/* Circle or Checkmark */} +
+
{isCheckmark ? (
) : ( -
+
{number}
)} - {/* Line to next item */} - {!isLast &&
} + {!isLast &&
}
{/* Content */} -
+
-
{title}
+
{title}
{subtitle && ( -
{subtitle}
+
{subtitle}
)}
{children &&
{children}
} From 34417366482bd53af10f9f193d2e3303fb0bb845 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Sun, 1 Mar 2026 18:57:21 +0800 Subject: [PATCH 24/97] feat: link form data to new generate forms page flow --- app/student/forms/flow-test/page.tsx | 274 ++++++++++++--------------- app/student/forms/page.tsx | 6 +- 2 files changed, 126 insertions(+), 154 deletions(-) diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index bf2ff083..7b476b0e 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -1,112 +1,59 @@ "use client"; -import { useMemo, useState } from "react"; -import { ArrowDown, ChevronRight } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { ChevronRight, SearchIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { cn } from "@/lib/utils"; import { Divider } from "@/components/ui/divider"; +import { FormTemplate } from "@/lib/db/use-moa-backend"; +import { Loader } from "@/components/ui/loader"; +import { IFormSignatory } from "@betterinternship/core/forms"; +import { FormInput } from "@/components/EditForm"; +import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; -type Template = { - id: string; - title: string; - tags: string[]; -}; - -type Recipient = { - title: string; - value: string; -}; - -const templates: Template[] = [ - { - id: "student-moa", - title: "Student MOA (CS-ST)", - tags: ["Deployment", "Required"], - }, - { - id: "annex-a", - title: "Internship Annex A (CS-ST)", - tags: ["Deployment", "Compliance"], - }, - { - id: "company-info", - title: "Company Info Sheet (CS-ST)", - tags: ["Pre-deployment", "Information"], - }, - { - id: "student-moa2", - title: "Student MOA (CS-ST)", - tags: ["Deployment", "Required"], - }, - { - id: "annex-a2", - title: "Internship Annex A (CS-ST)", - tags: ["Deployment", "Compliance"], - }, - { - id: "company-info2", - title: "Company Info Sheet (CS-ST)", - tags: ["Pre-deployment", "Information"], - }, -]; +export default function FlowTestPage({ + formTemplates, + isLoading, +}: { + formTemplates: FormTemplate[]; + isLoading: boolean; +}) { + const [selectedTemplateName, setSelectedTemplateName] = useState(""); + const selectedTemplate = useMemo( + () => + formTemplates?.find( + (template) => template.formName === selectedTemplateName, + ) ?? null, + [formTemplates, selectedTemplateName], + ); + const [searchQuery, setSearchQuery] = useState(""); + const form = useFormRendererContext(); + const recipients = form.formMetadata.getSigningParties(); + const filteredTemplates = useMemo( + () => + formTemplates + .filter((template) => + searchQuery + .toLowerCase() + .split(" ") + .some((q) => template.formLabel.toLowerCase().includes(q)), + ) + .toSorted((a, b) => { + const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); + const bLabel = b.formLabel.replaceAll(/[()[\]\-,]/g, ""); + return aLabel.localeCompare(bLabel); + }), + [formTemplates, searchQuery], + ); -const recipientsByTemplate: Record = { - "student-moa": [ - { title: "Student", value: "You" }, - { - title: "Company/Organization Internship Point Person", - value: "pointperson@company.com", - }, - { - title: "Company/Organization Representative", - value: "mo*************p@gmail.com", - }, - { - title: "Company/Organization Internship Supervisor", - value: "mo*************p@gmail.com", - }, - { - title: "Student Guardian", - value: "mo**************d@gmail.com", - }, - { - title: "Department Internship Coordinator", - value: "ja***********o@dlsu.edu.ph", - }, - ], - "annex-a": [ - { title: "Student", value: "You" }, - { - title: "Company/Organization Internship Supervisor", - value: "mo*************p@gmail.com", - }, - { - title: "Department Internship Coordinator", - value: "ja***********o@dlsu.edu.ph", - }, - { - title: "College Internship Coordinator", - value: "co***********r@dlsu.edu.ph", - }, - ], - "company-info": [ - { - title: "Student", - value: "No signatures required for this template.", - }, - ], -}; + useEffect(() => { + if (selectedTemplateName) form.updateFormName(selectedTemplateName); + }, [selectedTemplateName, form]); -export default function FlowTestPage() { - const [selectedTemplateId, setSelectedTemplateId] = useState(templates[0].id); - const selectedTemplate = useMemo( - () => templates.find((template) => template.id === selectedTemplateId), - [selectedTemplateId], - ); - const recipients = recipientsByTemplate[selectedTemplateId] ?? []; + if (isLoading) return Loading form templates...; return (
@@ -114,21 +61,33 @@ export default function FlowTestPage() {
+
+ ); +} diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 7b476b0e..ce0b0eea 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -4,15 +4,14 @@ import { useEffect, useMemo, useState } from "react"; import { ChevronRight, SearchIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { cn } from "@/lib/utils"; import { Divider } from "@/components/ui/divider"; import { FormTemplate } from "@/lib/db/use-moa-backend"; import { Loader } from "@/components/ui/loader"; -import { IFormSignatory } from "@betterinternship/core/forms"; import { FormInput } from "@/components/EditForm"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; +import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; export default function FlowTestPage({ formTemplates, @@ -30,6 +29,7 @@ export default function FlowTestPage({ [formTemplates, selectedTemplateName], ); const [searchQuery, setSearchQuery] = useState(""); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); const form = useFormRendererContext(); const recipients = form.formMetadata.getSigningParties(); const filteredTemplates = useMemo( @@ -39,7 +39,7 @@ export default function FlowTestPage({ searchQuery .toLowerCase() .split(" ") - .some((q) => template.formLabel.toLowerCase().includes(q)), + .every((q) => template.formLabel.toLowerCase().includes(q)), ) .toSorted((a, b) => { const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); @@ -172,6 +172,7 @@ export default function FlowTestPage({ @@ -188,6 +189,13 @@ export default function FlowTestPage({
+ {form.document.url && ( + setIsPreviewOpen(false)} + /> + )}
); } From 3c526d6fbee4f6ac339f3247d496ea16f47c23fc Mon Sep 17 00:00:00 2001 From: neue-dev Date: Sun, 1 Mar 2026 20:06:26 +0800 Subject: [PATCH 26/97] patch: enable form preview button --- .../forms/flow-test/FlowTestPreviewModal.tsx | 61 +++++++++++++++++++ app/student/forms/flow-test/page.tsx | 25 +++++--- 2 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 app/student/forms/flow-test/FlowTestPreviewModal.tsx diff --git a/app/student/forms/flow-test/FlowTestPreviewModal.tsx b/app/student/forms/flow-test/FlowTestPreviewModal.tsx new file mode 100644 index 00000000..d3a7697e --- /dev/null +++ b/app/student/forms/flow-test/FlowTestPreviewModal.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; +import { Button } from "@/components/ui/button"; +import { X } from "lucide-react"; + +interface FlowTestPreviewModalProps { + documentUrl: string; + isOpen: boolean; + onClose: () => void; +} + +export function FlowTestPreviewModal({ + documentUrl, + isOpen, + onClose, +}: FlowTestPreviewModalProps) { + return ( +
+ +
+
+ +
+
+
+
+ ); +} diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 7b476b0e..7760e646 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -4,15 +4,14 @@ import { useEffect, useMemo, useState } from "react"; import { ChevronRight, SearchIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { cn } from "@/lib/utils"; import { Divider } from "@/components/ui/divider"; import { FormTemplate } from "@/lib/db/use-moa-backend"; import { Loader } from "@/components/ui/loader"; -import { IFormSignatory } from "@betterinternship/core/forms"; import { FormInput } from "@/components/EditForm"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; +import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; export default function FlowTestPage({ formTemplates, @@ -30,6 +29,7 @@ export default function FlowTestPage({ [formTemplates, selectedTemplateName], ); const [searchQuery, setSearchQuery] = useState(""); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); const form = useFormRendererContext(); const recipients = form.formMetadata.getSigningParties(); const filteredTemplates = useMemo( @@ -39,7 +39,7 @@ export default function FlowTestPage({ searchQuery .toLowerCase() .split(" ") - .some((q) => template.formLabel.toLowerCase().includes(q)), + .every((q) => template.formLabel.toLowerCase().includes(q)), ) .toSorted((a, b) => { const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); @@ -49,10 +49,6 @@ export default function FlowTestPage({ [formTemplates, searchQuery], ); - useEffect(() => { - if (selectedTemplateName) form.updateFormName(selectedTemplateName); - }, [selectedTemplateName, form]); - if (isLoading) return Loading form templates...; return ( @@ -87,7 +83,10 @@ export default function FlowTestPage({ @@ -188,6 +188,13 @@ export default function FlowTestPage({
+ {form.document.url && ( + setIsPreviewOpen(false)} + /> + )}
); } From 372fb2d5af8b7933678b8df377af37b58aceaf94 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Sun, 1 Mar 2026 20:50:22 +0800 Subject: [PATCH 27/97] feat: trigger specify signing parties after clicking 'sign via betterinternship' --- app/student/forms/flow-test/page.tsx | 31 ++++++++++++++++--- .../components/SigningPartyTimeline.tsx | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 7760e646..7de9955b 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -12,6 +12,9 @@ import { Loader } from "@/components/ui/loader"; import { FormInput } from "@/components/EditForm"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; +import useModalRegistry from "@/components/modals/modal-registry"; +import { useFormFiller } from "@/components/features/student/forms/form-filler.ctx"; +import { useMyAutofill } from "@/hooks/use-my-autofill"; export default function FlowTestPage({ formTemplates, @@ -28,24 +31,31 @@ export default function FlowTestPage({ ) ?? null, [formTemplates, selectedTemplateName], ); + const [searchQuery, setSearchQuery] = useState(""); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const form = useFormRendererContext(); + const formFiller = useFormFiller(); const recipients = form.formMetadata.getSigningParties(); + const signingPartyBlocks = + form.formMetadata.getSigningPartyBlocks("initiator"); + + const modalRegistry = useModalRegistry(); + const autofillValues = useMyAutofill(); const filteredTemplates = useMemo( () => formTemplates - .filter((template) => + ?.filter((template) => searchQuery .toLowerCase() .split(" ") .every((q) => template.formLabel.toLowerCase().includes(q)), ) - .toSorted((a, b) => { + ?.toSorted((a, b) => { const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); const bLabel = b.formLabel.replaceAll(/[()[\]\-,]/g, ""); return aLabel.localeCompare(bLabel); - }), + }) ?? [], [formTemplates, searchQuery], ); @@ -175,7 +185,20 @@ export default function FlowTestPage({ > Preview PDF -
diff --git a/components/modals/components/SigningPartyTimeline.tsx b/components/modals/components/SigningPartyTimeline.tsx index 373cf418..182ac8b6 100644 --- a/components/modals/components/SigningPartyTimeline.tsx +++ b/components/modals/components/SigningPartyTimeline.tsx @@ -21,7 +21,7 @@ export const SigningPartyTimeline = ({ const sourceParty = signingParties.find( (p) => p._id === party.signatory_source?._id, ); - sourceTitle = sourceParty?.signatory_title.trim() || ""; + sourceTitle = sourceParty?.signatory_title?.trim?.() || ""; } const isSourceFromYou = sourceTitle === "Student"; From cab484bdb9129a8ace5f6c404b140fada213877b Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 13:59:57 +0800 Subject: [PATCH 28/97] fix: transition for signing flow --- app/student/forms/flow-test/page.tsx | 43 +++++++++++++++---- .../student/forms/FormActionButtons.tsx | 1 - .../components/SpecifySigningPartiesModal.tsx | 32 -------------- components/modals/modal-registry.tsx | 5 +-- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 7de9955b..902d3c30 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { ChevronRight, SearchIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { ChevronRight, Eye, PenLineIcon, SearchIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; @@ -15,6 +15,7 @@ import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; import useModalRegistry from "@/components/modals/modal-registry"; import { useFormFiller } from "@/components/features/student/forms/form-filler.ctx"; import { useMyAutofill } from "@/hooks/use-my-autofill"; +import { FormAndDocumentLayout } from "@/components/features/student/forms/FormFlowRouter"; export default function FlowTestPage({ formTemplates, @@ -34,6 +35,7 @@ export default function FlowTestPage({ const [searchQuery, setSearchQuery] = useState(""); const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [isSigningFlow, setIsSigningFlow] = useState(false); const form = useFormRendererContext(); const formFiller = useFormFiller(); const recipients = form.formMetadata.getSigningParties(); @@ -61,12 +63,31 @@ export default function FlowTestPage({ if (isLoading) return Loading form templates...; + const handleSigningPartiesSubmit = async () => { + setIsSigningFlow(true); + return Promise.resolve({ success: true }); + }; + return (
-
-
))} -
+
+ +
)}
diff --git a/app/student/forms/layout.tsx b/app/student/forms/layout.tsx index 96d01890..a6ca4ae7 100644 --- a/app/student/forms/layout.tsx +++ b/app/student/forms/layout.tsx @@ -3,8 +3,6 @@ import { useState, useEffect, - createContext, - useContext, useMemo, useCallback, useLayoutEffect, @@ -13,33 +11,11 @@ import { useRouter, usePathname } from "next/navigation"; import { MyFormsContextProvider } from "./myforms.ctx"; import { FormRendererContextProvider } from "@/components/features/student/forms/form-renderer.ctx"; import { FormFillerContextProvider } from "@/components/features/student/forms/form-filler.ctx"; -import { FormsNavigation } from "@/components/features/student/forms/FormsNavigation"; import { useMyForms } from "./myforms.ctx"; import { SignContextProvider } from "@/components/providers/sign.ctx"; import { useMobile } from "@/hooks/use-mobile"; import { useHeaderContext, MobileAddonConfig } from "@/lib/ctx-header"; -interface FormsLayoutContextType { - activeView: "generate" | "history"; - setActiveView: (view: "generate" | "history") => void; - currentFormName: string | null; - setCurrentFormName: (name: string | null) => void; - currentFormLabel: string | null; - setCurrentFormLabel: (label: string | null) => void; -} - -const FormsLayoutContext = createContext( - undefined, -); - -export const useFormsLayout = () => { - const context = useContext(FormsLayoutContext); - if (!context) { - throw new Error("useFormsLayout must be used within FormsLayout"); - } - return context; -}; - function FormsLayoutContent({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); @@ -50,8 +26,6 @@ function FormsLayoutContent({ children }: { children: React.ReactNode }) { "generate" | "history" | null >(null); const [isInitialized, setIsInitialized] = useState(false); - const [currentFormName, setCurrentFormName] = useState(null); - const [currentFormLabel, setCurrentFormLabel] = useState(null); const hasFormsToShow = (myForms?.forms?.length ?? 0) > 0; @@ -99,17 +73,8 @@ function FormsLayoutContent({ children }: { children: React.ReactNode }) { show: true, activeView, onViewChange: handleViewChange, - currentFormName, - currentFormLabel, }; - }, [ - isMobile, - isInitialized, - activeView, - handleViewChange, - currentFormName, - currentFormLabel, - ]); + }, [isMobile, isInitialized, activeView, handleViewChange]); // Sync config to context after render useLayoutEffect(() => { @@ -128,37 +93,7 @@ function FormsLayoutContent({ children }: { children: React.ReactNode }) { }; }, [setMobileAddonConfig]); - return ( - -
-
- -
- -
- {!isInitialized ?
: children} -
-
- - ); + return children; } const FormsLayout = ({ children }: { children: React.ReactNode }) => { diff --git a/app/student/forms/page.tsx b/app/student/forms/page.tsx index e88d0cf9..2b42f356 100644 --- a/app/student/forms/page.tsx +++ b/app/student/forms/page.tsx @@ -5,9 +5,6 @@ import { useRouter } from "next/navigation"; import { FormService } from "@/lib/api/services"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMyForms } from "./myforms.ctx"; -import { FormGenerateView } from "../../../components/forms/FormGenerateView"; -import { FormHistoryView } from "../../../components/forms/FormHistoryView"; -import { useFormsLayout } from "./layout"; import { FORM_TEMPLATES_STALE_TIME, FORM_TEMPLATES_GC_TIME, @@ -34,8 +31,6 @@ export default function FormsPage() { const router = useRouter(); const myForms = useMyForms(); const queryClient = useQueryClient(); - - const { activeView } = useFormsLayout(); const { redirectIfNotLoggedIn, isAuthenticated } = useAuthContext(); // Auth redirect at body level (runs first) @@ -120,18 +115,10 @@ export default function FormsPage() { }, [formFilloutProcess.getAllPending()]); return ( - <> - {/* Show the active view */} - {activeView === "history" ? ( - - ) : ( - !!ft) ?? []} - isLoading={isLoading} - /> - )} - + !!ft) ?? []} + isLoading={isLoading} + /> ); } diff --git a/components/forms/FormHistoryView.tsx b/components/forms/FormHistoryView.tsx index f56913fb..bf2771be 100644 --- a/components/forms/FormHistoryView.tsx +++ b/components/forms/FormHistoryView.tsx @@ -9,10 +9,11 @@ import { FormLog } from "./FormLog"; import { IFormSigningParty } from "@betterinternship/core/forms"; import { cn, formatDate } from "@/lib/utils"; import { Button } from "../ui/button"; -import { useFormsLayout } from "@/app/student/forms/layout"; import { useAppContext } from "@/lib/ctx-app"; +import { useMemo } from "react"; interface FormHistoryViewProps { + formLabel?: string; forms: Array<{ form_process_id?: string; label: string; @@ -31,63 +32,38 @@ interface FormHistoryViewProps { /** * Form History View */ -export function FormHistoryView({ forms }: FormHistoryViewProps) { - const { setActiveView } = useFormsLayout(); - const { isMobile } = useAppContext(); +export function FormHistoryView({ forms, formLabel }: FormHistoryViewProps) { + const filteredForms = useMemo( + () => forms.filter((form) => form.label === formLabel || !formLabel), + [forms, formLabel], + ); return ( -
-
-
-
- - Form History -
- {forms.length} generated forms -
- - -
- {forms.length === 0 ? ( -
-

No forms yet

-

- You haven't generated any forms yet. Generate your first form to - get started! -

-
- -
-
- ) : ( - forms - .toSorted( - (a, b) => - Date.parse(b.timestamp ?? "") - Date.parse(a.timestamp ?? ""), - ) - .map((form, index) => ( - - )) - )} -
+
+
+ {filteredForms.length === 0 ? ( + <> + ) : ( + forms + .toSorted( + (a, b) => + Date.parse(b.timestamp ?? "") - Date.parse(a.timestamp ?? ""), + ) + .map((form, index) => ( + + )) + )}
); From be6316a4fdcd2b20ec364dc8c2a9d139c0c10460 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 16:13:49 +0800 Subject: [PATCH 31/97] patch: make the form gen collapsible so it doesnt intrude history --- .../forms/flow-test/FlowTestSigningLayout.tsx | 18 +- app/student/forms/flow-test/page.tsx | 209 +++++++++++++----- components/ui/accordion.tsx | 102 ++++----- components/ui/timeline.tsx | 2 +- 4 files changed, 201 insertions(+), 130 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index 9e54919f..0e83fc85 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -4,8 +4,6 @@ import { ChevronLeft } from "lucide-react"; import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; import { Button } from "@/components/ui/button"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; -import { FormHistoryView } from "@/components/forms/FormHistoryView"; -import { IFormSigningParty } from "@betterinternship/core/forms"; interface SigningRecipient { signatory_title: string; @@ -16,19 +14,6 @@ interface SigningRecipient { interface FlowTestSigningLayoutProps { formLabel?: string; - generatedForms?: { - form_process_id?: string; - label: string; - prefilled_document_id?: string | null; - pending_document_id?: string | null; - signed_document_id?: string | null; - latest_document_url?: string | null; - timestamp: string; - signing_parties?: IFormSigningParty[]; - status?: string | null; - rejection_reason?: string; - pending?: boolean; - }[]; documentUrl?: string; recipients: SigningRecipient[]; onBack: () => void; @@ -36,7 +21,6 @@ interface FlowTestSigningLayoutProps { export function FlowTestSigningLayout({ formLabel, - generatedForms, documentUrl, recipients, onBack, @@ -78,7 +62,7 @@ export function FlowTestSigningLayout({

{formLabel}

-

+

These people will receive a copy of this form, in this order:

diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index b301e16c..3739663e 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -7,6 +7,12 @@ import { Card } from "@/components/ui/card"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { cn } from "@/lib/utils"; import { Divider } from "@/components/ui/divider"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { FormTemplate } from "@/lib/db/use-moa-backend"; import { Loader } from "@/components/ui/loader"; import { FormInput } from "@/components/EditForm"; @@ -76,6 +82,13 @@ export default function FlowTestPage({ }) ?? [], [formTemplates, searchQuery], ); + const hasHistoryLogs = useMemo( + () => + (generatedForms ?? []).some( + (entry) => entry.label === form.formLabel || !form.formLabel, + ), + [generatedForms, form.formLabel], + ); if (isLoading) return Loading form templates...; @@ -197,7 +210,6 @@ export default function FlowTestPage({ > setIsSigningFlow(false)} @@ -206,7 +218,7 @@ export default function FlowTestPage({
Loading form template... ) : ( -
+

{selectedTemplate?.formLabel} @@ -223,65 +235,140 @@ export default function FlowTestPage({

-
- {recipients.length - ? "These people will receive a copy of this form, in this order:" - : "This form does not require any signatures."} -
+ {hasHistoryLogs ? ( + + + + Generate another + + + + {recipients.map((recipient, index) => ( + + {recipient.signatory_title} + + } + subtitle={ + recipient.signatory_source?._id === + "initiator" && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + +
+
+ + +
+ +
+
+
+
+ ) : ( + <> +
+ {recipients.length + ? "These people will receive a copy of this form, in this order:" + : "This form does not require any signatures."} +
- - {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === "initiator" && ( - - {"you will specify this email"} - - ) - } - isLast={index === recipients.length - 1} - /> - ))} - -
-
- - -
- -
+ + {recipients.map((recipient, index) => ( + + {recipient.signatory_title} + + } + subtitle={ + recipient.signatory_source?._id === + "initiator" && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + +
+
+ + +
+ +
+ + )} ) { - return + ...props +}: React.ComponentProps) { + return ; } function AccordionItem({ - className, - ...props - }: React.ComponentProps) { - return ( - - ) + className, + ...props +}: React.ComponentProps) { + return ( + + ); } function AccordionTrigger({ - className, - children, - ...props - }: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + + ); } function AccordionContent({ - className, - children, - ...props - }: React.ComponentProps) { - return ( - -
{children}
-
- ) + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/components/ui/timeline.tsx b/components/ui/timeline.tsx index aa6f1edb..adaabb35 100644 --- a/components/ui/timeline.tsx +++ b/components/ui/timeline.tsx @@ -40,7 +40,7 @@ export const TimelineItem = ({ {/* Content */}
-
{title}
+
{title}
{subtitle && (
{subtitle}
)} From 3048505a853c84ec642381be75bf15b0aff86403 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 16:49:47 +0800 Subject: [PATCH 32/97] patch: remove signing party modal --- app/student/forms/flow-test/page.tsx | 83 ++++++++++------------------ components/forms/FormHistoryView.tsx | 2 +- components/forms/FormLog.tsx | 5 +- 3 files changed, 33 insertions(+), 57 deletions(-) diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 3739663e..fbcdbe2e 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -18,9 +18,6 @@ import { Loader } from "@/components/ui/loader"; import { FormInput } from "@/components/EditForm"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; -import useModalRegistry from "@/components/modals/modal-registry"; -import { useFormFiller } from "@/components/features/student/forms/form-filler.ctx"; -import { useMyAutofill } from "@/hooks/use-my-autofill"; import { FlowTestSigningLayout } from "./FlowTestSigningLayout"; import { IFormSigningParty } from "@betterinternship/core/forms"; import { FormHistoryView } from "@/components/forms/FormHistoryView"; @@ -59,13 +56,7 @@ export default function FlowTestPage({ const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isSigningFlow, setIsSigningFlow] = useState(false); const form = useFormRendererContext(); - const formFiller = useFormFiller(); const recipients = form.formMetadata.getSigningParties(); - const signingPartyBlocks = - form.formMetadata.getSigningPartyBlocks("initiator"); - - const modalRegistry = useModalRegistry(); - const autofillValues = useMyAutofill(); const filteredTemplates = useMemo( () => formTemplates @@ -188,10 +179,10 @@ export default function FlowTestPage({
- {hasHistoryLogs ? ( - + Generate another - - - {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === - "initiator" && ( - - {"you will specify this email"} + + {recipients.length > 1 && ( + + {recipients.map((recipient, index) => ( + + {recipient.signatory_title} - ) - } - isLast={index === recipients.length - 1} - /> - ))} - + } + subtitle={ + recipient.signatory_source?._id === + "initiator" && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + + )}
)} - ) : ( - forms + filteredForms .toSorted( (a, b) => Date.parse(b.timestamp ?? "") - Date.parse(a.timestamp ?? ""), diff --git a/components/forms/FormLog.tsx b/components/forms/FormLog.tsx index 55048934..eb96471b 100644 --- a/components/forms/FormLog.tsx +++ b/components/forms/FormLog.tsx @@ -60,7 +60,7 @@ export const FormLog = ({ if (pending) { return ( -
+
{/* Status Badge */}
@@ -91,13 +91,12 @@ export const FormLog = ({ return (
downloadUrl && status === "done" ? handleDownload() : setIsOpen(!isOpen) } >
- {/* Status Badge */}
{status === "rejected" ? ( Date: Mon, 2 Mar 2026 21:58:49 +0800 Subject: [PATCH 33/97] chore: fix qol issues --- app/student/forms/flow-test/page.tsx | 437 ++++++++++++++------------- 1 file changed, 227 insertions(+), 210 deletions(-) diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index fbcdbe2e..83a37ab6 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -1,7 +1,13 @@ "use client"; import { useMemo, useState } from "react"; -import { ChevronRight, Eye, PenLineIcon, SearchIcon } from "lucide-react"; +import { + ChevronRight, + Eye, + FileSearch, + PenLineIcon, + SearchIcon, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; @@ -15,7 +21,6 @@ import { } from "@/components/ui/accordion"; import { FormTemplate } from "@/lib/db/use-moa-backend"; import { Loader } from "@/components/ui/loader"; -import { FormInput } from "@/components/EditForm"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; import { FlowTestPreviewModal } from "./FlowTestPreviewModal"; import { FlowTestSigningLayout } from "./FlowTestSigningLayout"; @@ -52,26 +57,18 @@ export default function FlowTestPage({ [formTemplates, selectedTemplateName], ); - const [searchQuery, setSearchQuery] = useState(""); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isSigningFlow, setIsSigningFlow] = useState(false); const form = useFormRendererContext(); const recipients = form.formMetadata.getSigningParties(); - const filteredTemplates = useMemo( + const sortedTemplates = useMemo( () => - formTemplates - ?.filter((template) => - searchQuery - .toLowerCase() - .split(" ") - .every((q) => template.formLabel.toLowerCase().includes(q)), - ) - ?.toSorted((a, b) => { - const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); - const bLabel = b.formLabel.replaceAll(/[()[\]\-,]/g, ""); - return aLabel.localeCompare(bLabel); - }) ?? [], - [formTemplates, searchQuery], + formTemplates?.toSorted((a, b) => { + const aLabel = a.formLabel.replaceAll(/[()[\]\-,]/g, ""); + const bLabel = b.formLabel.replaceAll(/[()[\]\-,]/g, ""); + return aLabel.localeCompare(bLabel); + }) ?? [], + [formTemplates], ); const hasHistoryLogs = useMemo( () => @@ -108,57 +105,48 @@ export default function FlowTestPage({ : "translate-x-0 opacity-100", )} > -
+

Form Templates

-
- setSearchQuery(value)} - > - -
-
- {filteredTemplates?.map((template) => { - const isActive = template.formName === selectedTemplateName; - return ( - - ); - })} -
+ + + ); + })} +
+ ) : ( +
+ We currently don't automate forms for your department.
+
+ Have a copy of your department's form templates?
+ + Message us so we can help you out! + +
+ )}
@@ -185,176 +187,191 @@ export default function FlowTestPage({ : "md:translate-x-0 opacity-100 max-w-5xl mx-auto", )} > -
+ {selectedTemplateName ? (
- setIsSigningFlow(false)} - /> -
+
+ setIsSigningFlow(false)} + /> +
-
- {form.loading || form.document.name !== selectedTemplateName ? ( - Loading form template... - ) : ( -
-
-

- {selectedTemplate?.formLabel} -

- -
- {hasHistoryLogs ? ( - - - - Generate another - - - {recipients.length > 1 && ( - - {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === - "initiator" && ( - - {"you will specify this email"} +
+ {form.loading || form.document.name !== selectedTemplateName ? ( + Loading form template... + ) : ( +
+
+

+ {selectedTemplate?.formLabel} +

+ +
+ {hasHistoryLogs ? ( + + + + Generate another + + + {recipients.length > 1 && ( + + {recipients.map((recipient, index) => ( + + {recipient.signatory_title} - ) + } + subtitle={ + recipient.signatory_source?._id === + "initiator" && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + + )} +
+
+ + + > + + Sign via BetterInternship + +
+
+
+
+ ) : ( + <> +
+ {recipients.length + ? "These people will receive a copy of this form, in this order:" + : "This form does not require any signatures."} +
+ + + {recipients.map((recipient, index) => ( + + {recipient.signatory_title} + + } + subtitle={ + recipient.signatory_source?._id === + "initiator" && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ))} + +
+
+
- - - - ) : ( - <> -
- {recipients.length - ? "These people will receive a copy of this form, in this order:" - : "This form does not require any signatures."} -
- - - {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === - "initiator" && ( - - {"you will specify this email"} - - ) - } - isLast={index === recipients.length - 1} - /> - ))} - -
-
-
- -
- - )} - + + )} + +
+ )} +
+
+ ) : ( +
+ + {sortedTemplates.length ? ( +
+ Click on a form template to view. +
+ ) : ( +
+ We don't have form templates for your department.
)}
-
+ )}
{form.document.url && ( From 27f801739472fef930a3618a3cd925ac22a7444f Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 22:29:18 +0800 Subject: [PATCH 34/97] feat: add inputs for recipient emails --- .../forms/flow-test/FlowTestSigningLayout.tsx | 66 ++++++++++++------- app/student/forms/flow-test/page.tsx | 8 +-- components/ui/timeline.tsx | 15 ++++- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index 0e83fc85..ac080e92 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -4,6 +4,8 @@ import { ChevronLeft } from "lucide-react"; import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; import { Button } from "@/components/ui/button"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; +import { FormInput } from "@/components/EditForm"; +import { useState } from "react"; interface SigningRecipient { signatory_title: string; @@ -25,6 +27,10 @@ export function FlowTestSigningLayout({ recipients, onBack, }: FlowTestSigningLayoutProps) { + const [recipientEmails, setRecipientEmails] = useState< + Record + >({}); + return (
@@ -62,34 +68,48 @@ export function FlowTestSigningLayout({

{formLabel}

-

- These people will receive a copy of this form, in this - order: -

- {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === "initiator" && ( - - you will specify this email - - ) - } - isLast={index === recipients.length - 1} - /> - ))} + {recipients.map((recipient, index) => { + const fromMe = + recipient.signatory_source?._id === "initiator"; + return ( + + setRecipientEmails({ + ...recipientEmails, + [recipient.signatory_title]: value, + }) + } + /> + ) + } + isLast={index === recipients.length - 1} + /> + ); + })}
+
+

+ + Don't know the recipient emails? That's okay: +
+ Enter a contact who can forward it to the correct address. +
+

+
diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 83a37ab6..a8c7405a 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -1,13 +1,7 @@ "use client"; import { useMemo, useState } from "react"; -import { - ChevronRight, - Eye, - FileSearch, - PenLineIcon, - SearchIcon, -} from "lucide-react"; +import { ChevronRight, Eye, FileSearch, PenLineIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; diff --git a/components/ui/timeline.tsx b/components/ui/timeline.tsx index adaabb35..67390ad7 100644 --- a/components/ui/timeline.tsx +++ b/components/ui/timeline.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { CheckIcon } from "lucide-react"; +import { CheckIcon, MailWarningIcon } from "lucide-react"; export const Timeline = ({ children }: { children: React.ReactNode }) => { return
{children}
; @@ -11,6 +11,7 @@ interface TimelineItemProps { subtitle?: React.ReactNode; isLast?: boolean; children?: React.ReactNode; + fromMe?: boolean; } export const TimelineItem = ({ @@ -19,8 +20,10 @@ export const TimelineItem = ({ subtitle, isLast = false, children, + fromMe, }: TimelineItemProps) => { const isCheckmark = number === -1; + const isExclamation = fromMe; return (
@@ -29,6 +32,10 @@ export const TimelineItem = ({
+ ) : isExclamation ? ( +
+ +
) : (
{number} @@ -40,7 +47,11 @@ export const TimelineItem = ({ {/* Content */}
-
{title}
+ {fromMe ? ( +
{title}
+ ) : ( +
{title}
+ )} {subtitle && (
{subtitle}
)} From cc6055f69979be7c1d49856c90ad4895483c9bac Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 23:07:14 +0800 Subject: [PATCH 35/97] patch: next handling of email inputs --- app/student/forms/[name]/page.tsx | 66 ------ .../forms/flow-test/FlowTestSigningLayout.tsx | 190 +++++++++++++----- app/student/forms/flow-test/page.tsx | 2 +- 3 files changed, 140 insertions(+), 118 deletions(-) delete mode 100644 app/student/forms/[name]/page.tsx diff --git a/app/student/forms/[name]/page.tsx b/app/student/forms/[name]/page.tsx deleted file mode 100644 index 35e30587..00000000 --- a/app/student/forms/[name]/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; -import { FormAndDocumentLayout } from "@/components/features/student/forms/FormFlowRouter"; -import { useFormsLayout } from "../layout"; -import { useParams } from "next/navigation"; -import { useEffect } from "react"; -import { toast } from "sonner"; -import { configure } from "@betterinternship/components"; - -// ! move this to the topmost client component you can find -configure({ orchestratorApi: "https://orca.betterinternship.com/process" }); - -/** - * The individual form page. - * Allows viewing an individual form. - */ -export default function FormPage() { - const params = useParams(); - const form = useFormRendererContext(); - - const { setCurrentFormName, setCurrentFormLabel } = useFormsLayout(); - - // Show mobile notice toast on mount - useEffect(() => { - const isMobile = window.innerWidth < 640; // sm breakpoint - if (isMobile) { - toast( - "Our desktop experience might currently be preferable, so let us know if you have insights about how we can make mobile better! Chat us on Facebook or email us at hello@betterinternship.com if you go through any issues.", - { - duration: 6000, - className: "text-justify", - }, - ); - } - }, []); - - useEffect(() => { - const { name } = params; - form.updateFormName(name as string); - setCurrentFormName(name as string); - - return () => setCurrentFormName(null); - }, [params, setCurrentFormName, form]); - - useEffect(() => { - setCurrentFormLabel(form.formLabel); - }, [form.formLabel, setCurrentFormLabel]); - - // Warn user before unloading the page - useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - e.preventDefault(); - e.returnValue = ""; - }; - - window.addEventListener("beforeunload", handleBeforeUnload); - return () => window.removeEventListener("beforeunload", handleBeforeUnload); - }, []); - - return ( -
- -
- ); -} diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index ac080e92..7f7726ca 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -2,10 +2,18 @@ import { ChevronLeft } from "lucide-react"; import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; +import { FormFillerRenderer } from "@/components/features/student/forms/FormFillerRenderer"; import { Button } from "@/components/ui/button"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { FormInput } from "@/components/EditForm"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; +import { useFormFiller } from "@/components/features/student/forms/form-filler.ctx"; +import { useMyAutofill } from "@/hooks/use-my-autofill"; +import { + getBlockField, + isBlockField, +} from "@/components/features/student/forms/utils"; interface SigningRecipient { signatory_title: string; @@ -27,21 +35,68 @@ export function FlowTestSigningLayout({ recipients, onBack, }: FlowTestSigningLayoutProps) { + const form = useFormRendererContext(); + const formFiller = useFormFiller(); + const autofillValues = useMyAutofill(); + const [values, setValues] = useState({}); const [recipientEmails, setRecipientEmails] = useState< Record >({}); + const [rightPaneStep, setRightPaneStep] = useState<"timeline" | "fields">( + "timeline", + ); + + const manualBlocks = useMemo( + () => + form.blocks.filter( + (block) => + isBlockField(block) && getBlockField(block)?.source === "manual", + ), + [form.blocks], + ); + + const manualKeyedFields = useMemo(() => { + if (!form.keyedFields || form.keyedFields.length === 0) return []; + + // Get field names from manual blocks + const manualFieldNames = new Set( + manualBlocks.map((block) => getBlockField(block)?.field).filter(Boolean), + ); + + // Filter keyedFields to only those in manual blocks + return form.keyedFields.filter((kf) => manualFieldNames.has(kf.field)); + }, [form.keyedFields, manualBlocks]); + + const nextEnabled = useMemo(() => { + switch (rightPaneStep) { + case "timeline": + return recipients.every( + (recipient) => + !!recipientEmails[recipient.signatory_title] || + recipient.signatory_source?._id !== "initiator", + ); + case "fields": + return true; + } + }, [recipients, recipientEmails, rightPaneStep]); return (
-
+
+

+ {formLabel} +

@@ -52,8 +107,8 @@ export function FlowTestSigningLayout({ {documentUrl ? ( ) : (
@@ -62,53 +117,86 @@ export function FlowTestSigningLayout({ )}
-
-
-
-

- {formLabel} -

+
+
+
+
+

+ These people will receive this form, in this order: +

+ + {recipients.map((recipient, index) => { + const fromMe = + recipient.signatory_source?._id === "initiator"; + return ( + + setRecipientEmails({ + ...recipientEmails, + [recipient.signatory_title]: value, + }) + } + /> + ) + } + isLast={index === recipients.length - 1} + /> + ); + })} + +
+
+

+ + Don't know the recipient emails? That's okay: +
+ Enter a contact who can forward it to the correct + address. +
+

+
+
+
+
+ +
- - - {recipients.map((recipient, index) => { - const fromMe = - recipient.signatory_source?._id === "initiator"; - return ( - - setRecipientEmails({ - ...recipientEmails, - [recipient.signatory_title]: value, - }) - } - /> - ) - } - isLast={index === recipients.length - 1} - /> - ); - })} -
-
-

- - Don't know the recipient emails? That's okay: -
- Enter a contact who can forward it to the correct address. -
-

+ +
+
+ +
diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index a8c7405a..ec5b5f17 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -289,7 +289,7 @@ export default function FlowTestPage({ <>
{recipients.length - ? "These people will receive a copy of this form, in this order:" + ? "These people will receive this form, in this order:" : "This form does not require any signatures."}
From df8788f417f59d15121fcb58bc1db596301cc1f0 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Mon, 2 Mar 2026 23:52:56 +0800 Subject: [PATCH 36/97] feat: 3-step flow for signing --- .../forms/flow-test/FlowTestSigningLayout.tsx | 253 ++++++++++++------ .../student/forms/FormFillerRenderer.tsx | 12 +- 2 files changed, 170 insertions(+), 95 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index 7f7726ca..1a2e9ec5 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -6,10 +6,9 @@ import { FormFillerRenderer } from "@/components/features/student/forms/FormFill import { Button } from "@/components/ui/button"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { FormInput } from "@/components/EditForm"; +import { cn } from "@/lib/utils"; import { useMemo, useState } from "react"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; -import { useFormFiller } from "@/components/features/student/forms/form-filler.ctx"; -import { useMyAutofill } from "@/hooks/use-my-autofill"; import { getBlockField, isBlockField, @@ -36,15 +35,13 @@ export function FlowTestSigningLayout({ onBack, }: FlowTestSigningLayoutProps) { const form = useFormRendererContext(); - const formFiller = useFormFiller(); - const autofillValues = useMyAutofill(); const [values, setValues] = useState({}); const [recipientEmails, setRecipientEmails] = useState< Record >({}); - const [rightPaneStep, setRightPaneStep] = useState<"timeline" | "fields">( - "timeline", - ); + const [rightPaneStep, setRightPaneStep] = useState< + "timeline" | "fields" | "confirm" + >("timeline"); const manualBlocks = useMemo( () => @@ -77,8 +74,12 @@ export function FlowTestSigningLayout({ ); case "fields": return true; + case "confirm": + return true; } }, [recipients, recipientEmails, rightPaneStep]); + const stepNumber = + rightPaneStep === "timeline" ? 1 : rightPaneStep === "fields" ? 2 : 3; return (
@@ -100,10 +101,25 @@ export function FlowTestSigningLayout({
-
-
-
-
+
+
+
+
{documentUrl ? ( -
-
-
-
-

- These people will receive this form, in this order: -

- - {recipients.map((recipient, index) => { - const fromMe = - recipient.signatory_source?._id === "initiator"; - return ( - - setRecipientEmails({ - ...recipientEmails, - [recipient.signatory_title]: value, - }) - } - /> - ) - } - isLast={index === recipients.length - 1} - /> - ); - })} - +
+
+ + Step {stepNumber} of 3 + +
+
+
+
+
+

+ These people will receive this form, in this order: +

+ + {recipients.map((recipient, index) => { + const fromMe = + recipient.signatory_source?._id === "initiator"; + return ( + + setRecipientEmails({ + ...recipientEmails, + [recipient.signatory_title]: value, + }) + } + /> + ) + } + isLast={index === recipients.length - 1} + /> + ); + })} + +
+
+

+ + Don't know the recipient emails? That's okay: +
+ Enter a contact who can forward it to the correct + address. +
+

+
-
-

- - Don't know the recipient emails? That's okay: -
- Enter a contact who can forward it to the correct - address. -
-

+
+
+ + +
-
-
- -
-
-
-
-
- +
+
+
+ +
+
+
+ + +
+
+
+ {rightPaneStep === "confirm" && ( +
+
+ + +
+
+ )}
diff --git a/components/features/student/forms/FormFillerRenderer.tsx b/components/features/student/forms/FormFillerRenderer.tsx index 3121b258..86bbdd53 100644 --- a/components/features/student/forms/FormFillerRenderer.tsx +++ b/components/features/student/forms/FormFillerRenderer.tsx @@ -5,7 +5,6 @@ import { ClientBlock } from "@betterinternship/core/forms"; import { FieldRenderer } from "./FieldRenderer"; import { HeaderRenderer, ParagraphRenderer } from "./BlockRenderer"; import { useFormRendererContext } from "./form-renderer.ctx"; -import { FormActionButtons } from "./FormActionButtons"; import { getBlockField, isBlockField } from "./utils"; import { useFormFiller } from "./form-filler.ctx"; import { useMyAutofill } from "@/hooks/use-my-autofill"; @@ -223,7 +222,7 @@ export function FormFillerRenderer({ onChange={formFiller.setValue} errors={formFiller.errors} setSelected={form.setSelectedPreviewId} - onBlurValidate={(fieldKey, field) => { + onBlurValidate={(fieldKey) => { // Before validating, sync form values to params so validators can access them const currentValues = formFiller.getFinalValues(autofillValues); @@ -256,9 +255,6 @@ export function FormFillerRenderer({ />
-
- -
); } @@ -280,7 +276,7 @@ const BlocksRenderer = ({ onChange: (key: string, value: any) => void; errors: Record; setSelected: (selected: string) => void; - onBlurValidate?: (fieldKey: string, field: any) => void; + onBlurValidate?: (fieldKey: string) => void; fieldRefs: Record; selectedFieldId?: string; }) => { @@ -323,9 +319,7 @@ const BlocksRenderer = ({ field={actualField} value={values[actualField.field]} onChange={(v) => onChange(actualField.field, v)} - onBlur={() => - onBlurValidate?.(actualField.field, actualField) - } + onBlur={() => onBlurValidate?.(actualField.field)} error={errors[actualField.field]} allValues={values} isPhantom={isPhantom} From 04e7e8599b5bae2220736bf32c774e6641d7f9a9 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Tue, 3 Mar 2026 00:21:25 +0800 Subject: [PATCH 37/97] fix: change layout to be more consistent with site --- .../forms/flow-test/FlowTestSigningLayout.tsx | 52 +++++++++++-------- app/student/forms/flow-test/page.tsx | 2 +- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index 1a2e9ec5..b8061e0b 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronLeft } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; import { FormFillerRenderer } from "@/components/features/student/forms/FormFillerRenderer"; import { Button } from "@/components/ui/button"; @@ -82,27 +82,37 @@ export function FlowTestSigningLayout({ rightPaneStep === "timeline" ? 1 : rightPaneStep === "fields" ? 2 : 3; return ( -
-
-

- {formLabel} -

- +
+
+
+
+
+

+ {formLabel} +

+
+ +
+
-
-
+
+
-
+
diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index ec5b5f17..86af9a47 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -177,7 +177,7 @@ export default function FlowTestPage({ className={cn( "flex min-h-0 flex-col bg-background overflow-hidden transition-[transform,opacity] duration-500 ease-in-out will-change-transform w-full", isSigningFlow - ? "md:-translate-x-2 opacity-100 max-w-7xl mx-auto" + ? "md:-translate-x-2 opacity-100" : "md:translate-x-0 opacity-100 max-w-5xl mx-auto", )} > From e58de35f3f4f7ac993cb1a5077dfbbb3bfc08c66 Mon Sep 17 00:00:00 2001 From: neue-dev Date: Tue, 3 Mar 2026 00:45:36 +0800 Subject: [PATCH 38/97] fix: issues with flow movement between form filler and template list --- .../forms/flow-test/FlowTestSigningLayout.tsx | 69 ++++++++++--------- app/student/forms/flow-test/page.tsx | 2 +- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index b8061e0b..a1799f6b 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -86,8 +86,8 @@ export function FlowTestSigningLayout({
-
-

+
+

{formLabel}

@@ -97,7 +97,7 @@ export function FlowTestSigningLayout({ setRightPaneStep("timeline"); onBack(); }} - className="flex items-center gap-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 px-3 py-2 transition-colors" + className="flex shrink-0 items-center gap-2 whitespace-nowrap px-3 py-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900" > Back to Templates @@ -108,8 +108,8 @@ export function FlowTestSigningLayout({
@@ -145,7 +145,7 @@ export function FlowTestSigningLayout({
setRightPaneStep("timeline")} > Previous
- {rightPaneStep === "confirm" && ( -
-
- - -
+
+
+ +
- )} +
diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 86af9a47..0f516203 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -132,7 +132,7 @@ export default function FlowTestPage({ >
-

+

{template.formLabel}

From 9f434f295fc8d926004792e240ad093ecf33c78f Mon Sep 17 00:00:00 2001 From: neue-dev Date: Tue, 3 Mar 2026 01:22:33 +0800 Subject: [PATCH 39/97] feat: add confirm step --- .../forms/flow-test/FlowTestSigningLayout.tsx | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index a1799f6b..f31d8f95 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -7,12 +7,13 @@ import { Button } from "@/components/ui/button"; import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { FormInput } from "@/components/EditForm"; import { cn } from "@/lib/utils"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useFormRendererContext } from "@/components/features/student/forms/form-renderer.ctx"; import { getBlockField, isBlockField, } from "@/components/features/student/forms/utils"; +import { Badge } from "@/components/ui/badge"; interface SigningRecipient { signatory_title: string; @@ -78,9 +79,18 @@ export function FlowTestSigningLayout({ return true; } }, [recipients, recipientEmails, rightPaneStep]); + const fromMe = recipients.some( + (recipient) => recipient.signatory_source?._id === "initiator", + ); const stepNumber = rightPaneStep === "timeline" ? 1 : rightPaneStep === "fields" ? 2 : 3; + // Clean up when switching form + useEffect(() => { + setRecipientEmails({}); + setValues({}); + }, [formLabel]); + return (
@@ -204,12 +214,14 @@ export function FlowTestSigningLayout({

- - Don't know the recipient emails? That's okay: -
- Enter a contact who can forward it to the correct - address. -
+ {fromMe && ( + + Don't know the recipient emails? That's okay: +
+ Enter a contact who can forward it to the correct + address. +
+ )}

@@ -272,13 +284,30 @@ export function FlowTestSigningLayout({
-
+
+ These emails will receive the form at some point: + {Object.entries(recipientEmails).map( + ([recipientTitle, recipientEmail]) => { + return ( +
+ + {recipientTitle}: + + {recipientEmail} + + +
+ ); + }, + )} +
+
+ /> @@ -271,7 +324,8 @@ export function FlowTestSigningLayout({ diff --git a/app/student/forms/flow-test/page.tsx b/app/student/forms/flow-test/page.tsx index 0f516203..331ededf 100644 --- a/app/student/forms/flow-test/page.tsx +++ b/app/student/forms/flow-test/page.tsx @@ -231,26 +231,30 @@ export default function FlowTestPage({ {recipients.length > 1 && ( - {recipients.map((recipient, index) => ( - - {recipient.signatory_title} - - } - subtitle={ - recipient.signatory_source?._id === - "initiator" && ( - - {"you will specify this email"} + {recipients.map((recipient, index) => { + const fromMe = + recipient.signatory_source?._id === + "initiator"; + return ( + + {recipient.signatory_title} - ) - } - isLast={index === recipients.length - 1} - /> - ))} + } + subtitle={ + fromMe && ( + + {"you will specify this email"} + + ) + } + isLast={index === recipients.length - 1} + /> + ); + })} )}
@@ -272,7 +276,13 @@ export default function FlowTestPage({ } > - Sign via BetterInternship + {recipients.some( + (recipient) => + recipient.signatory_source?._id === + "initiator", + ) + ? "Sign via BetterInternship" + : "Fillout Document"}
-
+
{ className="block outline-none focus:outline-none border-none" >

BetterInternship logo BetterInternship From 8e0c7c919828c9efca66dd5bade23786cf1d2383 Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Tue, 3 Mar 2026 12:07:07 +0800 Subject: [PATCH 44/97] fix: app crash on displaying job details in some instances --- app/student/search/[job_id]/page.tsx | 5 ++++- app/student/search/page.tsx | 1 + components/features/hire/dashboard/JobsContent.tsx | 2 +- components/features/hire/listings/jobDetails.tsx | 3 +++ .../features/hire/listings/listings-details-panel.tsx | 4 ++++ components/modals/components/MassApplyJobsSelector.tsx | 7 ++++++- components/shared/jobs.tsx | 5 +++-- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/student/search/[job_id]/page.tsx b/app/student/search/[job_id]/page.tsx index 887c282c..7caf09b7 100644 --- a/app/student/search/[job_id]/page.tsx +++ b/app/student/search/[job_id]/page.tsx @@ -27,6 +27,7 @@ import { ApplyConfirmModal } from "@/components/modals/ApplyConfirmModal"; import { applyToJob } from "@/lib/application"; import { useApplicationActions } from "@/lib/api/student.actions.api"; import { ShareJobButton } from "@/components/features/student/job/share-job-button"; +import { useAuthContext } from "@/lib/ctx-auth"; /** * The individual job page. @@ -44,6 +45,7 @@ export default function JobPage() { const profile = useProfileData(); const { universities } = useDbRefs(); + const { isAuthenticated } = useAuthContext(); const goProfile = useCallback(() => { applyConfirmModalRef.current?.close(); @@ -134,7 +136,8 @@ export default function JobPage() { portfolio_link: profile.data?.portfolio_link ?? null, }} job={job.data} - /> + isAuthenticated={isAuthenticated()} + />

diff --git a/app/student/search/page.tsx b/app/student/search/page.tsx index b6d995c7..125391a3 100644 --- a/app/student/search/page.tsx +++ b/app/student/search/page.tsx @@ -597,6 +597,7 @@ export default function SearchPage() { openAppModal={() => applyConfirmModalRef.current?.open()} />, ]} + isAuthenticated={isAuthenticated()} /> ) : (
diff --git a/components/features/hire/dashboard/JobsContent.tsx b/components/features/hire/dashboard/JobsContent.tsx index a1cbfd06..aa59a049 100644 --- a/components/features/hire/dashboard/JobsContent.tsx +++ b/components/features/hire/dashboard/JobsContent.tsx @@ -70,7 +70,7 @@ export function JobsContent({ animate={{ scale: 1, filter: "blur(0px)", opacity: 1 }} transition={{ duration: 0.3, ease: "easeOut" }} > - {sortedJobs && sortedJobs.length > 0 + {sortedJobs && sortedJobs.length > 0 && !showLoader && !isLoading ? (
{ diff --git a/components/features/hire/listings/jobDetails.tsx b/components/features/hire/listings/jobDetails.tsx index 3f671665..197121b9 100644 --- a/components/features/hire/listings/jobDetails.tsx +++ b/components/features/hire/listings/jobDetails.tsx @@ -9,6 +9,7 @@ import { ArrowLeft } from "lucide-react"; import { useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { useState } from "react"; +import { useAuthContext } from "@/app/hire/authctx"; interface JobDetailsPageProps { job: Job; @@ -19,6 +20,7 @@ const JobDetailsPage = ({ }: JobDetailsPageProps) => { const router = useRouter(); const { isMobile } = useAppContext(); + const { isAuthenticated } = useAuthContext(); const [exitingBack, setExitingBack] = useState(false); const handleBack = () => { @@ -40,6 +42,7 @@ const JobDetailsPage = ({ diff --git a/components/features/hire/listings/listings-details-panel.tsx b/components/features/hire/listings/listings-details-panel.tsx index f2c96e5f..165256c3 100644 --- a/components/features/hire/listings/listings-details-panel.tsx +++ b/components/features/hire/listings/listings-details-panel.tsx @@ -1,3 +1,4 @@ +import { useAuthContext } from "@/app/hire/authctx"; import { JobDetails } from "@/components/shared/jobs"; import { Button } from "@/components/ui/button"; import { Job } from "@/lib/db/db.types"; @@ -28,6 +29,8 @@ export function ListingsDetailsPanel({ updateJob, setIsEditing, }: ListingsDetailsPanelProps) { + const { isAuthenticated } = useAuthContext(); + if (!selectedJob?.id) { return (
@@ -58,6 +61,7 @@ export function ListingsDetailsPanel({ job={selectedJob} // @ts-ignore update_job={updateJob} + isAuthenticated={isAuthenticated()} actions={ isEditing ? [ diff --git a/components/modals/components/MassApplyJobsSelector.tsx b/components/modals/components/MassApplyJobsSelector.tsx index c71af192..6e432d1e 100644 --- a/components/modals/components/MassApplyJobsSelector.tsx +++ b/components/modals/components/MassApplyJobsSelector.tsx @@ -6,6 +6,7 @@ import { JobCard, JobDetails } from "@/components/shared/jobs"; import { useJobsData } from "@/lib/api/student.data.api"; import { useMassApply } from "@/lib/api/god.api"; import { Job } from "@/lib/db/db.types"; +import { useAuthContext } from "@/lib/ctx-auth"; interface MassApplyJobsSelectorProps { selectedStudentIds: Set; @@ -22,6 +23,7 @@ export function MassApplyJobsSelector({ const [jobsPage, setJobsPage] = useState(1); const jobsPageSize = 10; const massApply = useMassApply(); + const { isAuthenticated } = useAuthContext(); // Use the same hook as student search page to fetch jobs const jobs = useJobsData({ @@ -161,7 +163,10 @@ export function MassApplyJobsSelector({ {selectedJob ? ( <>
- +
diff --git a/components/shared/jobs.tsx b/components/shared/jobs.tsx index 61867d16..4d7d5821 100644 --- a/components/shared/jobs.tsx +++ b/components/shared/jobs.tsx @@ -1017,6 +1017,7 @@ export function JobDetails({ user, actions = [], applyDisabledText = "Complete required items to apply.", + isAuthenticated, }: { job: Job; user?: { @@ -1025,6 +1026,7 @@ export function JobDetails({ }; actions?: React.ReactNode[]; applyDisabledText?: string; + isAuthenticated: boolean; }) { const hasGithub = !!user?.github_link?.trim(); const hasPortfolio = !!user?.portfolio_link?.trim(); @@ -1037,11 +1039,10 @@ export function JobDetails({ (needsGithub && !hasGithub) || (needsPortfolio && !hasPortfolio); const { isMobile } = useAppContext(); - const { isAuthenticated } = useAuthContext(); return ( <> - {isAuthenticated() && ( + {isAuthenticated && ( Date: Tue, 3 Mar 2026 12:17:36 +0800 Subject: [PATCH 45/97] feat: add buffer when confirming, make it split pane instead --- .../forms/flow-test/FlowTestSigningLayout.tsx | 131 +++++++++++------- 1 file changed, 79 insertions(+), 52 deletions(-) diff --git a/app/student/forms/flow-test/FlowTestSigningLayout.tsx b/app/student/forms/flow-test/FlowTestSigningLayout.tsx index 9caf20d2..c208a3a5 100644 --- a/app/student/forms/flow-test/FlowTestSigningLayout.tsx +++ b/app/student/forms/flow-test/FlowTestSigningLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, LucideClipboardCheck } from "lucide-react"; import { FormPreviewPdfDisplay } from "@/components/features/student/forms/previewer"; import { FormFillerRenderer } from "@/components/features/student/forms/FormFillerRenderer"; import { Button } from "@/components/ui/button"; @@ -19,6 +19,7 @@ import { useMyAutofill, useMyAutofillUpdate } from "@/hooks/use-my-autofill"; import { toast } from "sonner"; import { toastPresets } from "@/components/ui/sonner-toast"; import { FormValues } from "@betterinternship/core/forms"; +import { TextLoader } from "@/components/ui/loader"; interface SigningRecipient { signatory_title: string; @@ -48,6 +49,7 @@ export function FlowTestSigningLayout({ const [recipientEmails, setRecipientEmails] = useState< Record >({}); + const [confirmStepBuffering, setConfirmStepBuffering] = useState(false); const [rightPaneStep, setRightPaneStep] = useState< "timeline" | "fields" | "confirm" >("timeline"); @@ -148,6 +150,13 @@ export function FlowTestSigningLayout({ setRightPaneStep(fromMe ? "timeline" : "fields"); }, [formLabel, recipients]); + // Buffer + useEffect(() => { + if (rightPaneStep === "confirm") setConfirmStepBuffering(true); + const timeout = setTimeout(() => setConfirmStepBuffering(false), 1500); + return () => clearTimeout(timeout); + }, [rightPaneStep]); + return (
@@ -176,7 +185,7 @@ export function FlowTestSigningLayout({
@@ -184,8 +193,7 @@ export function FlowTestSigningLayout({ className="grid min-h-0 flex-1 grid-cols-1 transition-[grid-template-columns] duration-500 ease-in-out xl:[grid-template-columns:minmax(0,1fr)_var(--right-pane-width)]" style={ { - "--right-pane-width": - rightPaneStep === "confirm" ? "0px" : "600px", + "--right-pane-width": "600px", } as React.CSSProperties } > @@ -193,7 +201,7 @@ export function FlowTestSigningLayout({ className={cn( "min-h-0 bg-white rounded-r-none transition-[transform] duration-500 ease-in-out", rightPaneStep === "confirm" - ? "xl:scale-[1.01]" + ? "xl:scale-[1.005]" : "xl:scale-100", )} > @@ -213,9 +221,7 @@ export function FlowTestSigningLayout({
@@ -316,7 +322,10 @@ export function FlowTestSigningLayout({
-
-
-
-
-
- These emails will receive the form at some point: - {Object.entries(recipientEmails).map( - ([recipientTitle, recipientEmail]) => { - return ( -
- - {recipientTitle}: - - {recipientEmail} - - + +
+
+
+ + Please check that all your inputs are correct + {fromMe && ( +
+ Make sure these emails are right: +
+ {Object.entries(recipientEmails).map( + ([recipientTitle, recipientEmail], index) => ( +
+ + + {index + 1}. {recipientTitle}: + + + {recipientEmail} + + +
+ ), + )} +
+
+ )}
- ); - }, - )} -
-
- - +
+
+ + +
+
+
+
+
From e1a5431d9e49cd2cc906da75d3ca46f4f8b0d7b9 Mon Sep 17 00:00:00 2001 From: Jay Carlos Date: Tue, 3 Mar 2026 12:19:21 +0800 Subject: [PATCH 46/97] feat: add currency formatter utility function --- components/shared/jobs.tsx | 15 +++++++-------- lib/utils/num-utils.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/components/shared/jobs.tsx b/components/shared/jobs.tsx index 4d7d5821..328c0ba6 100644 --- a/components/shared/jobs.tsx +++ b/components/shared/jobs.tsx @@ -2,7 +2,7 @@ import { Badge, BoolBadge } from "@/components/ui/badge"; import { Job } from "@/lib/db/db.types"; import { useDbMoa } from "@/lib/db/use-bi-moa"; import { useDbRefs } from "@/lib/db/use-refs"; -import { cn } from "@/lib/utils"; +import { cn, formatCurrency } from "@/lib/utils"; import { AlertTriangle, Building, @@ -439,7 +439,7 @@ export const JobDetailsSummary = ({ job }: { job: Job }) => {
{workModes ? ( -
+