Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"jose": "^6.2.2",
"lucide-react": "^0.548.0",
"next": "15.5.7",
"prisma": "^6.15.0",
Expand All @@ -67,6 +68,7 @@
"react-hook-form": "^7.65.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"react-use-websocket": "^4.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.9"
Expand Down
31 changes: 21 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 21 additions & 11 deletions src/app/(web)/(oa)/assessment/[assessmentId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import AssessmentOutro from '@/lib/components/assessment-flow/AssessmentOutro';
import AssessmentSidebar from '@/lib/components/assessment-flow/AssessmentSidebar';
import AssessmentNavbar from '@/lib/components/assessment-flow/AssessmentNavbar';
import AssessmentContent from '@/lib/components/assessment-flow/AssessmentContent';
import { useHeartbeat } from '@/lib/hooks/useHeartbeat';
import { LostConnectionModal } from '@/lib/components/modal/LostConnectionModal';
import AssessmentSkeleton from '@/lib/components/assessment-flow/AssessmentSkeleton';

export default function AssessmentPage({ params }: { params: Promise<{ assessmentId: string }> }) {
const { assessmentId } = use(params);
const assessment = useAssessment(assessmentId);
const { isConnected } = useHeartbeat(assessment.token ?? null);

if (assessment.isLoading)
return (
Expand Down Expand Up @@ -46,6 +50,8 @@ export default function AssessmentPage({ params }: { params: Promise<{ assessmen

return (
<div className="flex h-screen w-full flex-col overflow-hidden">
{/* onOpenChange is returning nothing as we don't have recovery implemented just yet */}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you say "recovery," do you mean a client disconnects and then reconnects after a brief delay?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is to tee us nicely for handling us having a seperate countdown for clients who have disconnected, and give them a chance to reconnect.

<LostConnectionModal open={!isConnected} onOpenChange={() => {}} />
<AssessmentNavbar candidateName={assessment.candidateName} />
<div className="flex flex-1 overflow-hidden">
<AssessmentSidebar
Expand All @@ -54,17 +60,21 @@ export default function AssessmentPage({ params }: { params: Promise<{ assessmen
formattedTime={assessment.timer.formattedTime}
/>
<main className="flex-1 overflow-hidden">
<AssessmentContent
currentSection={assessment.currentSection}
availableLanguages={assessment.availableLanguages}
publicTestCases={assessment.publicTestCases}
testCaseResults={assessment.testCaseResults}
isTransitioning={assessment.isTransitioning}
onLanguageChange={assessment.changeLanguage}
onEditorMount={assessment.handleEditorMount}
onRunTests={assessment.runTests}
onSubmit={assessment.submitAndContinue}
/>
{!isConnected ? (
<AssessmentSkeleton />
) : (
<AssessmentContent
currentSection={assessment.currentSection}
availableLanguages={assessment.availableLanguages}
publicTestCases={assessment.publicTestCases}
testCaseResults={assessment.testCaseResults}
isTransitioning={assessment.isTransitioning}
onLanguageChange={assessment.changeLanguage}
onEditorMount={assessment.handleEditorMount}
onRunTests={assessment.runTests}
onSubmit={assessment.submitAndContinue}
/>
)}
</main>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/app/api/oa/[assessmentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export async function GET(
{ params }: { params: Promise<{ assessmentId: string }> }
) {
try {
// TODO: add route security
const { assessmentId } = await params;
const result = await AssessmentService.getAssessmentForCandidate(assessmentId);
return Response.json({ data: result }, { status: 200 });
Expand Down
17 changes: 17 additions & 0 deletions src/app/api/token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TokenSchema } from '@/lib/schemas/token.schema';
import { handleError } from '@/lib/utils/errors.utils';
import { generateToken } from '@/lib/utils/token.utils';
import { type NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
try {
// TODO: add route security
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we made a ticket for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. Will be written, but not urgent for showcase

const body = await request.json();
const parsed = TokenSchema.parse(body);
const token = await generateToken(parsed.email);

return Response.json({ data: token }, { status: 201 });
} catch (err) {
return handleError(err);
}
}
20 changes: 20 additions & 0 deletions src/lib/api/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* POST /api/token
*/
export async function createToken(email: string): Promise<string> {
const res = await fetch(`/api/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});

const json = await res.json();

if (!res.ok) {
throw new Error(json.message);
}

return json.data;
}
61 changes: 61 additions & 0 deletions src/lib/components/assessment-flow/AssessmentSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Skeleton } from '@/lib/components/ui/Skeleton';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to note (I saw this post abt this package that does exact skeleton according to the content - not really needed here but could be something so that skeleton is just a wrapper and we don't have to do this in the future) not prio tho

import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from '@/lib/components/ui/Resizable';
import { useMemo } from 'react';

export default function AssessmentSkeleton() {
// generating random line widths for the skelton code lines
const skeletonCodeLineWidths = useMemo(
() => Array.from({ length: 16 }, () => `${Math.random() * 40 + 30}%`),
[]
);

return (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={35} minSize={20}>
<div className="flex h-full flex-col">
<div className="box-content shrink-0 px-5 pt-4 pb-2">
<Skeleton className="h-9 w-3/4 rounded-md" />
</div>
<div className="flex-1 space-y-3 overflow-hidden px-5 pt-1 pb-4">
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-5/6 rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-4/6 rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-3/4 rounded" />
<div className="space-y-3 pt-2">
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-5/6 rounded" />
<Skeleton className="h-4 w-full rounded" />
</div>
</div>
</div>
</ResizablePanel>

<ResizableHandle className="bg-sarge-gray-200 w-px" />

<ResizablePanel defaultSize={65} minSize={40}>
<div className="flex h-full flex-col overflow-hidden bg-[#3a414f]">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we just doing this hex bc we have nothing in the design system? (idt we have skeleton so would be fine if not)

Copy link
Copy Markdown
Collaborator Author

@LOTaher LOTaher Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hex is the same color as our monacode text editor theme

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eventually we'll make a design token for this then I know olivia has new designs for the editor

<div className="min-h-0 flex-1 space-y-2 px-4 pt-4">
{skeletonCodeLineWidths.map((w, i) => (
<Skeleton key={i} className="h-4 rounded" style={{ width: w }} />
))}
</div>
<div className="border-sarge-gray-200 flex items-center justify-between border-t bg-white px-4 py-3">
<div className="flex gap-2">
<Skeleton className="h-8 w-24 rounded-md" />
</div>
<div className="flex gap-2">
<Skeleton className="h-8 w-24 rounded-md" />
<Skeleton className="h-8 w-24 rounded-md" />
</div>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
);
}
23 changes: 12 additions & 11 deletions src/lib/components/assessment-flow/AssessmentTestCasesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChevronDown, ChevronUp, CircleCheck, CircleX, Play, ChevronRight } from
import TestCaseCard from '@/lib/components/core/TestCaseCard';
import type { TestCaseDTO } from '@/lib/schemas/task-template.schema';
import type { TestCaseResult } from '@/lib/types/candidate-assessment.types';
import { Button } from '@/lib/components/ui/Button';

type AssessmentTestCasesPanelProps = {
testCases: TestCaseDTO[];
Expand Down Expand Up @@ -82,8 +83,8 @@ export default function AssessmentTestCasesPanel({
<div className="flex h-15 flex-shrink-0 items-center justify-between px-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<button
type="button"
<Button
variant="icon"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this is better

onClick={() => setIsOpen((v) => !v)}
className="text-sarge-gray-700"
>
Expand All @@ -92,7 +93,7 @@ export default function AssessmentTestCasesPanel({
) : (
<ChevronUp className="size-5" />
)}
</button>
</Button>
<span className="text-sarge-gray-700 text-sm font-medium">Test Cases</span>
</div>
{hasResults && (
Expand All @@ -113,22 +114,22 @@ export default function AssessmentTestCasesPanel({
)}
</div>
<div className="flex items-center gap-2">
<button
type="button"
<Button
variant="secondary"
onClick={onRunTests}
className="bg-sarge-gray-50 border-sarge-primary-500 text-sarge-primary-500 flex h-9 items-center gap-2 rounded-lg border px-4 text-sm font-medium"
className="flex h-9 items-center gap-2 rounded-lg border px-4 text-sm font-medium"
>
<Play className="size-4" />
Run Tests
</button>
<button
type="button"
</Button>
<Button
variant="primary"
onClick={onSubmit}
className="bg-sarge-primary-500 text-primary-foreground flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium"
className="text-primary-foreground flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium"
>
Submit
<ChevronRight className="size-4" />
</button>
</Button>
</div>
</div>

Expand Down
30 changes: 30 additions & 0 deletions src/lib/components/modal/LostConnectionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Dialog, DialogContent, DialogTitle } from '@/lib/components/ui/Modal';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Button } from '@/lib/components/ui/Button';

export type LostConnectionModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};

export function LostConnectionModal({ open, onOpenChange }: LostConnectionModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="h-[234px] w-[381px] gap-4" showCloseButton={true}>
<VisuallyHidden>
<DialogTitle>Network Disconnected</DialogTitle>
</VisuallyHidden>
<div className="flex flex-col items-center gap-6 text-center">
<p className="text-sarge-gray-800 mt-2 text-base leading-tight font-medium tracking-wide">
You seem to be having issues with your connection.
</p>
<p>
If you disconnect again during this exam, your current progress will be
auto-submitted.
</p>
<Button className="px-4 py-2">I understand</Button>
</div>
</DialogContent>
</Dialog>
);
}
Loading
Loading