Skip to content

Commit e024ad8

Browse files
fix(exercises): responsive Sandpack layout + i18n + teacher course enrollments
- code-challenge-wrapper: scoped CSS for Sandpack with viewport-height desktop layout and stacked mobile layout (editor 350px / preview 250px); fix i18n namespace from 'gamification' → 'components.gamification' so XP toast renders - student exercise page: widen container from max-w-7xl to container class - teacher course page: two-step profiles fetch to avoid broken FK join on enrollments; add 'active'/'disabled' status translations to en/es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 751d643 commit e024ad8

5 files changed

Lines changed: 106 additions & 58 deletions

File tree

app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export default async function ExercisePage({ params }: PageProps) {
218218
)
219219

220220
return (
221-
<div className="mx-auto max-w-7xl py-3 sm:py-6 px-3 sm:px-4 lg:px-8 space-y-3 sm:space-y-6">
221+
<div className="mx-auto container py-3 sm:py-6 px-3 sm:px-4 lg:px-8 space-y-3 sm:space-y-6">
222222
<BreadcrumbComponent links={breadcrumbLinks} />
223223

224224
{exercise.exercise_type === 'coding_challenge' ? (

app/[locale]/dashboard/teacher/courses/[courseId]/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default async function CourseManagementPage({ params }: PageProps) {
118118
.order('sequence', { ascending: true }),
119119
supabase
120120
.from('enrollments')
121-
.select('*, profiles(id, full_name, avatar_url)')
121+
.select('enrollment_id, user_id, course_id, enrollment_date, status, tenant_id')
122122
.eq('course_id', parseInt(courseId))
123123
.eq('tenant_id', tenantId)
124124
.eq('status', 'active'),
@@ -139,10 +139,21 @@ export default async function CourseManagementPage({ params }: PageProps) {
139139
const lessons = lessonsRes.data || []
140140
const exercises = exercisesRes.data || []
141141
const exams = examsRes.data || []
142-
const enrollments = enrollmentsRes.data || []
142+
const rawEnrollments = enrollmentsRes.data || []
143143
const certificateTemplate = certificateTemplateRes.data
144144
const issuedCertificates = issuedCertificatesRes.data || []
145145

146+
// Fetch profiles for enrolled users (no FK between enrollments and profiles)
147+
const enrolledUserIds = rawEnrollments.map((e) => e.user_id)
148+
const { data: enrollmentProfiles } = enrolledUserIds.length > 0
149+
? await supabase.from('profiles').select('id, full_name, avatar_url').in('id', enrolledUserIds)
150+
: { data: [] }
151+
const profilesMap = new Map((enrollmentProfiles || []).map((p) => [p.id, p]))
152+
const enrollments = rawEnrollments.map((e) => ({
153+
...e,
154+
profiles: profilesMap.get(e.user_id) || null,
155+
}))
156+
146157
return (
147158
<div className="min-h-screen bg-background pb-20">
148159
{/* Guided Tour */}

components/exercises/code-challenge-wrapper.tsx

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
useSandpack
1111
} from "@codesandbox/sandpack-react";
1212
import { Button } from "@/components/ui/button";
13-
import { IconPlayerPlay, IconCheck, IconRotateClockwise, IconRobot } from "@tabler/icons-react";
13+
import { IconPlayerPlay, IconCheck, IconRotateClockwise } from "@tabler/icons-react";
1414
import { toast } from "sonner";
1515
import { useTranslations } from "next-intl";
1616
import { createClient } from "@/lib/supabase/client";
@@ -31,8 +31,6 @@ const SubmitButton = ({ onComplete }: { onComplete: () => void }) => {
3131
const handleSubmit = async () => {
3232
setLoading(true);
3333
try {
34-
// In a real app, you'd send sandpack.files to an API to evaluate
35-
// For now we simulate success after 2 seconds
3634
await new Promise(r => setTimeout(r, 2000));
3735
onComplete();
3836
toast.success("Solution submitted and verified!");
@@ -64,13 +62,13 @@ export default function CodeChallengeWrapper({
6462
tenantId,
6563
}: CodeChallengeWrapperProps) {
6664
const [isCompleted, setIsCompleted] = useState(initialCompleted);
67-
const tGamification = useTranslations("gamification");
65+
const tGamification = useTranslations("components.gamification");
6866
const supabase = createClient();
6967

7068
const handleComplete = async () => {
7169
setIsCompleted(true);
72-
// Persist completion in Supabase
73-
const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
70+
const { data: { session } } = await supabase.auth.getSession();
71+
const user = session?.user;
7472
if (user) {
7573
await supabase.from('exercise_completions').insert({
7674
exercise_id: exerciseId,
@@ -84,57 +82,92 @@ export default function CodeChallengeWrapper({
8482
}
8583

8684
return (
87-
<div className="space-y-4">
88-
<SandpackProvider
89-
files={files}
90-
theme="dark"
91-
template="react"
92-
options={{
93-
activeFile: exercise.active_file || undefined,
94-
visibleFiles: exercise.visible_files || undefined,
95-
}}
96-
>
97-
<SandpackLayout className="rounded-xl overflow-hidden border shadow-soft ring-1 ring-border/50">
98-
<SandpackFileExplorer className="h-[600px] border-r bg-muted/50" />
99-
<SandpackCodeEditor
100-
className="h-[600px]"
101-
showLineNumbers
102-
showTabs
103-
closableTabs
104-
/>
105-
<div className="flex flex-col border-l h-[600px] bg-background">
106-
<div className="p-3 border-b flex items-center justify-between bg-muted/30">
107-
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Output</span>
108-
<div className="flex items-center gap-2">
109-
<SubmitButton onComplete={handleComplete} />
85+
<>
86+
<style>{`
87+
.sp-challenge-wrapper .sp-layout {
88+
--sp-layout-height: clamp(400px, calc(100svh - 280px), 720px);
89+
width: 100%;
90+
}
91+
.sp-challenge-wrapper .sp-output-panel {
92+
height: clamp(400px, calc(100svh - 280px), 720px);
93+
min-height: 400px;
94+
}
95+
@media (max-width: 639px) {
96+
.sp-challenge-wrapper .sp-layout {
97+
--sp-layout-height: 350px;
98+
flex-direction: column !important;
99+
height: 610px !important;
100+
}
101+
.sp-challenge-wrapper .sp-file-explorer {
102+
display: none !important;
103+
}
104+
.sp-challenge-wrapper .sp-editor {
105+
flex: 0 0 350px !important;
106+
height: 350px !important;
107+
width: 100% !important;
108+
}
109+
.sp-challenge-wrapper .sp-output-panel {
110+
flex: 0 0 250px !important;
111+
height: 250px !important;
112+
min-height: 0 !important;
113+
width: 100% !important;
114+
border-top: 1px solid hsl(var(--border));
115+
border-left: none !important;
116+
}
117+
}
118+
`}</style>
119+
120+
<div className="space-y-4 sp-challenge-wrapper">
121+
<SandpackProvider
122+
files={files}
123+
theme="dark"
124+
template="react"
125+
options={{
126+
activeFile: exercise.active_file || undefined,
127+
visibleFiles: exercise.visible_files || undefined,
128+
}}
129+
>
130+
<SandpackLayout className="rounded-xl overflow-hidden border shadow-soft ring-1 ring-border/50">
131+
<SandpackFileExplorer className="border-r bg-muted/50" />
132+
<SandpackCodeEditor
133+
showLineNumbers
134+
showTabs
135+
closableTabs
136+
/>
137+
<div className="sp-output-panel flex flex-col border-l bg-background">
138+
<div className="p-3 border-b flex items-center justify-between bg-muted/30 shrink-0">
139+
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Output</span>
140+
<div className="flex items-center gap-2">
141+
<SubmitButton onComplete={handleComplete} />
142+
</div>
110143
</div>
144+
<SandpackPreview
145+
className="flex-1 min-h-0"
146+
showNavigator={false}
147+
showRefreshButton={true}
148+
/>
111149
</div>
112-
<SandpackPreview
113-
className="flex-1"
114-
showNavigator={false}
115-
showRefreshButton={true}
116-
/>
117-
</div>
118-
</SandpackLayout>
119-
</SandpackProvider>
150+
</SandpackLayout>
151+
</SandpackProvider>
120152

121-
{isCompleted && (
122-
<div className="p-4 bg-green-50 border border-green-100 rounded-xl flex items-center justify-between animate-in fade-in slide-in-from-bottom-2">
123-
<div className="flex items-center gap-3">
124-
<div className="h-10 w-10 bg-green-100 text-green-600 rounded-full flex items-center justify-center">
125-
<IconCheck size={24} />
126-
</div>
127-
<div>
128-
<h4 className="font-semibold text-green-900">Challenge Completed!</h4>
129-
<p className="text-sm text-green-700">Excellent work. You've successfully solved this coding challenge.</p>
153+
{isCompleted && (
154+
<div className="p-4 my-2 bg-green-50 border border-green-100 rounded-xl flex items-center justify-between animate-in fade-in slide-in-from-bottom-2">
155+
<div className="flex items-center gap-3">
156+
<div className="h-10 w-10 bg-green-100 text-green-600 rounded-full flex items-center justify-center shrink-0">
157+
<IconCheck size={24} />
158+
</div>
159+
<div>
160+
<h4 className="font-semibold text-green-900">Challenge Completed!</h4>
161+
<p className="text-sm text-green-700">Excellent work. You've successfully solved this coding challenge.</p>
162+
</div>
130163
</div>
164+
<Button variant="outline" className="text-green-700 border-green-200 hover:bg-green-100 gap-2 shrink-0 ml-4">
165+
<IconRotateClockwise size={18} />
166+
Next Activity
167+
</Button>
131168
</div>
132-
<Button variant="outline" className="text-green-700 border-green-200 hover:bg-green-100 gap-2">
133-
<IconRotateClockwise size={18} />
134-
Next Activity
135-
</Button>
136-
</div>
137-
)}
138-
</div>
169+
)}
170+
</div>
171+
</>
139172
);
140173
}

messages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,9 @@
477477
"status": {
478478
"draft": "Draft",
479479
"published": "Published",
480-
"archived": "Archived"
480+
"archived": "Archived",
481+
"active": "Active",
482+
"disabled": "Disabled"
481483
},
482484
"difficulty": {
483485
"easy": "Easy",

messages/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,9 @@
477477
"status": {
478478
"draft": "Borrador",
479479
"published": "Publicado",
480-
"archived": "Archivado"
480+
"archived": "Archivado",
481+
"active": "Activo",
482+
"disabled": "Deshabilitado"
481483
},
482484
"difficulty": {
483485
"easy": "Fácil",

0 commit comments

Comments
 (0)