Skip to content

Commit 4cf794d

Browse files
committed
Exercise selection per workshop
Solves #36
1 parent 943ef27 commit 4cf794d

File tree

8 files changed

+470
-136
lines changed

8 files changed

+470
-136
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getExercisesWithResponse } from '@/lib/exercise-file-manager';
3+
import { verifyAdmin } from '@/lib/session';
4+
5+
/**
6+
* @route GET /api/exercises
7+
* @desc Get all exercises (for admin use in workshop management)
8+
* @response 200 { exercises: { [code: string]: { title: string } } } or 500 { error: string }
9+
* @access Admin only
10+
*/
11+
export async function GET(request: NextRequest) {
12+
if (!(await verifyAdmin())) {
13+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
14+
}
15+
16+
const exerciseResult = await getExercisesWithResponse();
17+
if (!exerciseResult.success) {
18+
return exerciseResult.error;
19+
}
20+
21+
// Only return the code and title for each exercise (what the UI needs)
22+
const exercises = Object.fromEntries(
23+
Object.entries(exerciseResult.value.exercises).map(([code, exercise]) => [
24+
code,
25+
{ title: exercise.title }
26+
])
27+
);
28+
29+
return NextResponse.json({ exercises });
30+
}
31+

workshop-ui/src/app/api/workshops/[id]/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,19 @@ export async function PUT(req: NextRequest, context: { params: Promise<{ id: str
5050
// Code sollte nicht überschrieben werden - entferne es aus den Update-Feldern
5151
const { code, ...fieldsToUpdate } = updateFields;
5252

53+
// Handle exerciseCodes explicitly: convert empty arrays to undefined (meaning "use defaults")
54+
// This ensures that removing all exercises works correctly
55+
if ('exerciseCodes' in fieldsToUpdate) {
56+
if (Array.isArray(fieldsToUpdate.exerciseCodes) && fieldsToUpdate.exerciseCodes.length === 0) {
57+
// Convert empty array to undefined and explicitly delete the property to ensure it's cleared
58+
delete fieldsToUpdate.exerciseCodes;
59+
delete workshops[index].exerciseCodes;
60+
}
61+
}
62+
63+
// Apply updates
5364
workshops[index] = { ...workshops[index], ...fieldsToUpdate };
65+
5466
if (await writeWorkshops(workshops)) {
5567
return NextResponse.json(workshops[index]);
5668
} else {

workshop-ui/src/app/page.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { trace } from '@opentelemetry/api';
55

66
import { getExercises } from '@/lib/exercise-file-manager';
77
import { getAppSession, validateAppSession } from '@/lib/session';
8+
import { readWorkshops } from '@/lib/workshopService';
89
import LogoutButton from '@/components/LogoutButton';
910

1011
import styles from './page.module.css';
@@ -16,7 +17,8 @@ type Difficulty = {
1617

1718
export default async function Home() {
1819
// redirect to login if not authenticated
19-
const isAuthenticated = await validateAppSession(await getAppSession());
20+
const session = await getAppSession();
21+
const isAuthenticated = await validateAppSession(session);
2022
if (!isAuthenticated) {
2123
const headersList = await headers();
2224
const pathname = headersList.get('x-pathname') || '/';
@@ -29,7 +31,26 @@ export default async function Home() {
2931
span?.addEvent('exercises_file_validation_error', { error: exercisesResult.error.error });
3032
throw new Error('Failed to load exercises');
3133
}
32-
const exercisesData = exercisesResult.value.exercises;
34+
const allExercises = exercisesResult.value.exercises;
35+
36+
// Get the workshop for this user
37+
const workshops = await readWorkshops();
38+
const workshop = workshops.find(w => w.code === session.accessCode);
39+
40+
// Filter exercises based on workshop configuration
41+
let filteredExercises: typeof allExercises;
42+
if (workshop?.exerciseCodes && workshop.exerciseCodes.length > 0) {
43+
// If workshop has specific exercise codes, only show those
44+
filteredExercises = Object.fromEntries(
45+
Object.entries(allExercises).filter(([code]) => workshop.exerciseCodes!.includes(code))
46+
);
47+
} else {
48+
// If no exercise codes specified, show exercises with visibleByDefault=true (default)
49+
// visibleByDefault defaults to true if not specified, so we show exercises where it's not explicitly false
50+
filteredExercises = Object.fromEntries(
51+
Object.entries(allExercises).filter(([_, exercise]) => exercise.visibleByDefault !== false)
52+
);
53+
}
3354

3455
function parseDifficulty(difficulty: string): Difficulty {
3556
switch (difficulty) {
@@ -52,7 +73,7 @@ export default async function Home() {
5273
<div className={styles.container}>
5374
<h1 className={styles.title}>AI Workshopübungen</h1>
5475
<div className={styles.exerciseGrid}>
55-
{Object.entries(exercisesData).map(([key, exercise]) => (
76+
{Object.entries(filteredExercises).map(([key, exercise]) => (
5677
<div key={key} className={styles.exerciseCard}>
5778
<Link href={exercise.url ?? `/chat/${key}`} className={styles.exerciseLink}>
5879
<span className={`${styles.exerciseDifficulty} ${parseDifficulty(exercise.difficulty).class}`}>{parseDifficulty(exercise.difficulty).label}</span>

workshop-ui/src/app/workshops/page.module.css

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -102,112 +102,17 @@
102102
}
103103
}
104104

105-
.newWorkshopButton,
106-
.submitButton,
107-
.saveButton,
108-
.cancelButton,
109-
.deleteButton {
105+
.newWorkshopButton {
110106
font-family: 'Poppins', sans-serif;
111107
padding: 0.5rem 1rem;
112108
border-radius: 6px;
113109
border: none;
114110
cursor: pointer;
115-
}
116-
117-
.newWorkshopButton {
118111
background-color: #2563eb;
119112
color: white;
120113
white-space: nowrap;
121114
}
122115

123-
.submitButton {
124-
background-color: #2563eb;
125-
color: white;
126-
}
127-
128-
.saveButton {
129-
background-color: #16a34a;
130-
color: white;
131-
}
132-
133-
.cancelButton {
134-
background-color: #8f98a1;
135-
color: white;
136-
}
137-
138-
.deleteButton {
139-
background-color: #dc2626; /* red for delete */
140-
color: white;
141-
transition: background-color 0.2s ease;
142-
}
143-
144-
.deleteButton:hover {
145-
background-color: #b91c1c; /* darker red on hover */
146-
}
147-
148-
.formWrapper {
149-
background-color: white;
150-
padding: 1rem;
151-
border-radius: 8px;
152-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
153-
margin-bottom: 1rem;
154-
}
155-
156-
.input,
157-
.textarea {
158-
border: 1px solid #d1d5db;
159-
padding: 0.5rem;
160-
width: 100%;
161-
margin-bottom: 0.5rem;
162-
border-radius: 4px;
163-
font-size: 1rem;
164-
font-family: 'Poppins', sans-serif;
165-
}
166-
167-
.textarea {
168-
height: 4rem;
169-
resize: vertical;
170-
}
171-
172-
.timeRow {
173-
display: flex;
174-
gap: 0.5rem;
175-
margin-bottom: 0.5rem;
176-
}
177-
178-
.dateTimeRow {
179-
display: flex;
180-
gap: 0.5rem;
181-
margin-bottom: 0.5rem;
182-
align-items: center;
183-
}
184-
185-
.timeInputGroup {
186-
display: flex;
187-
align-items: center;
188-
gap: 0.5rem;
189-
flex: 1;
190-
}
191-
192-
.timeLabel {
193-
font-size: 0.875rem;
194-
color: #374151;
195-
font-family: 'Poppins', sans-serif;
196-
font-weight: 500;
197-
white-space: nowrap;
198-
}
199-
200-
.editButtonsRow {
201-
display: flex;
202-
gap: 0.5rem;
203-
margin-top: 0.5rem;
204-
}
205-
206-
.submitButton:disabled {
207-
opacity: 0.5;
208-
cursor: not-allowed;
209-
}
210-
211116
/* Workshop table styles */
212117
.workshopTableWrapper {
213118
overflow-x: auto;

0 commit comments

Comments
 (0)