diff --git a/package-lock.json b/package-lock.json index 84a4c3a2..f4b95057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "frontend-temp", + "name": "nettdetektivene-frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend-temp", + "name": "nettdetektivene-frontend", "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.2.2", diff --git a/package.json b/package.json index 4debe6d3..a3643436 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "frontend-temp", + "name": "nettdetektivene-frontend", "version": "0.0.0", "private": true, "type": "module", diff --git a/src/app/router/__tests__/auth-routing.spec.ts b/src/app/router/__tests__/auth-routing.spec.ts index 1d1a54d3..18683367 100644 --- a/src/app/router/__tests__/auth-routing.spec.ts +++ b/src/app/router/__tests__/auth-routing.spec.ts @@ -142,7 +142,9 @@ describe('authenticated routing', () => { await router.push('/teacher/classrooms/7/notifications') await router.isReady() - expect(router.currentRoute.value.name).toBe('classroom-notifications') + expect(router.currentRoute.value.name).toBe( + 'teacher-classroom-notifications', + ) expect(router.currentRoute.value.fullPath).toBe( '/teacher/classrooms/7/notifications', ) @@ -159,7 +161,9 @@ describe('authenticated routing', () => { await router.push('/teacher/classrooms/7/pupils/15/notebook') await router.isReady() - expect(router.currentRoute.value.name).toBe('classroom-pupil-notebook') + expect(router.currentRoute.value.name).toBe( + 'teacher-classroom-pupil-notebook', + ) expect(router.currentRoute.value.fullPath).toBe( '/teacher/classrooms/7/pupils/15/notebook', ) @@ -179,6 +183,39 @@ describe('authenticated routing', () => { expect(router.currentRoute.value.fullPath).toBe('/pupil/join-classroom') }) + it('does not register duplicate route names', () => { + const pinia = createPinia() + const router = createAppRouter(createMemoryHistory(), pinia) + const routeNames = router + .getRoutes() + .flatMap((route) => (route.name ? [String(route.name)] : [])) + + expect(new Set(routeNames).size).toBe(routeNames.length) + }) + + it('does not register teacher classroom management routes under the pupil portal', async () => { + const pinia = createPinia() + const authStore = useAuthStore(pinia) + authStore.status = 'authenticated' + authStore.accessToken = 'access-token' + authStore.user = authenticatedUser('PUPIL', { + pupilProfile: { + displayName: 'Ada', + totalXp: 0, + currentLevel: 1, + currentClassroomId: 101, + hasCompletedOnboarding: true, + membershipStatus: 'ACTIVE', + }, + }) + + const router = createAppRouter(createMemoryHistory(), pinia) + await router.push('/pupil/classrooms/7/tasks/nyhetskvartalet') + await router.isReady() + + expect(router.currentRoute.value.name).toBe('not-found') + }) + it('waits for session restore before allowing the first protected navigation', async () => { const pinia = createPinia() const authStore = useAuthStore(pinia) diff --git a/src/classrooms/__tests__/classrooms.api.spec.ts b/src/classrooms/__tests__/classrooms.api.spec.ts new file mode 100644 index 00000000..797ae722 --- /dev/null +++ b/src/classrooms/__tests__/classrooms.api.spec.ts @@ -0,0 +1,94 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + getClassrooms, + getPupilsForClassroom, +} from '@/classrooms/api/classrooms.api' + +describe('classrooms.api', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('normalizes owned classrooms that use id instead of classroomId', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => [ + { + id: 7, + title: 'Detektivgjengen 6B', + description: '', + pupilCount: 24, + pendingPupilCount: 0, + teacherCount: 2, + taskCount: 5, + }, + ], + }) + vi.stubGlobal('fetch', fetchMock) + + await expect(getClassrooms('Bearer token')).resolves.toEqual([ + { + classroomId: 7, + title: 'Detektivgjengen 6B', + description: '', + pupilCount: 24, + pendingPupilCount: 0, + teacherCount: 2, + taskCount: 5, + }, + ]) + }) + + it('returns pupils from the classroom detail endpoint', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + id: 7, + title: 'Detektivgjengen 6B', + description: '', + joinCode: 'ABC123', + status: 'ACTIVE', + ownerTeacherId: 1, + teachers: [], + pupils: [ + { + userId: 15, + displayName: 'Ada', + pupilStatus: 'ACTIVE', + currentLevel: 4, + completedStopsCount: 3, + }, + ], + }), + }) + vi.stubGlobal('fetch', fetchMock) + + await expect(getPupilsForClassroom('7', 'Bearer token')).resolves.toEqual([ + { + userId: 15, + displayName: 'Ada', + pupilStatus: 'ACTIVE', + currentLevel: 4, + completedStopsCount: 3, + }, + ]) + expect(fetchMock).toHaveBeenCalledWith('/api/v1/classrooms/7', { + headers: { + Authorization: 'Bearer token', + }, + credentials: 'include', + }) + }) + + it('rejects pupil lookups without an authorization header', async () => { + await expect(getPupilsForClassroom('7', null)).rejects.toThrow( + 'Not authenticated', + ) + }) + + it('rejects pupil lookups with invalid classroom ids', async () => { + await expect( + getPupilsForClassroom('classroom-1', 'Bearer token'), + ).rejects.toThrow('Invalid classroom id') + }) +}) diff --git a/src/classrooms/_tests_/classrooms.store.spec.ts b/src/classrooms/__tests__/classrooms.store.spec.ts similarity index 94% rename from src/classrooms/_tests_/classrooms.store.spec.ts rename to src/classrooms/__tests__/classrooms.store.spec.ts index c03bb9d5..5f8bb7da 100644 --- a/src/classrooms/_tests_/classrooms.store.spec.ts +++ b/src/classrooms/__tests__/classrooms.store.spec.ts @@ -174,5 +174,14 @@ describe('classrooms store', () => { teacherCount: 1, }, ]) + + const store = useClassroomsStore() + + await store.fetchClassrooms() + await store.fetchClassrooms() + + expect(store.classrooms).toHaveLength(1) + expect(store.classrooms[0]!.classroomId).toBe(2) + expect(store.classrooms[0]!.title).toBe('New') }) }) diff --git a/src/classrooms/_tests_/pupil-routing.spec.ts b/src/classrooms/__tests__/pupil-routing.spec.ts similarity index 84% rename from src/classrooms/_tests_/pupil-routing.spec.ts rename to src/classrooms/__tests__/pupil-routing.spec.ts index f2d17c3e..d1c1bd75 100644 --- a/src/classrooms/_tests_/pupil-routing.spec.ts +++ b/src/classrooms/__tests__/pupil-routing.spec.ts @@ -118,4 +118,18 @@ describe('join classroom routing', () => { expect(router.currentRoute.value.name).toBe('not-found') }) + + it('only registers join and pending classroom routes for pupils', () => { + const pinia = createPinia() + const router = createAppRouter(createMemoryHistory(), pinia) + const routeNames = router + .getRoutes() + .flatMap((route) => (route.name ? [String(route.name)] : [])) + + expect(routeNames).toContain('pupil-join-classroom') + expect(routeNames).toContain('pupil-pending-approval') + expect(routeNames).not.toContain('pupil-classrooms') + expect(routeNames).not.toContain('pupil-classroom-details') + expect(routeNames).not.toContain('classroom-tasks') + }) }) diff --git a/src/classrooms/_tests_/teacher-routing.spec.ts b/src/classrooms/__tests__/teacher-routing.spec.ts similarity index 59% rename from src/classrooms/_tests_/teacher-routing.spec.ts rename to src/classrooms/__tests__/teacher-routing.spec.ts index 76f5ce4f..a49d8f8e 100644 --- a/src/classrooms/_tests_/teacher-routing.spec.ts +++ b/src/classrooms/__tests__/teacher-routing.spec.ts @@ -72,4 +72,46 @@ describe('teacher classroom navigation', () => { '/teacher/classrooms/7/moderation', ) }) + + it('resolves teacher classroom management routes under the teacher portal', () => { + const pinia = createPinia() + const router = createAppRouter(createMemoryHistory(), pinia) + + expect( + router.resolve({ + name: 'teacher-classroom-pupils', + params: { classroomId: 7 }, + }).fullPath, + ).toBe('/teacher/classrooms/7/pupils') + expect( + router.resolve({ + name: 'teacher-classroom-tasks', + params: { classroomId: 7, slug: 'nyhetskvartalet' }, + }).fullPath, + ).toBe('/teacher/classrooms/7/tasks/nyhetskvartalet') + expect( + router.resolve({ + name: 'teacher-classroom-notifications', + params: { classroomId: 7 }, + }).fullPath, + ).toBe('/teacher/classrooms/7/notifications') + expect( + router.resolve({ + name: 'teacher-classroom-mystery', + params: { classroomId: 7 }, + }).fullPath, + ).toBe('/teacher/classrooms/7/mystery') + expect( + router.resolve({ + name: 'teacher-classroom-moderation', + params: { classroomId: 7 }, + }).fullPath, + ).toBe('/teacher/classrooms/7/moderation') + expect( + router.resolve({ + name: 'teacher-classroom-pupil-notebook', + params: { classroomId: 7, pupilId: 15 }, + }).fullPath, + ).toBe('/teacher/classrooms/7/pupils/15/notebook') + }) }) diff --git a/src/classrooms/_tests_/classrooms.api.spec.ts b/src/classrooms/_tests_/classrooms.api.spec.ts deleted file mode 100644 index ae868b8c..00000000 --- a/src/classrooms/_tests_/classrooms.api.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { getClassrooms } from '@/classrooms/api/classrooms.api' - -describe('classrooms.api', () => { - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('normalizes owned classrooms that use id instead of classroomId', async () => { - const fetchMock = vi.fn().mockResolvedValue({ - ok: true, - json: async () => [ - { - id: 7, - title: 'Detektivgjengen 6B', - description: '', - pupilCount: 24, - pendingPupilCount: 0, - teacherCount: 2, - taskCount: 5, - }, - ], - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getClassrooms('Bearer token')).resolves.toEqual([ - { - classroomId: 7, - title: 'Detektivgjengen 6B', - description: '', - pupilCount: 24, - pendingPupilCount: 0, - teacherCount: 2, - taskCount: 5, - }, - ]) - }) -}) diff --git a/src/classrooms/api/classrooms.api.ts b/src/classrooms/api/classrooms.api.ts index 13e087e5..90ee1c87 100644 --- a/src/classrooms/api/classrooms.api.ts +++ b/src/classrooms/api/classrooms.api.ts @@ -23,19 +23,6 @@ export interface OwnedClassroomResponse { taskCount: number } -export type ClassroomPupil = { - userId: string - name: string - score: number - xp: number - level: number - avatar?: { - bodyType: 'default' - skinTone: 'light' | 'dark' - outfitStyle: 'standard' | 'standard2' - } -} - export type ClassroomResponse = { classroomId: number title: string @@ -86,119 +73,6 @@ function normalizeClassroomDetail( } } -const mockPupilsByClassroom: Record = { - 'classroom-1': [ - { - userId: 'pupil-1', - name: 'Ola Nordmann', - score: 1240, - xp: 720, - level: 4, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - { - userId: 'pupil-2', - name: 'Kari Nordmann', - score: 980, - xp: 450, - level: 3, - avatar: { - bodyType: 'default', - skinTone: 'dark', - outfitStyle: 'standard2', - }, - }, - { - userId: 'pupil-3', - name: 'Mina Hansen', - score: 1510, - xp: 880, - level: 5, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard2', - }, - }, - { - userId: 'pupil-4', - name: 'Emil Nordmann', - score: 670, - xp: 310, - level: 2, - avatar: { - bodyType: 'default', - skinTone: 'dark', - outfitStyle: 'standard', - }, - }, - { - userId: 'pupil-5', - name: 'Sara Lars', - score: 1320, - xp: 760, - level: 4, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - { - userId: 'pupil-6', - name: 'Sara Lars', - score: 1320, - xp: 760, - level: 4, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - { - userId: 'pupil-7', - name: 'Hanne Lars', - score: 1920, - xp: 970, - level: 8, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - { - userId: '8', - name: 'Lars', - score: 1420, - xp: 790, - level: 5, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - { - userId: '8', - name: 'MinaLars', - score: 420, - xp: 790, - level: 5, - avatar: { - bodyType: 'default', - skinTone: 'light', - outfitStyle: 'standard', - }, - }, - ], -} - export type JoinClassroomRequest = { classroomCode: string } @@ -327,25 +201,24 @@ async function fetchClassroomJson( export async function getPupilsForClassroom( classroomId: string, - authorizationHeader?: string | null, -): Promise { + authorizationHeader: string | null, +): Promise { const numericClassroomId = Number(classroomId) - if (authorizationHeader && Number.isFinite(numericClassroomId)) { - const classroom = await getClassroomDetail( - numericClassroomId, - authorizationHeader, - ) - - return classroom.pupils.map((pupil) => ({ - userId: String(pupil.userId), - name: pupil.displayName, - score: 0, - xp: 0, - level: 1, - })) + + if (!authorizationHeader) { + throw new Error('Not authenticated') } - return [...(mockPupilsByClassroom[classroomId] ?? [])] + if (!Number.isFinite(numericClassroomId)) { + throw new Error('Invalid classroom id') + } + + const classroom = await getClassroomDetail( + numericClassroomId, + authorizationHeader, + ) + + return classroom.pupils } export function requestRemovePupil( diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue index 93a2d96b..4c73027e 100644 --- a/src/classrooms/components/PupilTable.vue +++ b/src/classrooms/components/PupilTable.vue @@ -65,7 +65,7 @@ async function approvePupil(pupilId: number) { class="mx-auto w-full max-h-170 max-w-7xl overflow-y-auto bg-white drop-shadow-lg scrollbar-thin" >
- Loading pupils... + {{ t('pupilTable.loading') }}
@@ -77,14 +77,16 @@ async function approvePupil(pupilId: number) {

- Pending approval + {{ t('pupilTable.pending') }}

- - + + @@ -99,7 +101,7 @@ async function approvePupil(pupilId: number) { - Pending + {{ t('pupilTable.pendingBadge') }} @@ -156,7 +158,7 @@ async function approvePupil(pupilId: number) { @@ -177,7 +179,7 @@ async function approvePupil(pupilId: number) { diff --git a/src/classrooms/components/__tests__/PupilTable.spec.ts b/src/classrooms/components/__tests__/PupilTable.spec.ts index 2d078ead..b3560dfa 100644 --- a/src/classrooms/components/__tests__/PupilTable.spec.ts +++ b/src/classrooms/components/__tests__/PupilTable.spec.ts @@ -51,12 +51,12 @@ describe('PupilTable', () => { }, { path: '/teacher/classrooms/:classroomId/pupils/:pupilId/notebook', - name: 'classroom-pupil-notebook', + name: 'teacher-classroom-pupil-notebook', component: { template: '
' }, }, { path: '/teacher/classrooms/:classroomId/pupils/:pupilId', - name: 'classroom-pupil-progress', + name: 'teacher-classroom-pupil-progress', component: { template: '
' }, }, ], @@ -76,11 +76,11 @@ describe('PupilTable', () => { const links = wrapper.findAllComponents({ name: 'RouterLink' }) const notebookLink = links.find( - (link) => link.props('to')?.name === 'classroom-pupil-notebook', + (link) => link.props('to')?.name === 'teacher-classroom-pupil-notebook', ) expect(notebookLink?.props('to')).toEqual({ - name: 'classroom-pupil-notebook', + name: 'teacher-classroom-pupil-notebook', params: { classroomId: 7, pupilId: 15 }, }) }) @@ -111,12 +111,12 @@ describe('PupilTable', () => { }, { path: '/teacher/classrooms/:classroomId/pupils/:pupilId/notebook', - name: 'classroom-pupil-notebook', + name: 'teacher-classroom-pupil-notebook', component: { template: '
' }, }, { path: '/teacher/classrooms/:classroomId/pupils/:pupilId', - name: 'classroom-pupil-progress', + name: 'teacher-classroom-pupil-progress', component: { template: '
' }, }, ], diff --git a/src/classrooms/pages/ClassroomDetails.vue b/src/classrooms/pages/ClassroomDetails.vue index 4ee74008..ad2a19a9 100644 --- a/src/classrooms/pages/ClassroomDetails.vue +++ b/src/classrooms/pages/ClassroomDetails.vue @@ -165,8 +165,6 @@ onMounted(() => { } }) -const newTeacherInput = ref('') - const isEditing = ref(false) const editForm = ref({ @@ -180,8 +178,6 @@ watch(classroom, (c) => { title: c.title, description: c.description ?? '', } - - newTeacherInput.value = '' } }) @@ -197,8 +193,6 @@ function cancelEdit() { title: classroom.value.title, description: classroom.value.description ?? '', } - - newTeacherInput.value = '' } } @@ -208,14 +202,14 @@ async function saveEdit() { } if (!editForm.value.title.trim()) { - alert(t('classroomDetails.titlealert')) + alert(t('classroomDetails.titleAlert')) return } try { await classroomsStore.updateClassroom(classroom.value.classroomId, { - title: classroom.value.title, - description: classroom.value.description ?? '', + title: editForm.value.title.trim(), + description: editForm.value.description.trim(), teacherEmails: teacherEmails.value, status: classroom.value.status, }) @@ -225,7 +219,7 @@ async function saveEdit() { isEditing.value = false } catch (e) { console.error(e) - alert(t('classroomDetails.failalert')) + alert(t('classroomDetails.failAlert')) } } @@ -271,13 +265,6 @@ async function saveEdit() {
-
- {{ t('classroomTable.teachers') }}: -

- {{ classroom.teachers?.map((u) => u.email).join(', ') || '-' }} -

-
-
@@ -328,10 +315,6 @@ async function saveEdit() { }}

- -

- {{ classroom.description || '-' }} -

@@ -429,7 +412,7 @@ async function saveEdit() { @@ -456,7 +442,7 @@ async function saveEdit() { Mystery diff --git a/src/classrooms/pages/ClassroomNotifications.vue b/src/classrooms/pages/ClassroomNotifications.vue deleted file mode 100644 index aa4fd326..00000000 --- a/src/classrooms/pages/ClassroomNotifications.vue +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue index a455ea65..97048388 100644 --- a/src/classrooms/pages/ClassroomTasks.vue +++ b/src/classrooms/pages/ClassroomTasks.vue @@ -77,7 +77,7 @@ const filteredTasks = computed(() => { function selectStop(newSlug: string) { router.push({ - name: 'classroom-tasks', + name: 'teacher-classroom-tasks', params: { classroomId: classroomId.value, slug: newSlug, @@ -87,14 +87,14 @@ function selectStop(newSlug: string) { function goToCreate() { router.push({ - name: 'new-classroom-task', + name: 'teacher-new-classroom-task', params: { classroomId: classroomId.value }, }) } function goToEdit(taskId: number) { router.push({ - name: 'edit-classroom-task', + name: 'teacher-edit-classroom-task', params: { classroomId: classroomId.value, taskId }, }) } diff --git a/src/classrooms/pages/EditClassroomTask.vue b/src/classrooms/pages/EditClassroomTask.vue deleted file mode 100644 index 3e188a3f..00000000 --- a/src/classrooms/pages/EditClassroomTask.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/classrooms/pages/JoinClassroom.vue b/src/classrooms/pages/JoinClassroom.vue index ca50d4f9..66f19a08 100644 --- a/src/classrooms/pages/JoinClassroom.vue +++ b/src/classrooms/pages/JoinClassroom.vue @@ -23,8 +23,6 @@ const classroomId = computed( watch( classroomId, async (newId) => { - console.log('WATCH TRIGGERED:', newId) - if (newId !== null && newId !== undefined) { await router.push({ name: 'pupil-home' }) } @@ -50,8 +48,7 @@ async function join() { if (authStore.user?.pupilProfile?.membershipStatus === 'PENDING') { await router.push({ name: 'pupil-pending-approval' }) } - } catch (error) { - console.error('JOIN CLASSROOM ERROR:', error) + } catch { errorMessage.value = t('joinClassroom.invalid') } finally { isJoining.value = false diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue deleted file mode 100644 index 3e188a3f..00000000 --- a/src/classrooms/pages/NewClassroomTask.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/classrooms/pages/TaskEditorPage.vue b/src/classrooms/pages/TaskEditorPage.vue index cf0818c7..67da2fc0 100644 --- a/src/classrooms/pages/TaskEditorPage.vue +++ b/src/classrooms/pages/TaskEditorPage.vue @@ -109,7 +109,7 @@ async function redirectFromImageSelectTask(stopId: number) { if (stop) { await router.replace({ - name: 'classroom-tasks', + name: 'teacher-classroom-tasks', params: { classroomId: classroomId.value, slug: stop.slug, @@ -131,7 +131,7 @@ async function navigateAfterSave() { if (slug) { await router.push({ - name: 'classroom-tasks', + name: 'teacher-classroom-tasks', params: { classroomId: classroomId.value, slug, diff --git a/src/classrooms/pages/__tests__/ClassroomDetails.spec.ts b/src/classrooms/pages/__tests__/ClassroomDetails.spec.ts index 319f971c..ad5f3025 100644 --- a/src/classrooms/pages/__tests__/ClassroomDetails.spec.ts +++ b/src/classrooms/pages/__tests__/ClassroomDetails.spec.ts @@ -1,7 +1,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { reactive } from 'vue' import { createMemoryHistory, createRouter } from 'vue-router' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { createAppI18n } from '@/app/i18n' import { createPinia, setActivePinia } from 'pinia' import ClassroomDetails from '@/classrooms/pages/ClassroomDetails.vue' @@ -36,6 +36,10 @@ vi.mock('@/classrooms/model/classrooms.store', () => ({ })) describe('ClassroomDetails', () => { + beforeEach(() => { + store.updateClassroom.mockClear() + }) + async function mountPage() { window.localStorage.setItem('locale', 'en') @@ -91,4 +95,27 @@ describe('ClassroomDetails', () => { status: 'ACTIVE', }) }) + + it('shows classroom description and teacher emails once', async () => { + const wrapper = await mountPage() + + expect(wrapper.text().match(/Digital safety/g)).toHaveLength(1) + expect(wrapper.text().match(/owner@example\.com/g)).toHaveLength(1) + }) + + it('shows a cancel action while editing classroom details', async () => { + const wrapper = await mountPage() + + await wrapper + .findAll('button') + .find((button) => button.text() === 'Edit')! + .trigger('click') + + expect( + wrapper.findAll('button').some((button) => button.text() === 'Cancel'), + ).toBe(true) + expect( + wrapper.findAll('button').some((button) => button.text() === 'Save'), + ).toBe(true) + }) }) diff --git a/src/classrooms/routes.ts b/src/classrooms/routes.ts index 181e137f..bcd2cc3b 100644 --- a/src/classrooms/routes.ts +++ b/src/classrooms/routes.ts @@ -15,65 +15,65 @@ import TeacherMystery from '@/teacher-mystery/pages/TeacherMystery.vue' import TeacherPupilNotebookPage from '@/notebook/pages/TeacherPupilNotebookPage.vue' import ClassroomModerationPage from '@/classrooms/pages/ClassroomModerationPage.vue' -export function createClassroomRoutes( - prefix: string, - options: { - includeModerationRoute?: boolean - } = {}, -): RouteRecordRaw[] { +export function createPupilClassroomRoutes(): RouteRecordRaw[] { + return [ + { + path: 'join-classroom', + name: 'pupil-join-classroom', + component: JoinClassroom, + meta: { + hideBackButton: true, + hideHomeButton: true, + }, + }, + { + path: 'pending-approval', + name: 'pupil-pending-approval', + component: PendingApproval, + meta: { + hideBackButton: true, + hideHomeButton: true, + }, + }, + ] +} + +export function createTeacherClassroomRoutes(): RouteRecordRaw[] { const classroomDetailsBackTarget = ( route: RouteLocationNormalizedLoaded, ) => ({ - name: `${prefix}-classroom-details`, + name: 'teacher-classroom-details', params: { classroomId: route.params.classroomId }, }) - const routes: RouteRecordRaw[] = [ + return [ { path: 'classrooms', - name: `${prefix}-classrooms`, + name: 'teacher-classrooms', component: ClassroomOverview, meta: { - backTarget: { name: `${prefix}-home` }, + backTarget: { name: 'teacher-home' }, }, }, { path: 'new-classroom', - name: `${prefix}-new-classroom`, + name: 'teacher-new-classroom', component: NewClassroom, meta: { - backTarget: { name: `${prefix}-classrooms` }, - }, - }, - { - path: 'join-classroom', - name: `${prefix}-join-classroom`, - component: JoinClassroom, - meta: { - hideBackButton: true, - hideHomeButton: true, - }, - }, - { - path: 'pending-approval', - name: `${prefix}-pending-approval`, - component: PendingApproval, - meta: { - hideBackButton: true, - hideHomeButton: true, + backTarget: { name: 'teacher-classrooms' }, }, }, { path: 'classroom-details/:classroomId', - name: `${prefix}-classroom-details`, + name: 'teacher-classroom-details', component: ClassroomDetails, meta: { - backTarget: { name: `${prefix}-classrooms` }, + backTarget: { name: 'teacher-classrooms' }, }, }, { path: 'classrooms/:classroomId/pupils', - name: 'classroom-pupils', + name: 'teacher-classroom-pupils', component: PupilOverviewPage, meta: { backTarget: classroomDetailsBackTarget, @@ -81,22 +81,22 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/pupils/:pupilId', - name: 'classroom-pupil-progress', + name: 'teacher-classroom-pupil-progress', component: ClassroomDetails, meta: { backTarget: (route: RouteLocationNormalizedLoaded) => ({ - name: 'classroom-pupils', + name: 'teacher-classroom-pupils', params: { classroomId: route.params.classroomId }, }), }, }, { path: 'classrooms/:classroomId/pupils/:pupilId/notebook', - name: 'classroom-pupil-notebook', + name: 'teacher-classroom-pupil-notebook', component: TeacherPupilNotebookPage, meta: { backTarget: (route: RouteLocationNormalizedLoaded) => ({ - name: 'classroom-pupil-progress', + name: 'teacher-classroom-pupil-progress', params: { classroomId: route.params.classroomId, pupilId: route.params.pupilId, @@ -106,7 +106,7 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/notifications', - name: 'classroom-notifications', + name: 'teacher-classroom-notifications', component: NotificationsPage, meta: { backTarget: classroomDetailsBackTarget, @@ -114,7 +114,7 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/tasks/new', - name: 'new-classroom-task', + name: 'teacher-new-classroom-task', component: TaskEditorPage, meta: { backTarget: classroomDetailsBackTarget, @@ -122,7 +122,7 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/tasks/:taskId/edit', - name: 'edit-classroom-task', + name: 'teacher-edit-classroom-task', component: TaskEditorPage, meta: { backTarget: classroomDetailsBackTarget, @@ -130,7 +130,7 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/tasks/:slug', - name: 'classroom-tasks', + name: 'teacher-classroom-tasks', component: ClassroomTasks, meta: { backTarget: classroomDetailsBackTarget, @@ -157,23 +157,19 @@ export function createClassroomRoutes( }, { path: 'classrooms/:classroomId/mystery', - name: 'classroom-mystery', + name: 'teacher-classroom-mystery', component: TeacherMystery, meta: { backTarget: classroomDetailsBackTarget, }, }, - ] - if (options.includeModerationRoute) { - routes.push({ + { path: 'classrooms/:classroomId/moderation', - name: 'classroom-moderation', + name: 'teacher-classroom-moderation', component: ClassroomModerationPage, meta: { backTarget: classroomDetailsBackTarget, }, - }) - } - - return routes + }, + ] } diff --git a/src/locales/en.ts b/src/locales/en.ts index 22472043..14aba454 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -29,6 +29,8 @@ export const en = { createClassroom: 'Create classroom', save: 'Save', back: 'Back', + close: 'Close', + saving: 'Saving...', or: 'Or', }, app: { @@ -41,6 +43,12 @@ export const en = { nb: 'Norwegian', }, }, + confirmModal: { + title: 'Confirm action', + message: 'Are you sure you want to continue?', + cancel: 'Cancel', + confirm: 'Confirm', + }, auth: { login: { title: 'Login', @@ -141,6 +149,7 @@ export const en = { answerAllFirst: 'Answer all tasks first', selected: 'Selected', selectThis: 'Select this', + yourAnswer: 'Your answer', correct: 'Correct', incorrect: 'Not quite right', backToMap: 'Back to map', @@ -592,6 +601,8 @@ export const en = { leaderboardDescription: "View your students' progress", settingsTitle: 'Settings', settingsDescription: 'Adjust audio and manage your account', + classroomTitle: 'Classrooms', + classroomDescription: 'View and manage your classrooms', }, }, teacherSettings: { @@ -901,6 +912,7 @@ export const en = { description: 'Here you can view classroom rankings by average score', }, pupilTable: { + loading: 'Loading pupils...', name: 'Name', score: 'Score', xp: 'XP', @@ -909,6 +921,14 @@ export const en = { actions: 'Actions', remove: 'Remove', seeNotebook: 'See notebook', + pending: 'Pupils pending approval', + pendingBadge: 'Pending', + approved: 'Approved pupils', + approve: 'Approve', + confirm: 'Remove pupil', + message: 'Are you sure you want to remove this pupil from the classroom?', + cancel: 'Cancel', + noPupils: 'No approved pupils yet.', }, pupilOverviewPage: { title: 'Pupils', @@ -923,6 +943,10 @@ export const en = { example: "teacher{'@'}epost.no", add: 'Add', create: 'Create classroom', + noname: 'The classroom must have a name', + wrongemail: 'The email address must have a valid format', + atleast: 'The classroom must contain at least one teacher', + nosave: 'Could not create classroom', }, joinClassroom: { title: 'Join a classroom', @@ -955,9 +979,19 @@ export const en = { tasks: 'Tasks', action: 'Action', open: 'Open', + joinCode: 'Classroom code', }, classroomDetails: { label: 'Classroom', + titleAlert: 'The classroom must have a title.', + failAlert: 'Could not update the classroom.', + confirmMessage: + 'Are you sure you want to delete this classroom? All pupils and data will be removed.', + save: 'Save', + cancel: 'Cancel', + deleteClass: 'Delete classroom', + edit: 'Edit', + confirm: 'Delete', teacherAccess: { title: 'Teacher access', description: 'See and manage which teachers can access this classroom.', @@ -1013,7 +1047,23 @@ export const en = { joinCode: 'Classroom code', }, taskOverview: { + title: 'Tasks', + fail: 'Could not fetch tasks', + unpublish: 'Unpublish', + publish: 'Publish', + delete: 'Delete task', + message: + 'Are you sure you want to delete this task? This cannot be undone.', + ptitle: 'Change publication status', + pmessage: 'Are you sure you want to change this task publication status?', + confirm: 'Continue', + create: 'Create task', + notask: 'No tasks found', + unpublished: 'Unpublished', + published: 'Published', + edit: 'Edit', imageSelectLocked: 'Cannot be edited', + cancel: 'Cancel', }, newTask: { savefail: 'Saving failed', @@ -1077,6 +1127,12 @@ export const en = { missingWord: 'Enter a word before saving.', }, }, + pendingApproval: { + refresh: 'Refresh status', + waiting: 'Waiting for approval.', + request: + 'Your request to join the classroom is waiting for teacher approval.', + }, mystery: { title: 'Weekly mystery', subtitle: 'See what your teacher has picked, and submit your own find!', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 822644f3..da5c1414 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -29,6 +29,7 @@ export const nb = { createClassroom: 'Opprett klasserom', save: 'Lagre', back: 'Tilbake', + close: 'Lukk', or: 'Eller', saving: 'Lagrer...', }, @@ -42,6 +43,12 @@ export const nb = { nb: 'Norsk', }, }, + confirmModal: { + title: 'Bekreft handling', + message: 'Er du sikker på at du vil fortsette?', + cancel: 'Avbryt', + confirm: 'Bekreft', + }, auth: { login: { title: 'Logg inn', @@ -908,6 +915,7 @@ export const nb = { 'Her kan du se rangering av klasserom etter gjennomsnittspoeng', }, pupilTable: { + loading: 'Laster elever...', name: 'Navn', score: 'Poeng', xp: 'XP', @@ -917,11 +925,13 @@ export const nb = { remove: 'Fjern', seeNotebook: 'Se notatbok', pending: 'Elever som avventer godkjenning', + pendingBadge: 'Venter', approved: 'Godkjente elever', approve: 'Godkjenn', confirm: 'Fjern elev', message: 'Er du sikker på at du vil fjerne eleven fra klasseromet?', cancel: 'Avbryt', + noPupils: 'Ingen godkjente elever ennå.', }, pupilOverviewPage: { title: 'Elever', @@ -937,6 +947,10 @@ export const nb = { example: "lærer{'@'}epost.no", add: 'Legg til', create: 'Opprett klasserom', + noname: 'Klasserommet må ha navn', + wrongemail: 'E-postadressen må ha gyldig format.', + atleast: 'Klasserommet må inneholde minst en lærer.', + nosave: 'Kunne ikke opprette klasserom', }, joinClassroom: { title: 'Bli med i et klasserom', @@ -1033,13 +1047,13 @@ export const nb = { }, classroomDetails: { label: 'Klasserom', - titlealert: 'Klasserommet må ha tittel.', - failalert: 'Kunne ikke oppdatere klasserommet.', - confirmmessage: + titleAlert: 'Klasserommet må ha tittel.', + failAlert: 'Kunne ikke oppdatere klasserommet.', + confirmMessage: 'Er du sikker på at du vil slette kalsseromet? Alle elever og data vil bli fjernet.', save: 'Lagre', cancel: 'Avbryt', - deleteclass: 'Slett klasserom', + deleteClass: 'Slett klasserom', edit: 'Endre', addteachers: 'Legg til lærere', existingteachers: 'Eksisterende lærere', diff --git a/src/pupil-portal/routes.ts b/src/pupil-portal/routes.ts index d1d84e96..bde7386a 100644 --- a/src/pupil-portal/routes.ts +++ b/src/pupil-portal/routes.ts @@ -1,5 +1,5 @@ import type { RouteRecordRaw } from 'vue-router' -import { createClassroomRoutes } from '@/classrooms/routes.ts' +import { createPupilClassroomRoutes } from '@/classrooms/routes.ts' import { medalsRoutes } from '@/medals/routes' import { leaderboardRoutes } from '@/leaderboard/routes' import { createMapRoutes } from '@/map/routes' @@ -40,6 +40,6 @@ export function createPupilPortalChildRoutes(): RouteRecordRaw[] { ...notebookRoutes, ...mysteryRoutes, ...medalsRoutes, - ...createClassroomRoutes('pupil'), + ...createPupilClassroomRoutes(), ] } diff --git a/src/stops/components/intro/NyhetskvartaletIntroCard.vue b/src/stops/components/intro/NyhetskvartaletIntroCard.vue index 636cd969..5727c791 100644 --- a/src/stops/components/intro/NyhetskvartaletIntroCard.vue +++ b/src/stops/components/intro/NyhetskvartaletIntroCard.vue @@ -115,13 +115,13 @@ const { t } = useI18n() diff --git a/src/teacher-portal/routes.ts b/src/teacher-portal/routes.ts index 57815cd0..72501129 100644 --- a/src/teacher-portal/routes.ts +++ b/src/teacher-portal/routes.ts @@ -1,7 +1,7 @@ import type { RouteRecordRaw } from 'vue-router' import TeacherHomePage from '@/teacher-portal/pages/TeacherHomePage.vue' -import { createClassroomRoutes } from '@/classrooms/routes.ts' +import { createTeacherClassroomRoutes } from '@/classrooms/routes.ts' import { notificationsRoutes } from '@/notifications/routes.ts' import { teacherMysteryRoutes } from '@/teacher-mystery/routes.ts' import TeacherSettingsPage from '@/teacher-portal/pages/TeacherSettingsPage.vue' @@ -32,7 +32,7 @@ export function createTeacherPortalChildRoutes(): RouteRecordRaw[] { showScore: true, }, }, - ...createClassroomRoutes('teacher', { includeModerationRoute: true }), + ...createTeacherClassroomRoutes(), ...teacherMysteryRoutes, ...notificationsRoutes, {
NameActions{{ t('pupilTable.name') }} + {{ t('pupilTable.actions') }} +