- {{ t("classroomTable.classroom") }}
+ {{ t("classroomTable.classroom") }}
-
+
+
{{ t("classroomTable.description") }}
+
{{ t("classroomTable.joinCode") }}
-
+
+
+ {{ t("classroomTable.pupils") }}
+
+
+
+ {{ t("classroomTable.pending") }}
+
{{ t("classroomTable.teachers") }}
@@ -59,15 +61,15 @@ onMounted(() => {
v-for="classroom in store.classrooms"
:key="classroom.classroomId"
class="border-b hover:bg-gray-100"
- >
+ >
+ class="font-medium hover:underline"
+ :to="{
+ name: 'teacher-classroom-details',
+ params: { classroomId: classroom.classroomId }
+ }"
+ >
{{ classroom.title }}
@@ -81,7 +83,15 @@ onMounted(() => {
- {{ classroom.teacherEmails?.join(", ") || "-" }}
+ {{ classroom.pupilCount }}
+
+
+
+ {{ classroom.pendingPupilCount }}
+
+
+
+ {{ classroom.teacherCount }}
@@ -89,7 +99,7 @@ onMounted(() => {
class="rounded bg-gray-800 px-3 py-1 text-white hover:bg-gray-700"
:to="{
name: 'teacher-classroom-details',
- params: { classroomId: classroom.classroomId },
+ params: { classroomId: classroom.classroomId }
}"
>
{{ t("classroomTable.open") }}
diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue
index 84f55b54..0e78c7af 100644
--- a/src/classrooms/components/PupilTable.vue
+++ b/src/classrooms/components/PupilTable.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+ {{ title || "Are you sure?" }}
+
+
+
+ {{ message || "This action cannot be undone." }}
+
+
+
+
+ {{ cancelText || "Cancel" }}
+
+
+
+ {{ confirmText || "Delete" }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue
index 0e78c7af..143b9bfc 100644
--- a/src/classrooms/components/PupilTable.vue
+++ b/src/classrooms/components/PupilTable.vue
@@ -1,14 +1,32 @@
@@ -261,10 +261,9 @@ async function saveEdit() {
diff --git a/src/classrooms/pages/PendingApproval.vue b/src/classrooms/pages/PendingApproval.vue
index a5e94e5e..671dceeb 100644
--- a/src/classrooms/pages/PendingApproval.vue
+++ b/src/classrooms/pages/PendingApproval.vue
@@ -3,7 +3,9 @@ import { computed, onMounted, onUnmounted, watch } from "vue";
import { useAuthStore } from "@/auth/model/auth.store";
import { useRouter } from "vue-router";
import LogoutButton from "@/shared/components/LogoutButton.vue";
+import { useI18n } from "vue-i18n";
+const { t } = useI18n();
let interval: number;
const authStore = useAuthStore();
@@ -35,11 +37,11 @@ watch(status, (newStatus) => {
- Waiting for approval
+ {{ t("pendingApproval.waiting") }}
- Your request to join the classroom is pending teacher approval.
+ {{ t("pendingApproval.request") }}
@@ -55,7 +57,7 @@ watch(status, (newStatus) => {
class="inline-flex w-full items-center justify-center rounded-full border-2 border-[#7f6647] bg-white px-4 py-3 font-(--font-display-2) text-[0.95rem] leading-none text-[#5e4428] shadow-[0_4px_10px_rgba(94,68,40,0.12),inset_0_-2px_0_rgba(179,143,94,0.35)] transition duration-150 hover:-translate-y-px hover:border-[#6b5270] hover:shadow-[0_8px_16px_rgba(94,68,40,0.16),inset_0_-2px_0_rgba(179,143,94,0.35)] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#1a4a7a]"
@click="authStore.fetchCurrentUserProfile()"
>
- Refresh status
+ {{ t("pendingApproval.refresh") }}
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 455a4492..a6947495 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -414,9 +414,31 @@ export const en = {
},
classroomDetails: {
label: "Classroom",
+ titlealert: "Title is required",
+ failalert: "Failed to update classroom",
+ confirmmessage: "Are you sure you want to delete the classroom? All pupils and data will be removed.",
+ save: "Save",
+ cancel: "Cancel",
+ deleteclass: "Delete classroom",
+ edit: "Edit",
+ addteachers: "Add new teachers",
+ pendingApproval: {
+ refresh: "Refresh status",
+ waiting: "Waiting for approval.",
+ request: "Your request to join the classroom is awaiting teacher approval.",
+ },
pupilOverview: {
title: "Pupils",
description: "Track progress and open pupil profiles.",
+ confirm: "Remove pupil",
+ message: "Are you sure you want to remove this pupil from the classroom?",
+ cancel: "Cancel",
+ },
+ confirmModal: {
+ cancel: "Cancel",
+ confirm: "Delete",
+ title: "Are you sure?",
+ message: "This action cannot be undone."
},
taskOverview: {
title: "Tasks",
diff --git a/src/locales/nb.ts b/src/locales/nb.ts
index 9cb06e6f..bb45d555 100644
--- a/src/locales/nb.ts
+++ b/src/locales/nb.ts
@@ -414,9 +414,31 @@ export const nb = {
},
classroomDetails: {
label: "Klasserom",
+ 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",
+ edit: "Endre",
+ addteachers: "Legg til lærere",
+ pendingApproval: {
+ refresh: "Oppdater status",
+ waiting: "Venter på godkjenning.",
+ request: "Din forespørsel på å bli med i klasserommet avventer godkjenning fra lærer.",
+ },
pupilOverview: {
title: "Elever",
description: "Følg progresjon og åpne elevprofiler.",
+ confirm: "Fjern elev",
+ message: "Er du sikker på at du vil fjerne eleven fra klasseromet?",
+ cancel: "Avbryt",
+ },
+ confirmModal: {
+ cancel: "Avbryt",
+ confirm: "Fortsett",
+ title: "Er du sikker?",
+ message: "Denne handlingen kan ikke angres."
},
taskOverview: {
title: "Oppgaver",
From 0924f134546f5bc8b4b2d06a897bf7371f34298c Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Fri, 24 Apr 2026 13:43:47 +0200
Subject: [PATCH 15/66] i18n
---
src/classrooms/components/ConfirmModal.vue | 2 +-
src/classrooms/components/PupilTable.vue | 8 ++++----
src/classrooms/pages/ClassroomDetails.vue | 19 ++++++++++---------
src/locales/en.ts | 1 +
src/locales/nb.ts | 1 +
5 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/src/classrooms/components/ConfirmModal.vue b/src/classrooms/components/ConfirmModal.vue
index 5026ade4..4ac1304b 100644
--- a/src/classrooms/components/ConfirmModal.vue
+++ b/src/classrooms/components/ConfirmModal.vue
@@ -52,7 +52,7 @@ function confirm() {
class="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-500"
@click="confirm"
>
- {{ confirmText || t("confirmModal.delete") }}
+ {{ confirmText || t("confirmModal.confirm") }}
diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue
index b6db3ae9..4dddce1c 100644
--- a/src/classrooms/components/PupilTable.vue
+++ b/src/classrooms/components/PupilTable.vue
@@ -179,10 +179,10 @@ async function approvePupil(pupilId: number) {
diff --git a/src/classrooms/pages/ClassroomDetails.vue b/src/classrooms/pages/ClassroomDetails.vue
index 6398b105..49db538c 100644
--- a/src/classrooms/pages/ClassroomDetails.vue
+++ b/src/classrooms/pages/ClassroomDetails.vue
@@ -162,7 +162,7 @@ async function saveEdit() {
- Existing teachers
+ {{ t("classroomDetails.existingteachers") }}
diff --git a/src/locales/en.ts b/src/locales/en.ts
index a6947495..a13666a8 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -421,6 +421,7 @@ export const en = {
cancel: "Cancel",
deleteclass: "Delete classroom",
edit: "Edit",
+ existingteachers: "Existing teachers",
addteachers: "Add new teachers",
pendingApproval: {
refresh: "Refresh status",
diff --git a/src/locales/nb.ts b/src/locales/nb.ts
index bb45d555..f8dd5d7b 100644
--- a/src/locales/nb.ts
+++ b/src/locales/nb.ts
@@ -422,6 +422,7 @@ export const nb = {
deleteclass: "Slett klasserom",
edit: "Endre",
addteachers: "Legg til lærere",
+ existingteachers: "Eksisterende lærere",
pendingApproval: {
refresh: "Oppdater status",
waiting: "Venter på godkjenning.",
From 3686fcd71bbd98fc0c8477e2db9fe181dfc5010d Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:20:09 +0200
Subject: [PATCH 16/66] update task view and creation
---
.../components/ClassroomTaskCard.vue | 66 +++++
.../components/ClassroomTaskSidebar.vue | 32 +++
src/classrooms/components/TaskItemsEditor.vue | 56 ++++
src/classrooms/components/TaskMetaEditor.vue | 51 ++++
.../components/TaskOptionEditor.vue | 50 ++++
.../components/TaskSideBarFilter.vue | 37 +++
src/classrooms/components/TaskSummaryCard.vue | 49 ++++
src/classrooms/pages/ClassroomTasks.vue | 251 +++++-------------
src/classrooms/pages/NewClassroomTask.vue | 151 ++++++++++-
src/stops/api/stops.api.ts | 2 +-
10 files changed, 559 insertions(+), 186 deletions(-)
create mode 100644 src/classrooms/components/ClassroomTaskCard.vue
create mode 100644 src/classrooms/components/ClassroomTaskSidebar.vue
create mode 100644 src/classrooms/components/TaskItemsEditor.vue
create mode 100644 src/classrooms/components/TaskMetaEditor.vue
create mode 100644 src/classrooms/components/TaskOptionEditor.vue
create mode 100644 src/classrooms/components/TaskSideBarFilter.vue
create mode 100644 src/classrooms/components/TaskSummaryCard.vue
diff --git a/src/classrooms/components/ClassroomTaskCard.vue b/src/classrooms/components/ClassroomTaskCard.vue
new file mode 100644
index 00000000..dd9fabf5
--- /dev/null
+++ b/src/classrooms/components/ClassroomTaskCard.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+ Title
+
+
+
+ {{ task.taskType }} · Difficulty {{ task.difficultyLevel }}
+
+
+
+
+
+ {{ expanded ? "Hide" : "View" }}
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Items: {{ task.items.length }}
+
+
+
+
+
+ Max score: {{ task.maxScore }} · Published: {{ task.published }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/components/ClassroomTaskSidebar.vue b/src/classrooms/components/ClassroomTaskSidebar.vue
new file mode 100644
index 00000000..ccd5f9bb
--- /dev/null
+++ b/src/classrooms/components/ClassroomTaskSidebar.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Filter tasks
+
+
+
+
+ {{ f }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/components/TaskItemsEditor.vue b/src/classrooms/components/TaskItemsEditor.vue
new file mode 100644
index 00000000..db12fe45
--- /dev/null
+++ b/src/classrooms/components/TaskItemsEditor.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
Item {{ index + 1 }}
+
+
+ Remove
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/classrooms/components/TaskMetaEditor.vue b/src/classrooms/components/TaskMetaEditor.vue
new file mode 100644
index 00000000..0ae5d2f9
--- /dev/null
+++ b/src/classrooms/components/TaskMetaEditor.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/src/classrooms/components/TaskOptionEditor.vue b/src/classrooms/components/TaskOptionEditor.vue
new file mode 100644
index 00000000..c696d5d3
--- /dev/null
+++ b/src/classrooms/components/TaskOptionEditor.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/components/TaskSideBarFilter.vue b/src/classrooms/components/TaskSideBarFilter.vue
new file mode 100644
index 00000000..ac9fe54e
--- /dev/null
+++ b/src/classrooms/components/TaskSideBarFilter.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+ {{ t("classroomTasks.filter") }}
+
+
+
+
+ {{ opt.label }}
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/components/TaskSummaryCard.vue b/src/classrooms/components/TaskSummaryCard.vue
new file mode 100644
index 00000000..db8b903d
--- /dev/null
+++ b/src/classrooms/components/TaskSummaryCard.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+ {{ task.title.en }}
+
+
+
+ {{ task.taskType }} · {{ task.difficultyLevel }}
+
+
+
+
+
+ {{ isOpen ? 'Collapse' : 'Expand' }}
+
+
+
+ Edit
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue
index 11efc5ee..f4b51934 100644
--- a/src/classrooms/pages/ClassroomTasks.vue
+++ b/src/classrooms/pages/ClassroomTasks.vue
@@ -1,38 +1,47 @@
-
-
-
- Classroom Tasks
-
-
-
- Create Task
-
-
-
Loading...
-
{{ error }}
-
-
-
-
-
- {{ task.title.en }}
-
-
-
- {{ task.taskType }} · Difficulty {{ task.difficultyLevel }}
-
-
-
- Published: {{ task.published }}
-
-
+
+
-
-
- Edit
-
+
+
+
+
+
Tasks
- Delete
+ Create Task
-
-
-
-
-
- {{ editingTaskId ? "Edit Task" : "Create Task" }}
-
+
{{ error }}
-
+
Loading...
-
+
+
+
-
+
-
-
-
+
\ No newline at end of file
diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue
index cbf163c0..2e34ad24 100644
--- a/src/classrooms/pages/NewClassroomTask.vue
+++ b/src/classrooms/pages/NewClassroomTask.vue
@@ -1,3 +1,152 @@
+
+
- New task
+
+
+
+ {{ isEditMode ? "Edit Task" : "Create Task" }}
+
+
+
{{ error }}
+
+
+
+
+
Items
+
+
+ Add Item
+
+
+
+
+
+
+
+ Save Task
+
+
+
+ Cancel
+
+
+
+
\ No newline at end of file
diff --git a/src/stops/api/stops.api.ts b/src/stops/api/stops.api.ts
index 42462010..40d172fc 100644
--- a/src/stops/api/stops.api.ts
+++ b/src/stops/api/stops.api.ts
@@ -68,7 +68,7 @@ export interface TaskResponse {
stopId: number
createdByTeacherId: number
title: LocalizedText
- introText: LocalizedOptionalText | null
+ introText: LocalizedOptionalText
difficultyLevel: number
taskType: TaskType
passingRule: string
From 3e74af8fd5d397ffa861fb7be2874584d540d4c9 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:40:08 +0200
Subject: [PATCH 17/66] move task related classroom components to own folder
---
src/classrooms/components/{ => tasks}/ClassroomTaskCard.vue | 2 +-
.../components/{ => tasks}/ClassroomTaskSidebar.vue | 0
src/classrooms/components/{ => tasks}/TaskItemsEditor.vue | 2 +-
src/classrooms/components/{ => tasks}/TaskMetaEditor.vue | 2 +-
src/classrooms/components/{ => tasks}/TaskOptionEditor.vue | 2 +-
src/classrooms/components/{ => tasks}/TaskSideBarFilter.vue | 2 +-
src/classrooms/components/{ => tasks}/TaskSummaryCard.vue | 2 +-
src/classrooms/pages/ClassroomTasks.vue | 4 ++--
src/classrooms/pages/NewClassroomTask.vue | 4 ++--
9 files changed, 10 insertions(+), 10 deletions(-)
rename src/classrooms/components/{ => tasks}/ClassroomTaskCard.vue (95%)
rename src/classrooms/components/{ => tasks}/ClassroomTaskSidebar.vue (100%)
rename src/classrooms/components/{ => tasks}/TaskItemsEditor.vue (99%)
rename src/classrooms/components/{ => tasks}/TaskMetaEditor.vue (95%)
rename src/classrooms/components/{ => tasks}/TaskOptionEditor.vue (93%)
rename src/classrooms/components/{ => tasks}/TaskSideBarFilter.vue (94%)
rename src/classrooms/components/{ => tasks}/TaskSummaryCard.vue (93%)
diff --git a/src/classrooms/components/ClassroomTaskCard.vue b/src/classrooms/components/tasks/ClassroomTaskCard.vue
similarity index 95%
rename from src/classrooms/components/ClassroomTaskCard.vue
rename to src/classrooms/components/tasks/ClassroomTaskCard.vue
index dd9fabf5..6e8474c8 100644
--- a/src/classrooms/components/ClassroomTaskCard.vue
+++ b/src/classrooms/components/tasks/ClassroomTaskCard.vue
@@ -1,5 +1,5 @@
-
-
-
-
-
-
-
-
- Title
-
-
-
- {{ task.taskType }} · Difficulty {{ task.difficultyLevel }}
-
-
-
-
-
- {{ expanded ? "Hide" : "View" }}
-
-
-
- Edit
-
-
-
-
-
-
-
-
- Items: {{ task.items.length }}
-
-
-
-
-
- Max score: {{ task.maxScore }} · Published: {{ task.published }}
-
-
-
-
-
\ No newline at end of file
diff --git a/src/classrooms/components/tasks/TaskSideBarFilter.vue b/src/classrooms/components/tasks/TaskSideBarFilter.vue
index 6f7ed3e2..b6e51b93 100644
--- a/src/classrooms/components/tasks/TaskSideBarFilter.vue
+++ b/src/classrooms/components/tasks/TaskSideBarFilter.vue
@@ -9,7 +9,6 @@ const model = defineModel
()
const options: Array<{ label: string; value: TaskType | "ALL" }> = [
{ label: "All", value: "ALL" },
{ label: "News", value: "NEWS_COMPARISON" },
- { label: "Email", value: "EMAIL_REVIEW" },
{ label: "Generic", value: "GENERIC" },
]
diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue
index 0ed9405b..6175530b 100644
--- a/src/classrooms/pages/ClassroomTasks.vue
+++ b/src/classrooms/pages/ClassroomTasks.vue
@@ -27,7 +27,6 @@ const filter = ref("ALL")
const authorizationHeader = computed(() => authStore.authorizationHeader)
const isTeacher = computed(() => authStore.portalType === "TEACHER")
-// 🧠 Same structure as StopPage
const answers = ref>>(new Map())
function handleSelect(taskId: number, itemId: number, optionId: number) {
@@ -38,7 +37,6 @@ function handleSelect(taskId: number, itemId: number, optionId: number) {
answers.value.get(taskId)!.set(itemId, optionId)
}
-// Sidebar filters
const taskTypes = computed(() => {
const set = new Set(tasks.value.map((t) => t.taskType))
return ["ALL", ...Array.from(set)]
@@ -49,7 +47,6 @@ const filteredTasks = computed(() => {
return tasks.value.filter((t) => t.taskType === filter.value)
})
-// Load tasks
async function loadTasks() {
if (!authorizationHeader.value) return
@@ -57,7 +54,7 @@ async function loadTasks() {
error.value = null
try {
- const stopId = 1 // TODO: make dynamic if needed
+ const stopId = 1 // TODO: make dynamic
const summaries = await requestTasksForStop(
classroomId.value,
@@ -77,7 +74,6 @@ async function loadTasks() {
}
}
-// Navigation
function goToCreate() {
router.push({
name: "new-classroom-task",
@@ -96,19 +92,17 @@ onMounted(loadTasks)
-
+
-
-
Tasks
Create Task
@@ -118,7 +112,6 @@ onMounted(loadTasks)
{{ error }}
Loading...
-
No tasks found
@@ -135,30 +128,28 @@ onMounted(loadTasks)
@select="handleSelect"
/>
+
+
+ Edit task
+
+
+
-
-
-
- Edit task {{ task.id }}
-
-
-
From 8dcd5c11250a96e6fe6cf15e7c8efc9ca863aa65 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Mon, 27 Apr 2026 13:19:07 +0200
Subject: [PATCH 21/66] update task creation and editing
---
.../components/tasks/ClassroomTaskSidebar.vue | 11 +-
.../components/tasks/TaskItemsEditor.vue | 44 ++---
.../components/tasks/TaskMetaEditor.vue | 54 +++---
.../components/tasks/TaskOptionEditor.vue | 86 ++++++---
src/classrooms/pages/EditClassroomTask.vue | 167 +++++++++++++++++-
src/classrooms/pages/NewClassroomTask.vue | 56 +++---
src/stops/api/stops.api.ts | 2 +-
7 files changed, 319 insertions(+), 101 deletions(-)
diff --git a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
index ccd5f9bb..d9323ad1 100644
--- a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
+++ b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
@@ -1,11 +1,8 @@
@@ -21,8 +18,8 @@ const emit = defineEmits<{
v-for="f in filters"
:key="f"
class="w-full rounded border px-3 py-2 text-left text-sm"
- :class="f === active ? 'bg-gray-900 text-white' : 'hover:bg-gray-50'"
- @click="emit('select', f)"
+ :class="f === model ? 'bg-gray-900 text-white' : 'hover:bg-gray-50'"
+ @click="model = f"
>
{{ f }}
diff --git a/src/classrooms/components/tasks/TaskItemsEditor.vue b/src/classrooms/components/tasks/TaskItemsEditor.vue
index af5c9d15..f4ffadcc 100644
--- a/src/classrooms/components/tasks/TaskItemsEditor.vue
+++ b/src/classrooms/components/tasks/TaskItemsEditor.vue
@@ -1,6 +1,6 @@
-
+
-
-
Item {{ index + 1 }}
+
+
+
+ Item {{ index + 1 }}
+
Remove
-
+
+
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/classrooms/components/tasks/TaskMetaEditor.vue b/src/classrooms/components/tasks/TaskMetaEditor.vue
index e729df40..5751891b 100644
--- a/src/classrooms/components/tasks/TaskMetaEditor.vue
+++ b/src/classrooms/components/tasks/TaskMetaEditor.vue
@@ -21,31 +21,45 @@ const form = defineModel
({
-
+
-
Task Settings
+
Task settings
-
-
+
+
-
-
+
+
+
-
- GENERIC
- NEWS_COMPARISON
- EMAIL_REVIEW
- IMAGE_ASSESSMENT
-
-
-
+
+
Type
+
+ Generic
+ News comparison
+ Email review
+ Image assessment
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/classrooms/components/tasks/TaskOptionEditor.vue b/src/classrooms/components/tasks/TaskOptionEditor.vue
index 640dbbb2..5171b41a 100644
--- a/src/classrooms/components/tasks/TaskOptionEditor.vue
+++ b/src/classrooms/components/tasks/TaskOptionEditor.vue
@@ -5,45 +5,79 @@ const options = defineModel
({
default: () => [],
})
+function ensureExplanation(opt: TaskOptionResponse) {
+ if (!opt.explanationText) {
+ opt.explanationText = { en: "", nb: "" }
+ }
+ return opt.explanationText
+}
+
+function addOption() {
+ options.value.push({
+ id: Date.now(),
+ optionOrder: options.value.length + 1,
+ optionText: { en: "", nb: "" },
+ isCorrect: false,
+ explanationText: { en: "", nb: "" },
+ })
+}
-
+
-
Options
+
+ Options
+
- + Add option
+ Add option
diff --git a/src/classrooms/pages/EditClassroomTask.vue b/src/classrooms/pages/EditClassroomTask.vue
index 0d5ad0cc..e8136a18 100644
--- a/src/classrooms/pages/EditClassroomTask.vue
+++ b/src/classrooms/pages/EditClassroomTask.vue
@@ -1,3 +1,168 @@
+
+
- Edit task
+
+
+
+
+ {{ isEditMode ? "Edit Task" : "Create Task" }}
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+ Items
+
+
+
+ + Add item
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ {{ isEditMode ? "Save changes" : "Create task" }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue
index 8ee779a1..2da6a67c 100644
--- a/src/classrooms/pages/NewClassroomTask.vue
+++ b/src/classrooms/pages/NewClassroomTask.vue
@@ -2,6 +2,7 @@
import { computed, ref, onMounted } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useAuthStore } from "@/auth/model/auth.store"
+
import {
requestCreateTask,
requestTask,
@@ -9,8 +10,8 @@ import {
type TaskResponse,
} from "@/stops/api/stops.api"
-import TaskMetaEditor from "@/classrooms/components/tasks/TaskMetaEditor.vue";
-import TaskItemsEditor from "@/classrooms/components/tasks/TaskItemsEditor.vue";
+import TaskMetaEditor from "@/classrooms/components/tasks/TaskMetaEditor.vue"
+import TaskItemsEditor from "@/classrooms/components/tasks/TaskItemsEditor.vue"
const route = useRoute()
const router = useRouter()
@@ -55,6 +56,9 @@ async function load() {
}
}
+/**
+ * Add item
+ */
function addItem() {
form.value.items.push({
id: Date.now(),
@@ -66,10 +70,9 @@ function addItem() {
})
}
-function removeItem(index: number) {
- form.value.items.splice(index, 1)
-}
-
+/**
+ * Save task
+ */
async function save() {
if (!authStore.authorizationHeader) return
@@ -104,48 +107,53 @@ onMounted(load)
-
+
-
- {{ isEditMode ? "Edit Task" : "Create Task" }}
-
+
+
+ {{ isEditMode ? "Edit Task" : "Create Task" }}
+
-
{{ error }}
+
+ {{ error }}
+
+
-
-
Items
+
+
Items
- Add Item
+ + Add Item
-
+
+
- Save Task
+ Cancel
- Cancel
+ {{ isEditMode ? "Save Changes" : "Create Task" }}
+
diff --git a/src/stops/api/stops.api.ts b/src/stops/api/stops.api.ts
index 9d0f2a28..e22884b5 100644
--- a/src/stops/api/stops.api.ts
+++ b/src/stops/api/stops.api.ts
@@ -50,7 +50,7 @@ export interface TaskOptionResponse {
optionOrder: number
optionText: LocalizedText
isCorrect: boolean | null
- explanationText: LocalizedOptionalText | null
+ explanationText: LocalizedOptionalText
}
export interface TaskItemResponse {
From bcd880c71905e29bb32760329f9236c78ed9956d Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Mon, 27 Apr 2026 14:57:58 +0200
Subject: [PATCH 22/66] styling
---
src/classrooms/pages/EditClassroomTask.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/classrooms/pages/EditClassroomTask.vue b/src/classrooms/pages/EditClassroomTask.vue
index e8136a18..e962c51e 100644
--- a/src/classrooms/pages/EditClassroomTask.vue
+++ b/src/classrooms/pages/EditClassroomTask.vue
@@ -144,7 +144,7 @@ onMounted(load)
/>
Date: Mon, 27 Apr 2026 22:05:15 +0200
Subject: [PATCH 23/66] update task view
---
.../components/composables/UseTeacherStops.ts | 19 +++
.../components/tasks/ClassroomTaskSidebar.vue | 37 +++--
src/classrooms/pages/ClassroomDetails.vue | 2 +-
src/classrooms/pages/ClassroomTasks.vue | 137 ++++++++----------
src/classrooms/pages/NewClassroomTask.vue | 75 +++++++---
src/classrooms/routes.ts | 3 +-
src/stops/api/stops.api.ts | 37 +++++
.../model/teacherPlaytestMap.store.ts | 5 +-
8 files changed, 203 insertions(+), 112 deletions(-)
create mode 100644 src/classrooms/components/composables/UseTeacherStops.ts
diff --git a/src/classrooms/components/composables/UseTeacherStops.ts b/src/classrooms/components/composables/UseTeacherStops.ts
new file mode 100644
index 00000000..fb90c8c7
--- /dev/null
+++ b/src/classrooms/components/composables/UseTeacherStops.ts
@@ -0,0 +1,19 @@
+import { computed, onMounted, type Ref } from 'vue'
+import { useTeacherPlaytestMapStore } from '@/teacher-playtest/model/teacherPlaytestMap.store'
+
+export function useTeacherStops(classroomId: Ref) {
+ const mapStore = useTeacherPlaytestMapStore()
+
+ onMounted(() => {
+ if (classroomId.value) {
+ mapStore.fetchMapOverview(classroomId.value)
+ }
+ })
+
+ return {
+ stops: computed(() => mapStore.stops),
+ status: computed(() => mapStore.status),
+ error: computed(() => mapStore.errorMessage),
+ reload: () => mapStore.fetchMapOverview(classroomId.value),
+ }
+}
\ No newline at end of file
diff --git a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
index d9323ad1..f1fbfcc1 100644
--- a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
+++ b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
@@ -1,27 +1,42 @@
-
-
diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue
index 2da6a67c..f27203af 100644
--- a/src/classrooms/pages/NewClassroomTask.vue
+++ b/src/classrooms/pages/NewClassroomTask.vue
@@ -1,5 +1,5 @@
@@ -86,18 +112,18 @@ function goToEdit(taskId: number) {
Tasks
+
+
+ Create Task
+
{{ error }}
Loading...
-
- Create Task
-
-
No tasks found
@@ -105,25 +131,39 @@ function goToEdit(taskId: number) {
+
+
+ Unpublished
+
+
+
+ Published
+
+
+
-
- Edit task
-
-
+
+
+ Edit task
+
+
-
-
\ No newline at end of file
diff --git a/src/classrooms/pages/EditClassroomTask.vue b/src/classrooms/pages/EditClassroomTask.vue
index e962c51e..6df41346 100644
--- a/src/classrooms/pages/EditClassroomTask.vue
+++ b/src/classrooms/pages/EditClassroomTask.vue
@@ -96,9 +96,10 @@ async function save() {
)
}
+
await router.push({
name: "classroom-tasks",
- params: { classroomId },
+ params: { classroomId, slug: 'nyhetskvartalet' },
})
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to save task"
diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue
index f27203af..6a46b064 100644
--- a/src/classrooms/pages/NewClassroomTask.vue
+++ b/src/classrooms/pages/NewClassroomTask.vue
@@ -106,9 +106,13 @@ async function save() {
)
}
+ const selectedStop = computed(() =>
+ stops.value.find(s => s.stopId === selectedStopId.value)
+ )
+
router.push({
name: "classroom-tasks",
- params: { classroomId: classroomId.value, slug: 'nyhetskvartalet' },
+ params: { classroomId: classroomId.value, slug: selectedStop.value?.slug ?? undefined },
})
} catch (e) {
error.value = e instanceof Error ? e.message : "Save failed"
From c846ab14ce41688ca47f19d33ca4ca81a65e1b72 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 11:08:05 +0200
Subject: [PATCH 26/66] update join routing to determine path based on
membership
---
src/app/router/index.ts | 19 +++++++++++++------
src/classrooms/model/classrooms.store.ts | 12 ++----------
src/classrooms/pages/JoinClassroom.vue | 6 +++++-
3 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/src/app/router/index.ts b/src/app/router/index.ts
index 1a0c6365..f973325e 100644
--- a/src/app/router/index.ts
+++ b/src/app/router/index.ts
@@ -104,19 +104,26 @@ export function createAppRouter(
const status = user.pupilProfile?.membershipStatus;
const classroomId = user.pupilProfile?.currentClassroomId;
const isJoinPage = to.name === "pupil-join-classroom";
-
- if (!classroomId && !isJoinPage) {
- return { name: "pupil-join-classroom" };
- }
+ const isPendingPage = to.name === "pupil-pending-approval";
if (status === "PENDING") {
- if (to.name !== "pupil-pending-approval") {
+ if (!isPendingPage) {
return { name: "pupil-pending-approval" };
}
+ return true;
+ }
+
+ if (status === null || status === undefined) {
+ if (!isJoinPage) {
+ return { name: "pupil-join-classroom" };
+ }
+ return true;
}
if (status === "ACTIVE" && isJoinPage) {
- return { name: "pupil-home" };
+ if (isJoinPage || isPendingPage) {
+ return { name: "pupil-home" };
+ }
}
console.log({
diff --git a/src/classrooms/model/classrooms.store.ts b/src/classrooms/model/classrooms.store.ts
index 64ed67e2..6292f5d1 100644
--- a/src/classrooms/model/classrooms.store.ts
+++ b/src/classrooms/model/classrooms.store.ts
@@ -92,20 +92,12 @@ export const useClassroomsStore = defineStore("classrooms", () => {
{ classroomCode: cleanedCode }
);
+ await authStore.fetchCurrentUserProfile()
+
await fetchClassroomById(joined.classroomId)
console.log("JOINED RESPONSE:", joined);
- if (authStore.user?.pupilProfile) {
- authStore.user = {
- ...authStore.user,
- pupilProfile: {
- ...authStore.user.pupilProfile,
- currentClassroomId: joined.classroomId,
- },
- };
- }
-
console.log("UPDATED USER:", authStore.user);
return joined;
diff --git a/src/classrooms/pages/JoinClassroom.vue b/src/classrooms/pages/JoinClassroom.vue
index 711fbf19..c6634fd1 100644
--- a/src/classrooms/pages/JoinClassroom.vue
+++ b/src/classrooms/pages/JoinClassroom.vue
@@ -43,7 +43,11 @@ async function join() {
isJoining.value = true;
try {
- await classroomsStore.joinClassroom(code);
+ await classroomsStore.joinClassroom(code)
+
+ if (authStore.user?.pupilProfile?.membershipStatus === "PENDING") {
+ await router.push({ name: "pupil-pending-approval" })
+ }
} catch (error) {
console.error("JOIN CLASSROOM ERROR:", error);
errorMessage.value = t("joinClassroom.invalid");
From a4a2d6fd973dd4b30f9350e0d237a6064b255c4b Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 11:30:24 +0200
Subject: [PATCH 27/66] add option to publish and delete tasks
---
src/classrooms/pages/ClassroomTasks.vue | 123 +++++++++++++++++++---
src/classrooms/pages/NewClassroomTask.vue | 7 +-
src/stops/api/stops.api.ts | 17 +++
src/stops/model/task.store.ts | 37 +++++++
4 files changed, 164 insertions(+), 20 deletions(-)
create mode 100644 src/stops/model/task.store.ts
diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue
index 6d970a75..c5e9425a 100644
--- a/src/classrooms/pages/ClassroomTasks.vue
+++ b/src/classrooms/pages/ClassroomTasks.vue
@@ -1,15 +1,19 @@
-
-
-
-
- {{ t("classroomTasks.filter") }}
-
-
-
-
- {{ opt.label }}
-
-
-
-
\ No newline at end of file
From 0ce327b9a0112feab8a7926adad3563e8c80a94b Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 15:39:21 +0200
Subject: [PATCH 30/66] i18n
---
.../components/tasks/ClassroomTaskSidebar.vue | 2 +-
.../components/tasks/TaskItemsEditor.vue | 16 +--
.../components/tasks/TaskMetaEditor.vue | 21 ++--
.../components/tasks/TaskOptionEditor.vue | 22 +++--
.../components/tasks/TaskSummaryCard.vue | 49 ----------
src/classrooms/pages/ClassroomTasks.vue | 37 ++++---
src/classrooms/pages/EditClassroomTask.vue | 15 +--
src/classrooms/pages/NewClassroomTask.vue | 23 +++--
src/locales/en.ts | 97 +++++++++++++++++++
src/locales/nb.ts | 56 ++++++++++-
10 files changed, 227 insertions(+), 111 deletions(-)
delete mode 100644 src/classrooms/components/tasks/TaskSummaryCard.vue
diff --git a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
index f1fbfcc1..b96de208 100644
--- a/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
+++ b/src/classrooms/components/tasks/ClassroomTaskSidebar.vue
@@ -18,7 +18,7 @@ const emit = defineEmits<{
- Stops
+ {{ t("newTask.stops") }}
diff --git a/src/classrooms/components/tasks/TaskItemsEditor.vue b/src/classrooms/components/tasks/TaskItemsEditor.vue
index f4ffadcc..3492a4dd 100644
--- a/src/classrooms/components/tasks/TaskItemsEditor.vue
+++ b/src/classrooms/components/tasks/TaskItemsEditor.vue
@@ -1,6 +1,9 @@
-
-
-
-
-
-
-
-
- {{ task.title.en }}
-
-
-
- {{ task.taskType }} · {{ task.difficultyLevel }}
-
-
-
-
-
- {{ isOpen ? 'Collapse' : 'Expand' }}
-
-
-
- Edit
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue
index c5e9425a..4a84af9b 100644
--- a/src/classrooms/pages/ClassroomTasks.vue
+++ b/src/classrooms/pages/ClassroomTasks.vue
@@ -2,6 +2,7 @@
import { computed, watch, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useAuthStore } from "@/auth/model/auth.store"
+import { useI18n } from "vue-i18n";
import TaskCard from "@/stops/components/TaskCard.vue"
import ClassroomTaskSidebar from "@/classrooms/components/tasks/ClassroomTaskSidebar.vue"
@@ -29,6 +30,8 @@ const error = ref
(null)
const filter = ref<"ALL" | string>("ALL")
+const { t } = useI18n();
+
async function loadTasksForSlug(currentSlug: string) {
if (!authStore.authorizationHeader) return
if (!currentSlug) return
@@ -56,7 +59,7 @@ async function loadTasksForSlug(currentSlug: string) {
),
)
} catch (e) {
- error.value = e instanceof Error ? e.message : "Failed to load tasks"
+ error.value = e instanceof Error ? e.message : t("taskOverview.fail")
} finally {
isLoading.value = false
}
@@ -169,13 +172,15 @@ watch(
-
Tasks
+
+ {{ t("taskOverview.title") }}
+
- Create Task
+ {{ t("taskOverview.create") }}
@@ -186,7 +191,7 @@ watch(
v-if="!isLoading && filteredTasks.length === 0"
class="text-gray-500"
>
- No tasks found
+ {{ t("taskOverview.notask") }}
- Unpublished
+ {{ t("taskOverview.unpublished") }}
- Published
+ {{ t("taskOverview.published") }}
@@ -217,7 +222,7 @@ watch(
class="rounded-xl bg-gray-100 px-3 py-1.5 text-sm"
@click="goToEdit(task.id)"
>
- Edit
+ {{ t("taskOverview.edit") }}
- {{ task.published ? "Unpublish" : "Publish" }}
+ {{ task.published ? t("taskOverview.unpublish") : t("taskOverview.publish") }}
- Delete
+ {{ t("taskOverview.delete") }}
@@ -257,17 +262,19 @@ watch(
diff --git a/src/classrooms/pages/EditClassroomTask.vue b/src/classrooms/pages/EditClassroomTask.vue
index 6df41346..f728cfca 100644
--- a/src/classrooms/pages/EditClassroomTask.vue
+++ b/src/classrooms/pages/EditClassroomTask.vue
@@ -2,6 +2,7 @@
import { computed, ref, onMounted } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useAuthStore } from "@/auth/model/auth.store"
+import { useI18n } from "vue-i18n";
import {
requestCreateTask,
@@ -17,6 +18,8 @@ const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
+const { t } = useI18n();
+
const classroomId = Number(route.params.classroomId)
const taskId = route.params.taskId ? Number(route.params.taskId) : null
@@ -57,7 +60,7 @@ async function load() {
form.value = data
} catch (e) {
- error.value = e instanceof Error ? e.message : "Failed to load task"
+ error.value = e instanceof Error ? e.message : t("newTask.savefail")
} finally {
loading.value = false
}
@@ -102,7 +105,7 @@ async function save() {
params: { classroomId, slug: 'nyhetskvartalet' },
})
} catch (e) {
- error.value = e instanceof Error ? e.message : "Failed to save task"
+ error.value = e instanceof Error ? e.message : t("newTask.savefail")
} finally {
loading.value = false
}
@@ -128,14 +131,14 @@ onMounted(load)
- Items
+ {{ t("newTask.item") }}
- + Add item
+ {{ t("newTask.additem") }}
@@ -152,7 +155,7 @@ onMounted(load)
class="rounded-lg bg-gray-200 px-5 py-2 hover:bg-gray-300 transition"
@click="router.back()"
>
- Cancel
+ {{ t("newTask.cancel") }}
- {{ isEditMode ? "Save changes" : "Create task" }}
+ {{ isEditMode ? t("newTask.save") : t("newTask.create") }}
diff --git a/src/classrooms/pages/NewClassroomTask.vue b/src/classrooms/pages/NewClassroomTask.vue
index f5ac8dbe..09262eec 100644
--- a/src/classrooms/pages/NewClassroomTask.vue
+++ b/src/classrooms/pages/NewClassroomTask.vue
@@ -2,6 +2,7 @@
import { computed, ref, onMounted, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useAuthStore } from "@/auth/model/auth.store"
+import { useI18n } from "vue-i18n";
import { requestCreateTask, requestTask, requestUpdateTask, type TaskResponse } from "@/stops/api/stops.api"
@@ -13,6 +14,8 @@ const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
+const { t } = useI18n();
+
const classroomId = computed(() => Number(route.params.classroomId))
const taskId = route.params.taskId ? Number(route.params.taskId) : null
@@ -110,7 +113,7 @@ async function save() {
params: { classroomId: classroomId.value, slug: selectedStop.value?.slug ?? undefined },
})
} catch (e) {
- error.value = e instanceof Error ? e.message : "Save failed"
+ error.value = e instanceof Error ? e.message : t("newTask.savefail")
} finally {
loading.value = false
}
@@ -124,7 +127,7 @@ onMounted(load)
- {{ isEditMode ? "Edit Task" : "Create Task" }}
+ {{ isEditMode ? t("newTask.edit") : t("newTask.create") }}
@@ -139,7 +142,9 @@ onMounted(load)
v-model="selectedStopId"
class="w-full rounded-lg border px-3 py-2"
>
- Select stop
+
+ {{ t("newTask.select") }}
+
- Loading stops...
+ {{ t("newTask.loading") }}
@@ -162,13 +167,15 @@ onMounted(load)
-
Items
+
+ {{ t("newTask.item") }}
+
- + Add Item
+ {{ t("newTask.additem") }}
@@ -183,7 +190,7 @@ onMounted(load)
class="rounded-lg bg-gray-200 px-5 py-2 hover:bg-gray-300"
@click="router.back()"
>
- Cancel
+ {{ t("newTask.cancel") }}
- {{ isEditMode ? "Save Changes" : "Create Task" }}
+ {{ isEditMode ? t("newTask.save") : t("newTask.create") }}
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 23a945ed..ffcf428e 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -590,11 +590,40 @@ export const en = {
actions: "Actions",
remove: "Remove",
seeNotebook: "See notebook",
+ pending: "Pupils awaiting approval",
+ approved: "Approved pupils",
+ approve: "Approve",
+ confirm: "Remove pupil",
+ message: "Are you sure you want to remove the pupil from the classroom?",
+ cancel: "Cancel",
},
pupilOverviewPage: {
title: "Pupils",
description: "View pupils in this classroom, sorted by highest score.",
},
+ newClassroom: {
+ button: "New classroom",
+ title: "Create a new classroom",
+ name: "Name",
+ description: "Description",
+ teachers: "Teachers",
+ example: "teacher{'@'}email.com",
+ add: "Add",
+ create: "Create classroom"
+ },
+ joinClassroom: {
+ title: "Join a classroom",
+ description: "Enter the classroom code received from your teacher.",
+ code: "Classroom code",
+ join: "Join",
+ joining: "Joining...",
+ invalid: "The classroom code is not valid",
+ empty: "Please enter a classroom code",
+ noname: "The classroom must have a name",
+ wrongemail: "The email address given does not have a valid format.",
+ atleast: "The classroom must have at least one teacher.",
+ nosave: "Could not create classroom",
+ },
classroomOverview: {
title: "Classrooms",
description:
@@ -609,13 +638,81 @@ export const en = {
tasks: "Tasks",
action: "Action",
open: "Open",
+ joinCode: "Classroom code"
+ },
+ taskOverview: {
+ title: "Tasks",
+ fail: "Failed to load tasks",
+ unpublish: "Unpublish",
+ publish: "Publish",
+ delete: "Delete task",
+ message: "Are you sure you want to delete this task? This cannot be undone.",
+ ptitle: "Change publish state",
+ pmessage: "Are you sure you want to change the publish state of this task?",
+ confirm: "Confirm",
+ create: "Create task",
+ notask: "No tasks found",
+ unpublished: "Unpublished",
+ published: "Published",
+ edit: "Edit",
+ cancel: "Cancel",
+ },
+ newTask: {
+ savefail: "Save failed",
+ edit: "Edit task",
+ create: "Create task",
+ select: "Select stop",
+ loading: "Loading stops...",
+ additem: "Add item",
+ cancel: "Cancel",
+ save: "Save",
+ stops: "Stops",
+ remove: "Remove",
+ item: "Item",
+ prompt: "Prompt",
+ ep: "English prompt",
+ np: "Norwegian prompt",
+ et: "English title",
+ nt: "Norwegian title",
+ ei: "English introduction",
+ ni: "Norwegian introduction",
+ news: "News comparison",
+ difficulty: "Difficulty",
+ options: "Options",
+ eo: "English option",
+ no: "Norwegian option",
+ correct: "Correct",
+ answer: "Answer key",
+ ea: "English explanation",
+ na: "Norwegian explanation",
+ addoption: "Add option",
},
classroomDetails: {
label: "Classroom",
+ titlealert: "The classroom must have a title.",
+ failalert: "Could not update classroom.",
+ confirmmessage: "Are you sure you want to delete the classroom? All pupils and data will be removed.",
+ save: "Save",
+ cancel: "Cancel",
+ deleteclass: "Delete classroom",
+ edit: "Edit",
+ addteachers: "Add teachers",
+ existingteachers: "Existing teachers",
+ pendingApproval: {
+ refresh: "Update status",
+ waiting: "Awaiting approval.",
+ request: "Your request to join the classroom is awaiting approval by a teacher.",
+ },
pupilOverview: {
title: "Pupils",
description: "Track progress and open pupil profiles.",
},
+ confirmModal: {
+ cancel: "Cancel",
+ confirm: "Confirm",
+ title: "Are you sure?",
+ message: "This action can not be reversed."
+ },
taskOverview: {
title: "Tasks",
description: "Choose which game tasks the class should work on.",
diff --git a/src/locales/nb.ts b/src/locales/nb.ts
index 4a002d04..14f4de61 100644
--- a/src/locales/nb.ts
+++ b/src/locales/nb.ts
@@ -586,6 +586,12 @@ export const nb = {
actions: "Handlinger",
remove: "Fjern",
seeNotebook: "Se notatbok",
+ pending: "Elever som avventer godkjenning",
+ approved: "Godkjente elever",
+ approve: "Godkjenn",
+ confirm: "Fjern elev",
+ message: "Er du sikker på at du vil fjerne eleven fra klasseromet?",
+ cancel: "Avbryt",
},
pupilOverviewPage: {
title: "Elever",
@@ -631,6 +637,53 @@ export const nb = {
open: "Åpne",
joinCode: "Klasseromskode"
},
+ taskOverview: {
+ title: "Oppgaver",
+ fail: "Kunne ikke hente oppgavene",
+ unpublish: "Avpubliser",
+ publish: "Publiser",
+ delete: "Slett oppgave",
+ message: "Er du sikker på at du vil slette oppgaven? Dette kan ikke reverseres.",
+ ptitle: "Endre publiserings status",
+ pmessage: "Er du sikker på at du vil endre publiserings statusen til oppgaven?",
+ confirm: "Fortsett",
+ create: "Opprett oppgave",
+ notask: "Ingen oppgaver funnet",
+ unpublished: "Ikke publisert",
+ published: "Publisert",
+ edit: "Rediger",
+ cancel: "Avbryt",
+ },
+ newTask: {
+ savefail: "Lagring feilet",
+ edit: "Rediger oppgave",
+ create: "Opprett oppgave",
+ select: "Velg stoppested",
+ loading: "Laster stoppested...",
+ additem: "Legg til punkt",
+ cancel: "Avbryt",
+ save: "Lagre",
+ stops: "Stoppesteder",
+ remove: "Fjern",
+ item: "Punkt",
+ prompt: "Spørsmål",
+ ep: "Engelsk spørsmål",
+ np: "Norsk spørsmål",
+ et: "Engelsk tittel",
+ nt: "Norsk tittel",
+ ei: "Engelsk introduksjon",
+ ni: "Norsk introduksjon",
+ news: "Nyhetssammenligning",
+ difficulty: "Vanskelighetsgrad",
+ options: "Valgmuligheter",
+ eo: "Engelsk valgmulighet",
+ no: "Norsk valgmulighet",
+ correct: "Riktig",
+ answer: "Fasit",
+ ea: "Engelsk forklaring",
+ na: "Norsk forklaring",
+ addoption: "Legg til valgmulighet",
+ },
classroomDetails: {
label: "Klasserom",
titlealert: "Klasserommet må ha tittel.",
@@ -650,9 +703,6 @@ export const nb = {
pupilOverview: {
title: "Elever",
description: "Følg progresjon og åpne elevprofiler.",
- confirm: "Fjern elev",
- message: "Er du sikker på at du vil fjerne eleven fra klasseromet?",
- cancel: "Avbryt",
},
confirmModal: {
cancel: "Avbryt",
From ed3ad86837d9b55ad47d924a8ea895ccd7f35cc4 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 15:54:33 +0200
Subject: [PATCH 31/66] reload en.ts
---
src/locales/en.ts | 1373 +++++++++++++++++++--------------------------
1 file changed, 563 insertions(+), 810 deletions(-)
diff --git a/src/locales/en.ts b/src/locales/en.ts
index d2077889..ffcf428e 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -1,393 +1,577 @@
export const en = {
- common: {
- pupil: "Pupil",
- teacher: "Teacher",
- classroom: "Classroom",
- classroomCode: "Classroom code",
- username: "Username",
- avatar: "Avatar",
- introduction: "Introduction",
- mission: "Mission",
- home: "Home",
- help: "Help",
- leaderboard: "Leaderboard",
- map: "Map",
- weeklyMystery: "Weekly mystery",
- medals: "Medals",
- notebook: "Notebook",
- stop: "Stop",
- stopPlace: "Stop place",
- locked: "Locked",
- task: "Task",
- progress: "Progress",
- tryAgain: "Try again",
- startGame: "Start game",
- skip: "Skip",
- notifications: "Notifications",
- settings: "Settings",
- dashboard: "Home",
- createClassroom: "Create classroom",
- save: "Save",
- back: "Back",
- or: "Or",
- },
- app: {
- title: "You did it!",
- descriptionPrefix: "Visit",
- descriptionSuffix: "to read the documentation",
- languageLabel: "Language",
- languages: {
- en: "English",
- nb: "Norwegian",
- },
- },
- auth: {
- login: {
- title: "Login",
- registeredSuccess: "The user was registered. You can now log in.",
- pupilCardTitle: "I am a pupil",
- pupilCardDescription: "Log in with Feide",
- teacherCardTitle: "I am a teacher",
- teacherCardDescription: "Log in with email and password",
- register: "Register teacher",
- usernameLabel: "Username",
- emailLabel: "Email",
- passwordLabel: "Password",
- submit: "Log in",
- loggedInAs: "Logged in as {identity}",
- pupilTitle: "Pupil login",
- pupilSubtitle: "For pupils who are logging in with Feide.",
- teacherTitle: "Teacher login",
- teacherSubtitle:
- "For teachers who are logging in with email and password.",
- },
- register: {
- title: "Teacher registration",
- subtitle:
- "This registration is for teachers who will use email and password.",
- fullNameLabel: "Name",
- fullNamePlaceholder: "Ola Nordmann",
- emailLabel: "Email",
- emailPlaceholder: "joemil{'@'}gmail.com",
- schoolNameLabel: "School",
- schoolNamePlaceholder: "Sædalen School",
- passwordLabel: "Password",
- confirmPasswordLabel: "Confirm Password",
- submit: "Register",
- validation: {
- fullNameRequired: "Enter your name.",
- emailRequired: "Enter your email address.",
- emailInvalid: "Enter a valid email address.",
- schoolNameRequired: "Enter your school.",
- passwordTooShort: "Password must be at least 8 characters long.",
- passwordsMismatch: "Passwords do not match.",
- },
- errors: {
- emailRegistered: "This email address is already registered.",
- fullNameUnsupported:
- "The name contains characters that are not allowed.",
- schoolNameUnsupported:
- "The school name contains characters that are not allowed.",
- badRequest:
- "Registration could not be completed. Check the fields and try again.",
- forbidden: "Registration was blocked. Reload the page and try again.",
- },
- },
- },
- landing: {
- title: "Nettdetektivene",
- description:
- "The public landing page is coming later. For now, you can log in or register.",
- login: "Log in",
- register: "Register",
- },
- notFound: {
- label: "404",
- title: "Page not found",
- description: "The page you are looking for does not exist.",
- action: "Go home",
- dashboardAction: "Home screen",
- },
- map: {
- loading: "Loading map…",
- lockedMessage: "Complete the previous stop first",
- home: "Detective Office",
- teacherPlaytestBadge: "Teacher playtest",
- stops: {
- nyhetskvartalet: "News Quarter",
- fotografen: "Fotograph",
- postkontoret: "Post Office",
- markedsplassen: "Market Place",
- "den-sosiale-markedsplassen": "Social Meeting Place",
- passordbanken: "Password Bank",
- datasenteret: "Data Center",
- },
- },
- stop: {
- loading: "Loading tasks…",
- finish: "Finish stop",
- submitting: "Checking answers…",
- answerAllFirst: "Answer all tasks first",
- selected: "Selected",
- selectThis: "Select this",
- correct: "Correct",
- incorrect: "Not quite right",
- backToMap: "Back to map",
- answerKey: "Answer key",
- explanation: "Explanation",
- noTasks: "No published tasks are available for this stop yet.",
- teacherPlaytestBadge: "Teacher playtest",
- unsupportedTaskType: 'Task type "{type}" is not yet supported.',
- email: {
- inbox: "Innboks",
- whatWouldYouDo: "Hva ville du gjort?",
- },
- passwordBank: {
- inputLabel: "Type a password to test",
- inputPlaceholder: "For example BlueBike!Moon7",
- rulesDone: "rules",
- rules: {
- length: "At least 10 characters",
- uppercase: "Has at least one uppercase letter",
- lowercase: "Has at least one lowercase letter",
- number: "Has at least one number",
- special: "Has at least one special character",
- },
- strength: {
- idle: "Start typing to test the strength",
- weak: "Weak password",
- okay: "Okay password",
- strong: "Strong password",
- },
- feedback: {
- idle: "A strong password is long, unusual, and hard to guess.",
- weak: "Make it longer and mix in more kinds of characters.",
- okay: "Good start. Try making it longer or less predictable.",
- success: "Great job. This password passes every rule in the game.",
- },
- successLabel: "Completed",
- successTitle: "You passed every rule",
- successDescription:
- "Your password has length, uppercase and lowercase letters, numbers, and special characters. That makes it harder to guess.",
- },
- },
- pupilHome: {
- title: "Pupil home page",
- defaultName: "Pupil",
- cards: {
- helpTitle: "Help",
- helpDescription: "Get support and guidance",
- profileTitle: "Profile",
- profileDescription: "See your name, level, and avatar",
- settingsTitle: "Settings",
- settingsDescription: "Adjust music and sound effects",
- leaderboardTitle: "Leaderboard",
- leaderboardDescription: "Overview of progress",
- mapTitle: "Game map",
- mapDescription: "Explore different tasks",
- mysteryTitle: "Weekly mystery",
- mysteryDescription: "Explore this week's mystery",
- medalsTitle: "Medals",
- medalsDescription: "See earned medals",
- notebookTitle: "Notebook",
- notebookDescription: "Open notebook",
- },
- logout: "Log out",
- guide: {
- progressLabel: "Home guide progress",
- profileTitle: "This is your profile",
- profileDescription:
- "Open your profile page here to see your name, level, and avatar. You can also replay this home guide from there later.",
- mapTitle: "This is where the game starts",
- mapDescription:
- "The game map is your main route forward. Use it to reach tasks and follow your progress.",
- notebookTitle: "This is your notebook",
- notebookDescription:
- "Use the notebook to collect information and keep track of clues as you play.",
- leaderboardTitle: "This is the leaderboard",
- leaderboardDescription:
- "Here you can compare your progress with others and see how you are doing.",
- medalsTitle: "Here you can find your medals",
- medalsDescription:
- "This is where you see which medals you have unlocked and which ones you can still work towards.",
- mysteryTitle: "Here you can find the weekly mystery",
- mysteryDescription:
- "Open the weekly mystery here and work on the extra challenge for this week.",
- helpTitle: "Here you can find help",
- helpDescription:
- "Open this page if you need support or guidance while you play.",
- nextAction: "Next",
- doneAction: "Done",
+ common: {
+ pupil: "Pupil",
+ teacher: "Teacher",
+ classroom: "Classroom",
+ classroomCode: "Classroom code",
+ username: "Username",
+ avatar: "Avatar",
+ introduction: "Introduction",
+ mission: "Mission",
+ home: "Home",
+ help: "Help",
+ leaderboard: "Leaderboard",
+ map: "Map",
+ weeklyMystery: "Weekly mystery",
+ medals: "Medals",
+ notebook: "Notebook",
+ stop: "Stop",
+ stopPlace: "Stop place",
+ locked: "Locked",
+ task: "Task",
+ progress: "Progress",
+ tryAgain: "Try again",
+ startGame: "Start game",
+ skip: "Skip",
+ notifications: "Notifications",
+ settings: "Settings",
+ dashboard: "Home",
+ createClassroom: "Create classroom",
+ save: "Save",
+ back: "Back",
+ or: "Or",
+ },
+ app: {
+ title: "You did it!",
+ descriptionPrefix: "Visit",
+ descriptionSuffix: "to read the documentation",
+ languageLabel: "Language",
+ languages: {
+ en: "English",
+ nb: "Norwegian",
+ },
},
- },
- pupilProfile: {
- title: "Profile",
- description:
- "See your pupil identity, current level, and avatar customisation options.",
- identityTitle: "Pupil information",
- displayNameLabel: "Display name",
- levelLabel: "Current level",
- xpLabel: "XP",
- editNameAction: "Edit username",
- saveNameAction: "Save name",
- saveNamePlaceholderAction: "Save name (placeholder)",
- savingNameAction: "Saving...",
- cancelNameAction: "Cancel",
- namePlaceholder: "Detective name",
- nameUpdateSuccess: "Display name updated.",
- nameUpdateError: "Could not update the display name.",
- avatarTitle: "Avatar",
- avatarPlaceholder:
- "The avatar builder is not fully implemented yet, but this is where avatar editing belongs.",
- avatarAction: "Edit avatar",
- replayHomeGuideAction: "Show the home guide again",
- avatarOptions: {
- bodyType: "Choose whether the avatar should be a boy or a girl",
- eyeColor: "Choose eye color",
- skinTone: "Choose skin tone",
- hairStyle: "Choose hair color and hairstyle",
- outfit: "Choose clothes and colors",
- accessory: "Choose a hat or accessory",
+ auth: {
+ login: {
+ title: "Login",
+ registeredSuccess: "The user was registered. You can now log in.",
+ pupilCardTitle: "I am a pupil",
+ pupilCardDescription: "Log in with Feide",
+ teacherCardTitle: "I am a teacher",
+ teacherCardDescription: "Log in with email and password",
+ register: "Register teacher",
+ usernameLabel: "Username",
+ emailLabel: "Email",
+ passwordLabel: "Password",
+ submit: "Log in",
+ loggedInAs: "Logged in as {identity}",
+ pupilTitle: "Pupil login",
+ pupilSubtitle: "For pupils who are logging in with Feide.",
+ teacherTitle: "Teacher login",
+ teacherSubtitle:
+ "For teachers who are logging in with email and password.",
+ },
+ register: {
+ title: "Teacher registration",
+ subtitle:
+ "This registration is for teachers who will use email and password.",
+ fullNameLabel: "Name",
+ fullNamePlaceholder: "Ola Nordmann",
+ emailLabel: "Email",
+ emailPlaceholder: "joemil{'@'}gmail.com",
+ schoolNameLabel: "School",
+ schoolNamePlaceholder: "Sædalen School",
+ passwordLabel: "Password",
+ confirmPasswordLabel: "Confirm Password",
+ submit: "Register",
+ validation: {
+ fullNameRequired: "Enter your name.",
+ emailRequired: "Enter your email address.",
+ emailInvalid: "Enter a valid email address.",
+ schoolNameRequired: "Enter your school.",
+ passwordTooShort:
+ "Password must be at least 8 characters long.",
+ passwordsMismatch: "Passwords do not match.",
+ },
+ errors: {
+ emailRegistered: "This email address is already registered.",
+ fullNameUnsupported:
+ "The name contains characters that are not allowed.",
+ schoolNameUnsupported:
+ "The school name contains characters that are not allowed.",
+ badRequest:
+ "Registration could not be completed. Check the fields and try again.",
+ forbidden:
+ "Registration was blocked. Reload the page and try again.",
+ },
+ },
},
- },
- pupilSettings: {
- title: "Settings",
- description: "Adjust the game's audio settings.",
- audioTitle: "Audio",
- musicLabel: "Music",
- sfxLabel: "Sound effects",
- toggleMusicMute: "Toggle music mute",
- toggleSfxMute: "Toggle sound effects mute",
- },
- onboarding: {
- intro: {
- eyebrow: "Introduction",
- title: "Welcome to Nettdetektivene",
- lead: "You are one of the detectives helping solve digital mysteries.",
- paragraphOne:
- "Throughout the game you will explore places, solve tasks, and collect clues that help move the investigation forward.",
- paragraphTwo:
- "Use what you learn about online safety, source criticism, and security to make good choices along the way.",
- paragraphThree:
- "When you are ready, choose your avatar and start the game.",
- continue: "Choose avatar",
+ landing: {
+ title: "Nettdetektivene",
+ description:
+ "The public landing page is coming later. For now, you can log in or register.",
+ login: "Log in",
+ register: "Register",
+ },
+ notFound: {
+ label: "404",
+ title: "Page not found",
+ description: "The page you are looking for does not exist.",
+ action: "Go home",
+ dashboardAction: "Home screen",
+ },
+ map: {
+ loading: "Loading map…",
+ lockedMessage: "Complete the previous stop first",
+ home: "Detective Office",
+ teacherPlaytestBadge: "Teacher playtest",
+ stops: {
+ nyhetskvartalet: "News Quarter",
+ fotografen: "Photographer",
+ postkontoret: "Post Office",
+ markedsplassen: "Market Place",
+ "den-sosiale-markedsplassen": "Social Meeting Place",
+ passordbanken: "Password Bank",
+ datasenteret: "Data Center",
+ },
},
- avatar: {
- eyebrow: "Avatar",
- title: "Create your detective",
- lead: "Customise your avatar before starting the game.",
- completing: "Starting game...",
+ stop: {
+ loading: "Loading tasks…",
+ finish: "Finish stop",
+ submitting: "Checking answers…",
+ answerAllFirst: "Answer all tasks first",
+ selected: "Selected",
+ selectThis: "Select this",
+ correct: "Correct",
+ incorrect: "Not quite right",
+ backToMap: "Back to map",
+ answerKey: "Answer key",
+ explanation: "Explanation",
+ noTasks: "No published tasks are available for this stop yet.",
+ teacherPlaytestBadge: "Teacher playtest",
+ unsupportedTaskType: 'Task type "{type}" is not yet supported.',
+ email: {
+ inbox: "Innboks",
+ whatWouldYouDo: "Hva ville du gjort?",
+ },
},
- errors: {
- complete: "Could not complete the introduction. Try again.",
+ pupilHome: {
+ title: "Pupil home page",
+ defaultName: "Pupil",
+ cards: {
+ helpTitle: "Help",
+ helpDescription: "Get support and guidance",
+ profileTitle: "Profile",
+ profileDescription: "See your name, level, and avatar",
+ settingsTitle: "Settings",
+ settingsDescription: "Adjust music and sound effects",
+ leaderboardTitle: "Leaderboard",
+ leaderboardDescription: "Overview of progress",
+ mapTitle: "Game map",
+ mapDescription: "Explore different tasks",
+ mysteryTitle: "Weekly mystery",
+ mysteryDescription: "Explore this week's mystery",
+ medalsTitle: "Medals",
+ medalsDescription: "See earned medals",
+ notebookTitle: "Notebook",
+ notebookDescription: "Open notebook",
+ },
+ logout: "Log out",
},
- },
- success: {
- stop_completed_title: "You did it!",
- stop_completed_message: "Congratulations on completing this stop!",
- return_to_map: "Back to map",
- next_stop: "Next Stop",
- next_stop_label: "Next Stop",
- view_recap: "View Map Recap",
- },
- failure: {
- stop_failed_title: "Oops!",
- stop_failed_message: "It didn't work this time.",
- try_again: "Try again",
- },
- avatarEditor: {
- detectiveName: "Detective {name}",
- fallbackDisplayName: "Unknown",
- unlocksAtLevel: "Unlocks at level {level}",
- sections: {
- bodyType: "Body Type",
- skinColor: "Skin Color",
- hats: "Hats",
- outfits: "Outfits",
+ pupilProfile: {
+ title: "Profile",
+ description:
+ "See your pupil identity, current level, and avatar customisation options.",
+ identityTitle: "Pupil information",
+ displayNameLabel: "Display name",
+ levelLabel: "Current level",
+ xpLabel: "XP",
+ editNameAction: "Edit username",
+ saveNameAction: "Save name",
+ saveNamePlaceholderAction: "Save name (placeholder)",
+ savingNameAction: "Saving...",
+ cancelNameAction: "Cancel",
+ namePlaceholder: "Detective name",
+ nameUpdateSuccess: "Display name updated.",
+ nameUpdateError: "Could not update the display name.",
+ avatarTitle: "Avatar",
+ avatarPlaceholder:
+ "The avatar builder is not fully implemented yet, but this is where avatar editing belongs.",
+ avatarAction: "Edit avatar",
+ avatarOptions: {
+ bodyType: "Choose whether the avatar should be a boy or a girl",
+ eyeColor: "Choose eye color",
+ skinTone: "Choose skin tone",
+ hairStyle: "Choose hair color and hairstyle",
+ outfit: "Choose clothes and colors",
+ accessory: "Choose a hat or accessory",
+ },
},
- bodyTypes: {
- detectiveDefault: "Boy",
- detectiveAlt: "Girl",
+ pupilSettings: {
+ title: "Settings",
+ description: "Adjust the game's audio settings.",
+ audioTitle: "Audio",
+ musicLabel: "Music",
+ sfxLabel: "Sound effects",
+ toggleMusicMute: "Toggle music mute",
+ toggleSfxMute: "Toggle sound effects mute",
},
+ success: {
+ stop_completed_title: "You did it!",
+ stop_completed_message: "Congratulations on completing this stop!",
+ return_to_map: "Back to map",
+ next_stop: "Next Stop",
+ next_stop_label: "Next Stop",
+ view_recap: "View Map Recap",
+ },
+ avatarEditor: {
+ detectiveName: "Detective {name}",
+ fallbackDisplayName: "Unknown",
+ sections: {
+ skinColor: "Skin Color",
+ outfits: "Outfits",
+ },
- preview: {
- skinToneAlt: "Avatar skin tone",
- outfitAlt: "Avatar outfit",
- },
+ preview: {
+ skinToneAlt: "Avatar skin tone",
+ outfitAlt: "Avatar outfit",
+ },
- pupilProfile: {
- title: "Profile",
- description:
- "See your pupil identity, current level, and avatar customisation options.",
- identityTitle: "Pupil information",
- displayNameLabel: "Display name",
- levelLabel: "Current level",
- xpLabel: "XP",
- editNameAction: "Change display name",
- saveNameAction: "Save display name",
- savingNameAction: "Saving...",
- cancelNameAction: "Cancel",
- namePlaceholder: "Detective name",
- nameUpdateSuccess: "Display name updated.",
- nameUpdateError: "Could not update the display name.",
- avatarTitle: "Avatar",
- avatarPlaceholder:
- "The avatar builder is not fully implemented yet, but this is where avatar editing belongs.",
- avatarAction: "Edit avatar",
- avatarOptions: {
- bodyType: "Choose whether the avatar should be a boy or a girl",
- eyeColor: "Choose eye color",
- skinTone: "Choose skin tone",
- hairStyle: "Choose hair color and hairstyle",
- outfit: "Choose clothes and colors",
- accessory: "Choose a hat or accessory",
- },
+ pupilProfile: {
+ title: "Profile",
+ description:
+ "See your pupil identity, current level, and avatar customisation options.",
+ identityTitle: "Pupil information",
+ displayNameLabel: "Display name",
+ levelLabel: "Current level",
+ xpLabel: "XP",
+ editNameAction: "Change display name",
+ saveNameAction: "Save display name",
+ savingNameAction: "Saving...",
+ cancelNameAction: "Cancel",
+ namePlaceholder: "Detective name",
+ nameUpdateSuccess: "Display name updated.",
+ nameUpdateError: "Could not update the display name.",
+ avatarTitle: "Avatar",
+ avatarPlaceholder:
+ "The avatar builder is not fully implemented yet, but this is where avatar editing belongs.",
+ avatarAction: "Edit avatar",
+ avatarOptions: {
+ bodyType: "Choose whether the avatar should be a boy or a girl",
+ eyeColor: "Choose eye color",
+ skinTone: "Choose skin tone",
+ hairStyle: "Choose hair color and hairstyle",
+ outfit: "Choose clothes and colors",
+ accessory: "Choose a hat or accessory",
+ },
+ },
+ assets: {
+ skinTones: {
+ light: "Light skin tone",
+ dark: "Dark skin tone",
+ },
+ outfits: {
+ standard: "Standard outfit",
+ standard2: "Standard outfit 2",
+ },
+ },
},
- assets: {
- skinTones: {
- light: "Light skin tone",
- dark: "Dark skin tone",
- },
- outfits: {
- standard: "Standard outfit",
- standard2: "Standard outfit 2",
- },
+ teacherHome: {
+ title: "Teacher portal",
+ description: "Choose a teacher page from the dashboard.",
+ cards: {
+ leaderboardTitle: "Leaderboard",
+ leaderboardDescription: "View your students' progress",
+ settingsTitle: "Settings",
+ settingsDescription: "Adjust audio and manage your account",
+ },
},
- },
- teacherHome: {
- title: "Teacher portal",
- description: "Choose a teacher page from the dashboard.",
- cards: {
- leaderboardTitle: "Leaderboard",
- leaderboardDescription: "View your students' progress",
- settingsTitle: "Settings",
- settingsDescription: "Adjust audio and manage your account",
+ teacherSettings: {
+ title: "Settings",
+ audioTitle: "Audio",
+ musicLabel: "Music",
+ sfxLabel: "Sound effects",
+ passwordTitle: "Password",
+ changePasswordAction: "Change password",
+ currentPasswordLabel: "Current password",
+ currentPasswordPlaceholder: "Enter current password",
+ newPasswordGroupLabel: "New password",
+ newPasswordLabel: "New password",
+ newPasswordPlaceholder: "At least 8 characters",
+ confirmPasswordLabel: "Confirm new password",
+ confirmPasswordPlaceholder: "Enter the new password again",
+ savePasswordAction: "Save password",
+ cancelPasswordAction: "Cancel",
+ savingPassword: "Saving...",
+ toggleMusicMute: "Toggle music mute",
+ toggleSfxMute: "Toggle sound effects mute",
+ validation: {
+ currentPasswordRequired: "Enter your current password.",
+ newPasswordTooShort:
+ "The new password must be at least 8 characters long.",
+ passwordsMismatch: "The passwords do not match.",
+ },
+ errors: {
+ invalidCurrentPassword: "The current password is incorrect.",
+ invalidPayload:
+ "The password could not be saved. Check the fields and try again.",
+ unauthorized: "Your session has expired. Log in again.",
+ forbidden: "This user cannot change password.",
+ generic: "The password could not be changed.",
+ },
+ success: {
+ passwordChanged: "Password updated.",
+ },
},
- },
- teacherSettings: {
- title: "Settings",
- audioTitle: "Audio",
- musicLabel: "Music",
- sfxLabel: "Sound effects",
- passwordTitle: "Password",
- changePasswordAction: "Change password",
- currentPasswordLabel: "Current password",
- currentPasswordPlaceholder: "Enter current password",
- newPasswordGroupLabel: "New password",
- newPasswordLabel: "New password",
- newPasswordPlaceholder: "At least 8 characters",
- confirmPasswordLabel: "Confirm new password",
- confirmPasswordPlaceholder: "Enter the new password again",
- savePasswordAction: "Save password",
- cancelPasswordAction: "Cancel",
- savingPassword: "Saving...",
- toggleMusicMute: "Toggle music mute",
- toggleSfxMute: "Toggle sound effects mute",
- validation: {
- currentPasswordRequired: "Enter your current password.",
- newPasswordTooShort:
- "The new password must be at least 8 characters long.",
- passwordsMismatch: "The passwords do not match.",
+ notificationsPage: {
+ title: "Notifications",
+ description: "Review and filter notifications from the classrooms you manage.",
+ classroomTitle: "Classroom notifications",
+ classroomDescription: "Review notifications connected to {classroomTitle}.",
+ classroomDescriptionGeneric: "Review and filter notifications connected to this classroom.",
+ },
+ notifications: {
+ summaryLabel: "Unread notifications",
+ listTitle: "Inbox",
+ listDescription: "Mark notifications as read, or dismiss them after handling them.",
+ loading: "Loading notifications...",
+ empty: "No notifications match your current filters.",
+ unknownPupil: "Unknown pupil",
+ unknownStop: "Unknown stop",
+ unknownTime: "Unknown time",
+ fallbackClassroom: "Classroom {classroomId}",
+ actions: {
+ markRead: "Mark as read",
+ dismiss: "Dismiss",
+ },
+ states: {
+ unread: "Unread",
+ read: "Read",
+ resolved: "Resolved",
+ },
+ pagination: {
+ previous: "Previous",
+ next: "Next",
+ },
+ paginationLabel: "Page {page} of {totalPages} · {totalItems} notifications",
+ filters: {
+ title: "Filters",
+ description: "Choose which notifications should appear in the inbox.",
+ searchLabel: "Search",
+ searchPlaceholder: "Search by pupil, classroom, or event",
+ classroomsLabel: "Classrooms",
+ typesLabel: "Notification types",
+ pupilsLabel: "Pupils",
+ pupilsHint: "Select at least one classroom first",
+ noPupilsAvailable: "No pupils found for the selected classroom filters",
+ timeWindowLabel: "Time window",
+ unreadOnlyLabel: "Show unread only",
+ fromLabel: "From",
+ toLabel: "To",
+ applyAction: "Apply filters",
+ clearAction: "Clear",
+ collapseAction: "Hide filters",
+ expandAction: "Show filters",
+ timeWindows: {
+ all: "All",
+ "24h": "Last 24 hours",
+ "7d": "Last 7 days",
+ "30d": "Last 30 days",
+ custom: "Custom",
+ },
+ },
+ mutes: {
+ title: "Mute notification types",
+ description: "Mute settings apply per classroom and notification type.",
+ collapseAction: "Hide mute settings",
+ expandAction: "Show mute settings",
+ mutedLabel: "Muted",
+ activeLabel: "Active",
+ selectSingleClassroomHint:
+ "Select one classroom in the filters to change which notification types are muted.",
+ },
+ errors: {
+ loadInboxFailed: "Could not load notifications.",
+ loadReferencesFailed: "Could not load filter data.",
+ loadSummaryFailed: "Could not load the summary.",
+ markReadFailed: "Could not mark the notification as read.",
+ dismissFailed: "Could not dismiss the notification.",
+ muteFailed: "Could not update the mute setting.",
+ },
+ types: {
+ MAP_STOP_COMPLETED: "Stop completed",
+ GAME_COMPLETED: "Game completed",
+ PUPIL_STUCK: "Pupil stuck",
+ WEEKLY_MYSTERY_SUBMITTED: "Weekly mystery submitted",
+ },
+ "map-stop-completed":
+ "{pupilDisplayName} completed {stopTitle} in {classroomTitle}.",
+ "game-completed":
+ "{pupilDisplayName} completed the game in {classroomTitle}.",
+ "pupil-stuck":
+ "{pupilDisplayName} is stuck on {stopTitle} in {classroomTitle}.",
+ "weekly-mystery-submitted":
+ "{pupilDisplayName} submitted the weekly mystery in {classroomTitle}.",
+ },
+ pages: {
+ help: "Help",
+ leaderboard: "Leaderboard",
+ notebook: "Notebook",
+ mystery: "Mystery",
+ medals: "Medals",
+ profile: "Profile",
+ settings: "Settings",
+ },
+ medals: {
+ title: "Medals",
+ description:
+ "This is where earned and locked medals are shown. Each medal keeps a dedicated asset slot so artwork can be added later without changing the page structure.",
+ filters: {
+ all: "All",
+ earned: "Earned",
+ locked: "Locked",
+ },
+ summary: {
+ earned: "Earned",
+ locked: "Locked",
+ total: "Total",
+ ofTotal: "{percent}% of {total}",
+ lockedDetail: "Medals still missing",
+ totalDetail: "Registered medals",
+ },
+ empty: {
+ title: "No medals yet",
+ description:
+ "When medals are available from the backend, they will appear here in a clear overview with room for icon art and status.",
+ },
+ errors: {
+ unauthenticated: "You must be logged in to view medals.",
+ loadFailed: "Could not load medals.",
+ },
+ card: {
+ earned: "Earned",
+ locked: "Locked",
+ example: "Approved example",
+ stop: "Stop",
+ noDescription: "No description has been added yet.",
+ assetReady: "Ready for image asset",
+ assetSlot: "Asset slot: {path}",
+ },
+ patterns: {
+ completionTitle: "{stop} completed",
+ completionDescription: "Awarded for completing the {stop} stop.",
+ },
+ types: {
+ stopCompletion: "Stop completion",
+ exampleApproval: "Example approval",
+ },
+ definitions: {
+ exampleApproval: {
+ title: "Example approval",
+ description: "Awarded when a submitted example is approved.",
+ },
+ stops: {
+ nyhetskvartalet: {
+ title: "News Quarter completed",
+ description:
+ "Awarded when the News Quarter stop is completed.",
+ },
+ fotografen: {
+ title: "Fotograph completed",
+ description:
+ "Awarded when the Fotograph stop is completed.",
+ },
+ postkontoret: {
+ title: "Post Office completed",
+ description:
+ "Awarded when the Post Office stop is completed.",
+ },
+ markedsplassen: {
+ title: "Market Place completed",
+ description:
+ "Awarded when the Market Place stop is completed.",
+ },
+ "den-sosiale-markedsplassen": {
+ title: "Social Meeting Place completed",
+ description:
+ "Awarded when the Social Meeting Place stop is completed.",
+ },
+ passordbanken: {
+ title: "Password Bank completed",
+ description:
+ "Awarded when the Password Bank stop is completed.",
+ },
+ datasenteret: {
+ title: "Data Center completed",
+ description:
+ "Awarded when the Data Center stop is completed.",
+ },
+ },
+ },
+ codes: {
+ "example-approval": {
+ title: "Example approval",
+ description: "Awarded when a submitted example is approved.",
+ },
+ "nyhetskvartalet-completion": {
+ title: "News Quarter completed",
+ description:
+ "Awarded when the News Quarter stop is completed.",
+ },
+ "fotografen-completion": {
+ title: "Fotograph completed",
+ description: "Awarded when the Fotograph stop is completed.",
+ },
+ "postkontoret-completion": {
+ title: "Post Office completed",
+ description: "Awarded when the Post Office stop is completed.",
+ },
+ "markedsplassen-completion": {
+ title: "Market Place completed",
+ description:
+ "Awarded when the Market Place stop is completed.",
+ },
+ "den-sosiale-markedsplassen-completion": {
+ title: "Social Meeting Place completed",
+ description:
+ "Awarded when the Social Meeting Place stop is completed.",
+ },
+ "passordbanken-completion": {
+ title: "Password Bank completed",
+ description:
+ "Awarded when the Password Bank stop is completed.",
+ },
+ "datasenteret-completion": {
+ title: "Data Center completed",
+ description: "Awarded when the Data Center stop is completed.",
+ },
+ },
+ sections: {
+ earned: "Your earned medals",
+ earnedHint: "These are the ones you already got.",
+ locked: "Medals you can earn",
+ lockedHint: "These are not earned yet.",
+ noneEarned: "You have not earned any medals yet.",
+ },
+ emptyEarned: "Keep going! Your first medal will appear here.",
+ emptyLocked: "No medals have been added yet.",
+ },
+ notebook: {
+ fallbackTitle: "Notebook",
+ stopLabel: "Stop {id}",
+ sidebarTitle: "Stops",
+ emptySection: "No notes for this stop yet.",
+ loading: "Loading notes...",
+ form: {
+ titlePlaceholder: "Title",
+ contentPlaceholder: "Note",
+ submit: "Create note",
+ },
+ actions: {
+ deleteEntry: "Delete entry",
+ },
+ errors: {
+ createRequiresAuth: "You must be logged in to create notes.",
+ missingStop: "Could not find a valid stop for the note.",
+ missingFields: "Both title and content are required.",
+ createFailed: "Could not create the note.",
+ gameDeleteForbidden: "Game-generated notes cannot be deleted.",
+ deleteRequiresAuth: "You must be logged in to delete notes.",
+ deleteFailed: "Could not delete the note.",
+ loadRequiresAuth: "You must be logged in to open the notebook.",
+ loadFailed: "Could not load the notes.",
+ missingTeacherContext: "Could not find the pupil or classroom for this notebook.",
+ },
},
shared: {
logout: "Log out",
@@ -545,437 +729,6 @@ export const en = {
title: "Notifications",
description: "Review events and messages for this classroom.",
},
- errors: {
- invalidCurrentPassword: "The current password is incorrect.",
- invalidPayload:
- "The password could not be saved. Check the fields and try again.",
- unauthorized: "Your session has expired. Log in again.",
- forbidden: "This user cannot change password.",
- generic: "The password could not be changed.",
- },
- success: {
- passwordChanged: "Password updated.",
- },
- },
- notificationsPage: {
- title: "Notifications",
- description:
- "Review and filter notifications from the classrooms you manage.",
- classroomTitle: "Classroom notifications",
- classroomDescription: "Review notifications connected to {classroomTitle}.",
- classroomDescriptionGeneric:
- "Review and filter notifications connected to this classroom.",
- },
- notifications: {
- summaryLabel: "Unread notifications",
- listTitle: "Inbox",
- listDescription:
- "Mark notifications as read, or dismiss them after handling them.",
- loading: "Loading notifications...",
- empty: "No notifications match your current filters.",
- unknownPupil: "Unknown pupil",
- unknownStop: "Unknown stop",
- unknownTime: "Unknown time",
- fallbackClassroom: "Classroom {classroomId}",
- actions: {
- markRead: "Mark as read",
- dismiss: "Dismiss",
- },
- states: {
- unread: "Unread",
- read: "Read",
- resolved: "Resolved",
- },
- pagination: {
- previous: "Previous",
- next: "Next",
- },
- paginationLabel: "Page {page} of {totalPages} · {totalItems} notifications",
- filters: {
- title: "Filters",
- description: "Choose which notifications should appear in the inbox.",
- searchLabel: "Search",
- searchPlaceholder: "Search by pupil, classroom, or event",
- classroomsLabel: "Classrooms",
- typesLabel: "Notification types",
- pupilsLabel: "Pupils",
- pupilsHint: "Select at least one classroom first",
- noPupilsAvailable: "No pupils found for the selected classroom filters",
- timeWindowLabel: "Time window",
- unreadOnlyLabel: "Show unread only",
- fromLabel: "From",
- toLabel: "To",
- applyAction: "Apply filters",
- clearAction: "Clear",
- collapseAction: "Hide filters",
- expandAction: "Show filters",
- timeWindows: {
- all: "All",
- "24h": "Last 24 hours",
- "7d": "Last 7 days",
- "30d": "Last 30 days",
- custom: "Custom",
- },
- },
- mutes: {
- title: "Mute notification types",
- description: "Mute settings apply per classroom and notification type.",
- collapseAction: "Hide mute settings",
- expandAction: "Show mute settings",
- mutedLabel: "Muted",
- activeLabel: "Active",
- selectSingleClassroomHint:
- "Select one classroom in the filters to change which notification types are muted.",
- },
- errors: {
- loadInboxFailed: "Could not load notifications.",
- loadReferencesFailed: "Could not load filter data.",
- loadSummaryFailed: "Could not load the summary.",
- markReadFailed: "Could not mark the notification as read.",
- dismissFailed: "Could not dismiss the notification.",
- muteFailed: "Could not update the mute setting.",
- },
- types: {
- MAP_STOP_COMPLETED: "Stop completed",
- GAME_COMPLETED: "Game completed",
- PUPIL_STUCK: "Pupil stuck",
- WEEKLY_MYSTERY_SUBMITTED: "Weekly mystery submitted",
- },
- "map-stop-completed":
- "{pupilDisplayName} completed {stopTitle} in {classroomTitle}.",
- "game-completed":
- "{pupilDisplayName} completed the game in {classroomTitle}.",
- "pupil-stuck":
- "{pupilDisplayName} is stuck on {stopTitle} in {classroomTitle}.",
- "weekly-mystery-submitted":
- "{pupilDisplayName} submitted the weekly mystery in {classroomTitle}.",
- },
- pages: {
- help: "Help",
- leaderboard: "Leaderboard",
- notebook: "Notebook",
- mystery: "Mystery",
- medals: "Medals",
- profile: "Profile",
- settings: "Settings",
- },
- medals: {
- title: "Medals",
- description:
- "This is where earned and locked medals are shown. Each medal keeps a dedicated asset slot so artwork can be added later without changing the page structure.",
- filters: {
- all: "All",
- earned: "Earned",
- locked: "Locked",
- },
- summary: {
- earned: "Earned",
- locked: "Locked",
- total: "Total",
- ofTotal: "{percent}% of {total}",
- lockedDetail: "Medals still missing",
- totalDetail: "Registered medals",
- },
- empty: {
- title: "No medals yet",
- description:
- "When medals are available from the backend, they will appear here in a clear overview with room for icon art and status.",
- },
- errors: {
- unauthenticated: "You must be logged in to view medals.",
- loadFailed: "Could not load medals.",
- },
- card: {
- earned: "Earned",
- locked: "Locked",
- example: "Approved example",
- stop: "Stop",
- noDescription: "No description has been added yet.",
- assetReady: "Ready for image asset",
- assetSlot: "Asset slot: {path}",
- },
- patterns: {
- completionTitle: "{stop} completed",
- completionDescription: "Awarded for completing the {stop} stop.",
- },
- types: {
- stopCompletion: "Stop completion",
- exampleApproval: "Example approval",
- },
- definitions: {
- exampleApproval: {
- title: "Example approval",
- description: "Awarded when a submitted example is approved.",
- },
- stops: {
- nyhetskvartalet: {
- title: "News Quarter completed",
- description: "Awarded when the News Quarter stop is completed.",
- },
- fotografen: {
- title: "Fotograph completed",
- description: "Awarded when the Fotograph stop is completed.",
- },
- postkontoret: {
- title: "Post Office completed",
- description: "Awarded when the Post Office stop is completed.",
- },
- markedsplassen: {
- title: "Market Place completed",
- description: "Awarded when the Market Place stop is completed.",
- },
- "den-sosiale-markedsplassen": {
- title: "Social Meeting Place completed",
- description:
- "Awarded when the Social Meeting Place stop is completed.",
- },
- passordbanken: {
- title: "Password Bank completed",
- description: "Awarded when the Password Bank stop is completed.",
- },
- datasenteret: {
- title: "Data Center completed",
- description: "Awarded when the Data Center stop is completed.",
- },
- },
- },
- codes: {
- "example-approval": {
- title: "Example approval",
- description: "Awarded when a submitted example is approved.",
- },
- "nyhetskvartalet-completion": {
- title: "News Quarter completed",
- description: "Awarded when the News Quarter stop is completed.",
- },
- "fotografen-completion": {
- title: "Fotograph completed",
- description: "Awarded when the Fotograph stop is completed.",
- },
- "postkontoret-completion": {
- title: "Post Office completed",
- description: "Awarded when the Post Office stop is completed.",
- },
- "markedsplassen-completion": {
- title: "Market Place completed",
- description: "Awarded when the Market Place stop is completed.",
- },
- "den-sosiale-markedsplassen-completion": {
- title: "Social Meeting Place completed",
- description: "Awarded when the Social Meeting Place stop is completed.",
- },
- "passordbanken-completion": {
- title: "Password Bank completed",
- description: "Awarded when the Password Bank stop is completed.",
- },
- "datasenteret-completion": {
- title: "Data Center completed",
- description: "Awarded when the Data Center stop is completed.",
- },
- },
- sections: {
- earned: "Your earned medals",
- earnedHint: "These are the ones you already got.",
- locked: "Medals you can earn",
- lockedHint: "These are not earned yet.",
- noneEarned: "You have not earned any medals yet.",
- },
- emptyEarned: "Keep going! Your first medal will appear here.",
- emptyLocked: "No medals have been added yet.",
- },
- notebook: {
- fallbackTitle: "Notebook",
- stopLabel: "Stop {id}",
- sidebarTitle: "Stops",
- emptySection: "No notes for this stop yet.",
- loading: "Loading notes...",
- form: {
- titlePlaceholder: "Title",
- contentPlaceholder: "Note",
- submit: "Create note",
},
- actions: {
- deleteEntry: "Delete entry",
- },
- errors: {
- createRequiresAuth: "You must be logged in to create notes.",
- missingStop: "Could not find a valid stop for the note.",
- missingFields: "Both title and content are required.",
- createFailed: "Could not create the note.",
- gameDeleteForbidden: "Game-generated notes cannot be deleted.",
- deleteRequiresAuth: "You must be logged in to delete notes.",
- deleteFailed: "Could not delete the note.",
- loadRequiresAuth: "You must be logged in to open the notebook.",
- loadFailed: "Could not load the notes.",
- missingTeacherContext:
- "Could not find the pupil or classroom for this notebook.",
- },
- },
- shared: {
- logout: "Log out",
- },
- leaderboard: {
- rank: "Rank",
- classroom: "Classroom",
- averageScore: "Average score",
- description: "Here you can view classroom rankings by average score",
- },
- pupilTable: {
- name: "Name",
- score: "Score",
- xp: "XP",
- level: "Level",
- actions: "Actions",
- remove: "Remove",
- seeNotebook: "See notebook",
- },
- pupilOverviewPage: {
- title: "Pupils",
- description: "View pupils in this classroom, sorted by highest score.",
- },
- classroomOverview: {
- title: "Classrooms",
- description:
- "View classroom status and open the classroom you want to manage.",
- },
- classroomTable: {
- classroom: "Classroom",
- description: "Description",
- pupils: "Pupils",
- pending: "Pending",
- teachers: "Teachers",
- tasks: "Tasks",
- action: "Action",
- open: "Open",
- },
- classroomDetails: {
- label: "Classroom",
- pupilOverview: {
- title: "Pupils",
- description: "Track progress and open pupil profiles.",
- },
- taskOverview: {
- title: "Tasks",
- description: "Choose which game tasks the class should work on.",
- },
- gameOverview: {
- title: "Test the game",
- description: "Try the pupil experience as the class will see it.",
- },
- mysteryOverview: {
- title: "Weekly mystery",
- description: "Check status and edit the weekly mystery.",
- },
- notificationOverview: {
- title: "Notifications",
- description: "Review events and messages for this classroom.",
- },
- mystery: {
- title: 'Weekly mystery',
- subtitle: "See what your teacher has picked, and submit your own find!",
- loading: 'Loading mysteries...',
- weekLabel: 'Week {week}',
- prevWeek: 'Previous week',
- nextWeek: 'Next week',
- noMysteries: 'No mysteries have been posted yet.',
- noMysteriesThisWeek: 'No mysteries this week.',
- submissionImageAlt: 'Submission image',
-
- modal: {
- close: 'Close',
- },
-
- tabs: {
- featured: 'This week',
- submit: 'Submit',
- mine: 'My submissions',
- },
-
- reviewStatus: {
- pending: 'Pending',
- approved: 'Approved',
- rejected: 'Rejected',
- },
-
- submitForm: {
- heading: 'Submit a find',
- descriptionLabel: 'Write a comment',
- descriptionPlaceholder: 'Comment on what you found — what looks suspicious? Where did you find it?',
- uploadLabel: 'Upload image',
- previewAlt: 'Preview of uploaded image',
- removeImage: 'Remove image',
- submit: 'Submit',
- submitting: 'Submitting...',
- success: 'Your find has been submitted! Your teacher will review it soon.',
- unsupportedFileType: 'File type not supported. Please upload an image (JPEG, PNG, GIF or WebP).',
- },
-
- mySubmissions: {
- heading: 'My submissions',
- empty: "You haven't submitted anything yet. Try finding fake news online!",
- teacherComment: "Teacher's comment",
- },
-
- errors: {
- notAuthenticated: 'You must be logged in to view mysteries.',
- loadFailed: 'Could not load mysteries. Please try again.',
- submitFailed: 'Submission failed. Please try again.',
- },
- },
- teacherMystery: {
- title: 'Weekly mystery',
- subtitle: 'Review submitted examples and choose this week\'s mysteries.',
- loading: 'Loading...',
- weekLabel: 'Week {week}',
- prevWeek: 'Previous week',
- nextWeek: 'Next week',
- pupilLabel: 'Pupil',
-
- tabs: {
- pending: 'Pending review',
- approved: 'Approved',
- featured: 'This week\'s mysteries',
- },
-
- pending: {
- empty: 'No submissions are waiting for review.',
- },
-
- approved: {
- empty: 'No approved submissions yet.',
- reviewedBy: 'Approved',
- featuredThisWeek: 'Featured this week',
- },
-
- featured: {
- empty: 'No mysteries have been posted yet. Approve a submission and add it here.',
- add: 'Add mystery',
- },
-
- actions: {
- approve: 'Approve',
- reject: 'Reject',
- feature: 'Choose week',
- unfeature: 'Remove',
- },
-
- modal: {
- approveTitle: 'Approve submission',
- rejectTitle: 'Reject submission',
- commentLabel: 'Comment to pupil (optional)',
- commentPlaceholder: 'Great find! You noticed that...',
- featureTitle: 'Add as this week\'s mystery',
- weekLabel: 'Select week',
- featureConfirm: 'Add',
- cancel: 'Cancel',
- },
-
- errors: {
- reviewFailed: 'Could not review the submission. Please try again.',
- featureFailed: 'Could not add mystery. Please try again.',
- },
- },
- },
- joinCode: "Classroom code",
-} as const;
-
+ joinCode: "Classroom code",
+ } as const;
From 85846a85be6861366842a28127496719a7fd5add Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 16:23:25 +0200
Subject: [PATCH 32/66] update routing
---
src/app/__tests__/NotFoundPage.spec.ts | 2 +-
src/classrooms/routes.ts | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/app/__tests__/NotFoundPage.spec.ts b/src/app/__tests__/NotFoundPage.spec.ts
index 1c45e292..70f4501c 100644
--- a/src/app/__tests__/NotFoundPage.spec.ts
+++ b/src/app/__tests__/NotFoundPage.spec.ts
@@ -69,4 +69,4 @@ describe('NotFoundPage', () => {
expect(link.attributes('href')).toBe('/teacher/home')
expect(link.text()).toBe('Hjemskjerm')
})
-})
+})
\ No newline at end of file
diff --git a/src/classrooms/routes.ts b/src/classrooms/routes.ts
index 9a746454..0f683214 100644
--- a/src/classrooms/routes.ts
+++ b/src/classrooms/routes.ts
@@ -9,7 +9,6 @@ import TeacherPlaytestMapPage from "@/teacher-playtest/pages/TeacherPlaytestMapP
import NotificationsPage from "@/notifications/pages/NotificationsPage.vue";
import TeacherPlaytestStopPage from "@/teacher-playtest/pages/TeacherPlaytestStopPage.vue";
import ClassroomTasks from "@/classrooms/pages/ClassroomTasks.vue";
-import ClassroomMystery from "@/classrooms/pages/ClassroomMystery.vue";
import NewClassroomTask from "@/classrooms/pages/NewClassroomTask.vue";
import EditClassroomTask from "@/classrooms/pages/EditClassroomTask.vue";
import PendingApproval from "@/classrooms/pages/PendingApproval.vue";
From c97c56e9a6f3974886527be127734d91302deff9 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 16:23:53 +0200
Subject: [PATCH 33/66] update routing
---
src/app/router/index.ts | 108 ++++++++++++++++------------------------
1 file changed, 42 insertions(+), 66 deletions(-)
diff --git a/src/app/router/index.ts b/src/app/router/index.ts
index 2e571841..297459f4 100644
--- a/src/app/router/index.ts
+++ b/src/app/router/index.ts
@@ -80,86 +80,62 @@ export function createAppRouter(
return true
}
- const hasCompletedOnboarding = authStore.user?.pupilProfile?.hasCompletedOnboarding
- const classroomId = authStore.user?.pupilProfile?.currentClassroomId
- const hasClassroom = classroomId !== null && classroomId !== undefined
+ const user = authStore.user
+ const portalType = authStore.portalType
- const isGuestOnly = to.matched.some((record) => record.meta.guestOnly)
- if (routeAccess === 'public' && isGuestOnly) {
- return resolvePostLoginLocation(router, to.query.redirect, {
- portalType: authStore.portalType,
- hasCompletedOnboarding,
- hasClassroom,
- })
- }
+ const hasCompletedOnboarding = user?.pupilProfile?.hasCompletedOnboarding ?? false
- if (!canAccessRoute(to, authStore.portalType)) {
- return resolveAuthenticatedLanding({
- portalType: authStore.portalType,
- hasCompletedOnboarding,
- hasClassroom,
- })
- }
+ const classroomId = user?.pupilProfile?.currentClassroomId ?? null
+ const membershipStatus = user?.pupilProfile?.membershipStatus ?? null
- if (
- authStore.portalType === 'PUPIL' &&
- !hasClassroom &&
- to.name !== 'pupil-join-classroom'
- ) {
- return { name: 'pupil-join-classroom' }
- }
+ const isJoinPage = to.name === "pupil-join-classroom"
+ const isPendingPage = to.name === "pupil-pending-approval"
+ const isOnboardingPage = to.name === "pupil-onboarding"
- if (
- authStore.portalType === 'PUPIL' &&
- hasCompletedOnboarding === true &&
- to.name === 'pupil-onboarding'
- ) {
- return resolveAuthenticatedLanding({
- portalType: authStore.portalType,
- hasCompletedOnboarding,
- hasClassroom,
- })
- }
-
- if (
- authStore.portalType === 'PUPIL' &&
- hasCompletedOnboarding === false &&
- hasClassroom &&
- !to.matched.some((record) => record.meta.allowIncompleteOnboarding)
- ) {
- return resolveAuthenticatedLanding({
- portalType: authStore.portalType,
- hasCompletedOnboarding,
- hasClassroom,
- })
+ if (portalType === "PUPIL") {
+ if (!classroomId) {
+ if (!isJoinPage) {
+ return {name: "pupil-join-classroom"}
+ }
+ return true
+ }
}
- const status = user.pupilProfile?.membershipStatus;
- const classroomId = user.pupilProfile?.currentClassroomId;
- const isJoinPage = to.name === "pupil-join-classroom";
- const isPendingPage = to.name === "pupil-pending-approval";
-
- if (status === "PENDING") {
- if (!isPendingPage) {
- return { name: "pupil-pending-approval" };
- }
- return true;
+ if (membershipStatus === "PENDING") {
+ if (!isPendingPage) {
+ return {name: "pupil-pending-approval"}
}
+ return true
+ }
- if (status === null || status === undefined) {
- if (!isJoinPage) {
- return { name: "pupil-join-classroom" };
+ if (membershipStatus === "ACTIVE") {
+ if (!hasCompletedOnboarding) {
+ if (!isOnboardingPage) {
+ return {name: "pupil-onboarding"}
}
- return true;
+ return true
}
- if (status === "ACTIVE" && isJoinPage) {
- if (isJoinPage || isPendingPage) {
- return { name: "pupil-home" };
- }
+ if (isJoinPage || isPendingPage || isOnboardingPage) {
+ return {name: "pupil-home"}
}
}
+ if (!canAccessRoute(to, portalType)) {
+ return resolveAuthenticatedLanding({
+ portalType,
+ hasCompletedOnboarding,
+ hasClassroom: !!classroomId,
+ })
+ }
+ const isGuestOnly = to.matched.some((r) => r.meta.guestOnly)
+ if (routeAccess === "public" && isGuestOnly) {
+ return resolvePostLoginLocation(router, to.query.redirect, {
+ portalType,
+ hasCompletedOnboarding,
+ hasClassroom: !!classroomId,
+ })
+ }
return true
})
From 9e4e45d63af28ff02d29d54af30d1de721bb3a5a Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 16:50:02 +0200
Subject: [PATCH 34/66] add notebook back to pupiltable
---
.../components/__tests__/PupilTable.spec.ts | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/classrooms/components/__tests__/PupilTable.spec.ts b/src/classrooms/components/__tests__/PupilTable.spec.ts
index 095b84f1..47667dd1 100644
--- a/src/classrooms/components/__tests__/PupilTable.spec.ts
+++ b/src/classrooms/components/__tests__/PupilTable.spec.ts
@@ -6,13 +6,14 @@ import { createAppI18n } from '@/app/i18n'
import { useAuthStore } from '@/auth/model/auth.store'
import PupilTable from '@/classrooms/components/PupilTable.vue'
-vi.mock('@/classrooms/store/classrooms.store.ts', () => ({
+vi.mock('@/classrooms/model/classrooms.store.ts', () => ({
useClassroomsStore: () => ({
selectedClassroom: {
pupils: [
{
userId: 15,
displayName: 'Ada',
+ pupilStatus: 'ACTIVE',
},
],
},
@@ -70,11 +71,15 @@ describe('PupilTable', () => {
await flushPromises()
- const links = wrapper.findAll('a')
- const notebookLink = links[1]
+ const links = wrapper.findAllComponents({ name: 'RouterLink' })
- expect(notebookLink?.attributes('href')).toBe(
- '/teacher/classrooms/7/pupils/15/notebook'
+ const notebookLink = links.find(link =>
+ link.props('to')?.name === 'classroom-pupil-notebook'
)
+
+ expect(notebookLink?.props('to')).toEqual({
+ name: 'classroom-pupil-notebook',
+ params: { classroomId: 7, pupilId: 15 },
+ })
})
})
\ No newline at end of file
From 32ed411c95978f962768c2935cea376e3e8c9230 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 16:50:14 +0200
Subject: [PATCH 35/66] add notebook back to pupiltable
---
src/classrooms/components/PupilTable.vue | 26 +++++++++++++++++-------
1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/classrooms/components/PupilTable.vue b/src/classrooms/components/PupilTable.vue
index 94417644..01045712 100644
--- a/src/classrooms/components/PupilTable.vue
+++ b/src/classrooms/components/PupilTable.vue
@@ -159,13 +159,25 @@ async function approvePupil(pupilId: number) {
-
- {{ t("pupilTable.remove") }}
-
+
+
+ {{ t("pupilTable.seeNotebook") }}
+
+
+
+ {{ t("pupilTable.remove") }}
+
+
From 1a1603e6f119916ff7866ad398ffb77a277a5ae2 Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 16:50:24 +0200
Subject: [PATCH 36/66] fix test
---
src/classrooms/_tests_/classrooms.store.spec.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/classrooms/_tests_/classrooms.store.spec.ts b/src/classrooms/_tests_/classrooms.store.spec.ts
index b5223157..4e5316ba 100644
--- a/src/classrooms/_tests_/classrooms.store.spec.ts
+++ b/src/classrooms/_tests_/classrooms.store.spec.ts
@@ -15,6 +15,7 @@ describe("classrooms store", () => {
function mockAuth() {
const auth = useAuthStore();
auth.accessToken = "token";
+ auth.fetchCurrentUserProfile = vi.fn()
auth.user = {
subject: "teacher-1",
portalType: "TEACHER",
From 0e50fbf35992d794f3e647613484c36524f5f60a Mon Sep 17 00:00:00 2001
From: Maria <145002050+marikolafs@users.noreply.github.com>
Date: Tue, 28 Apr 2026 17:13:08 +0200
Subject: [PATCH 37/66] fix build errors
---
src/app/router/__tests__/auth-routing.spec.ts | 4 ++++
src/avatar/model/__tests__/avatar.store.spec.ts | 1 +
src/classrooms/pages/ClassroomTasks.vue | 10 ++++++++--
.../components/__tests__/LeaderboardComponent.spec.ts | 3 ++-
src/mystery/model/__tests__/mystery.store.spec.ts | 2 ++
src/profile/pages/__tests__/PupilProfilePage.spec.ts | 1 +
src/pupil-portal/pages/__tests__/PupilHomePage.spec.ts | 2 ++
.../pages/__tests__/PupilOnboardingPage.spec.ts | 2 ++
.../model/__tests__/teacher-mystery.store.spec.ts | 2 ++
9 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/src/app/router/__tests__/auth-routing.spec.ts b/src/app/router/__tests__/auth-routing.spec.ts
index 7dd39b61..a647f6a0 100644
--- a/src/app/router/__tests__/auth-routing.spec.ts
+++ b/src/app/router/__tests__/auth-routing.spec.ts
@@ -33,6 +33,7 @@ function authenticatedUser(
currentLevel: 1,
currentClassroomId: null,
hasCompletedOnboarding: true,
+ membershipStatus: null,
}
: null,
teacherProfile: null,
@@ -224,6 +225,7 @@ describe("authenticated routing", () => {
currentLevel: 1,
currentClassroomId: null,
hasCompletedOnboarding: false,
+ membershipStatus: null,
},
});
@@ -246,6 +248,7 @@ describe("authenticated routing", () => {
currentLevel: 1,
currentClassroomId: 101,
hasCompletedOnboarding: false,
+ membershipStatus: "ACTIVE"
},
});
@@ -268,6 +271,7 @@ describe("authenticated routing", () => {
currentLevel: 1,
currentClassroomId: 101,
hasCompletedOnboarding: false,
+ membershipStatus: "ACTIVE",
},
});
diff --git a/src/avatar/model/__tests__/avatar.store.spec.ts b/src/avatar/model/__tests__/avatar.store.spec.ts
index b5d64ddc..8744105a 100644
--- a/src/avatar/model/__tests__/avatar.store.spec.ts
+++ b/src/avatar/model/__tests__/avatar.store.spec.ts
@@ -150,6 +150,7 @@ describe('useAvatarStore', () => {
currentLevel: 1,
currentClassroomId: 2,
hasCompletedOnboarding: true,
+ membershipStatus: "ACTIVE",
},
teacherProfile: null,
}
diff --git a/src/classrooms/pages/ClassroomTasks.vue b/src/classrooms/pages/ClassroomTasks.vue
index 4a84af9b..2f63d617 100644
--- a/src/classrooms/pages/ClassroomTasks.vue
+++ b/src/classrooms/pages/ClassroomTasks.vue
@@ -159,7 +159,9 @@ watch(
() => [slug.value, stops.value],
([newSlug, stopsLoaded]) => {
if (!newSlug || stopsLoaded?.length === 0) return
- loadTasksForSlug(newSlug)
+ if (typeof newSlug === 'string') {
+ loadTasksForSlug(newSlug)
+ }
},
{ immediate: true },
)
@@ -215,7 +217,11 @@ watch(
-