feat: update analytics components and add automation translations

This commit is contained in:
Rotimi Best
2026-03-13 09:53:05 +01:00
parent c532b2e33f
commit fe75771d25
22 changed files with 1096 additions and 202 deletions
+6
View File
@@ -13,6 +13,12 @@
"svelteAllowShorthand": true,
"svelteIndentScriptAndStyle": true
}
},
{
"files": "apps/website/src/blog/**/*.md",
"options": {
"embeddedLanguageFormatting": "off"
}
}
]
}
@@ -15,7 +15,19 @@
let { courseAnalytics = null }: Props = $props();
const chartConfig: Chart.ChartConfig = {};
const progressChartConfig = $derived({
students: {
label: $t('analytics.number_of_students'),
color: 'var(--chart-1)'
}
} satisfies Chart.ChartConfig);
const gradeChartConfig = $derived({
students: {
label: $t('analytics.number_of_students'),
color: 'var(--chart-2)'
}
} satisfies Chart.ChartConfig);
let hasStudentData = $derived(Boolean(courseAnalytics?.students?.length));
@@ -89,20 +101,28 @@
}
});
let progressChartColors = $derived({
domain: progressChartData.map((item) => item.group),
range: progressChartData.map((item) => item.color)
});
let progressSeries = $derived([
{
key: 'students',
value: 'value',
label: progressChartConfig.students.label,
color: 'var(--color-students)'
}
]);
let gradeChartColors = $derived({
domain: gradeChartData.map((item) => item.group),
range: gradeChartData.map((item) => item.color)
});
let gradeSeries = $derived([
{
key: 'students',
value: 'value',
label: gradeChartConfig.students.label,
color: 'var(--color-students)'
}
]);
</script>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
<!-- Progress Distribution Chart -->
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
<div class="">
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">
{$t('analytics.student_progress_distribution')}
</h3>
@@ -111,15 +131,13 @@
<div class="animate-pulse text-gray-500 dark:text-gray-400">{$t('analytics.loading_chart')}</div>
</div>
{:else if hasStudentData}
<Chart.ChartContainer class="h-[300px] w-full" config={chartConfig}>
<Chart.ChartContainer class="h-[300px] w-full" config={progressChartConfig}>
<Chart.BarChart
data={progressChartData}
x="group"
y="value"
c="group"
cDomain={progressChartColors.domain}
cRange={progressChartColors.range}
axis="x"
props={progressChartProps}
series={progressSeries}
/>
</Chart.ChartContainer>
{:else}
@@ -130,23 +148,15 @@
</div>
<!-- Grade Distribution Chart -->
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
<div class="">
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{$t('analytics.grade_distribution')}</h3>
{#if !courseAnalytics}
<div class="flex h-[300px] items-center justify-center">
<div class="animate-pulse text-gray-500 dark:text-gray-400">{$t('analytics.loading_chart')}</div>
</div>
{:else if hasStudentData}
<Chart.ChartContainer class="h-[300px] w-full" config={chartConfig}>
<Chart.BarChart
data={gradeChartData}
x="group"
y="value"
c="group"
cDomain={gradeChartColors.domain}
cRange={gradeChartColors.range}
props={gradeChartProps}
/>
<Chart.ChartContainer class="h-[300px] w-full" config={gradeChartConfig}>
<Chart.BarChart data={gradeChartData} x="group" axis="x" props={gradeChartProps} series={gradeSeries} />
</Chart.ChartContainer>
{:else}
<div class="flex h-[300px] items-center justify-center text-gray-500 dark:text-gray-400">
@@ -1,8 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import UserIcon from '@lucide/svelte/icons/user';
import * as Pagination from '@cio/ui/base/pagination';
import * as Table from '@cio/ui/base/table';
import { courseApi } from '$features/course/api';
import { t } from '$lib/utils/functions/translations';
@@ -33,7 +36,9 @@
const GotoFullProfile = (student: CourseAnalytics['students'][number]) => {
if (!student) return;
goto(`/courses/${courseApi.course?.id}/people/${student.id}?back=/courses/${courseApi.course?.id}/analytics`);
goto(
resolve(`/courses/${courseApi.course?.id}/people/${student.id}?back=/courses/${courseApi.course?.id}/analytics`)
);
};
// Reset to first page when students array changes
@@ -52,116 +57,86 @@
/>
{:else}
<!-- Responsive Table -->
<div class="relative h-[300px] overflow-hidden">
<div class="h-full overflow-auto">
<table class="w-full min-w-[800px]">
<!-- Fixed Header -->
<thead class="sticky top-0 z-10 bg-white dark:bg-neutral-800">
<tr class="border-b border-gray-200">
<th
class="sticky top-0 left-0 z-20 min-w-[200px] bg-white px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:bg-neutral-800 dark:text-gray-300"
<div class="relative h-[300px] overflow-auto rounded-md border">
<Table.Root class="min-w-[800px]">
<Table.Header class="sticky top-0 z-10 bg-white dark:bg-neutral-800">
<Table.Row>
<Table.Head class="sticky top-0 left-0 z-20 min-w-[200px] bg-white px-4 py-3 dark:bg-neutral-800">
{$t('analytics.student_name')}
</Table.Head>
<Table.Head class="min-w-[150px] px-4 py-3">{$t('analytics.lessons_completed')}</Table.Head>
<Table.Head class="min-w-[140px] px-4 py-3">{$t('analytics.exercises_submitted')}</Table.Head>
<Table.Head class="min-w-[120px] px-4 py-3">{$t('analytics.average_grade')}</Table.Head>
<Table.Head class="min-w-[120px] px-4 py-3">{$t('analytics.last_seen')}</Table.Head>
<Table.Head class="min-w-[120px] px-4 py-3">{$t('analytics.actions')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each paginatedStudents as student (student.id)}
<Table.Row class="group h-[100px]">
<Table.Cell
class="sticky left-0 z-10 min-w-[200px] bg-white px-4 py-3 transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
>
{$t('analytics.student_name')}
</th>
<th
class="min-w-[150px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
>
{$t('analytics.lessons_completed')}
</th>
<th
class="min-w-[140px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
>
{$t('analytics.exercises_submitted')}
</th>
<th
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
>
{$t('analytics.average_grade')}
</th>
<th
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
>
{$t('analytics.last_seen')}
</th>
<th
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
>
{$t('analytics.actions')}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-600">
{#each paginatedStudents as student}
<tr class="group h-[100px] border-b hover:bg-gray-50 dark:hover:bg-gray-700">
<!-- Fixed Name Column -->
<td
class="sticky left-0 z-10 min-w-[200px] bg-white px-4 py-3 whitespace-nowrap transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
>
<div class="flex items-center gap-3">
<Avatar.Root class="size-8">
<Avatar.Image
src={student.profile.avatar_url ? student.profile.avatar_url : '/logo-192.png'}
alt={student.profile.fullname ? student.profile.fullname : $t('analytics.student')}
/>
<Avatar.Fallback>{shortenName(student.profile.fullname) || 'S'}</Avatar.Fallback>
</Avatar.Root>
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-gray-900 dark:text-white">
{student.profile.fullname}
</p>
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
{student.profile.email}
</p>
</div>
<div class="flex items-center gap-3">
<Avatar.Root class="size-8">
<Avatar.Image
src={student.profile.avatar_url ? student.profile.avatar_url : '/logo-192.png'}
alt={student.profile.fullname ? student.profile.fullname : $t('analytics.student')}
/>
<Avatar.Fallback>{shortenName(student.profile.fullname) || 'S'}</Avatar.Fallback>
</Avatar.Root>
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-gray-900 dark:text-white">
{student.profile.fullname}
</p>
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
{student.profile.email}
</p>
</div>
</td>
<!-- Scrollable Content -->
<td class="min-w-[150px] px-4 py-3 whitespace-nowrap">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{student.lessonsCompleted}/{student.totalLessons}
</span>
<div class="w-20 flex-shrink-0">
<div class="h-2 rounded-full bg-gray-200 dark:bg-neutral-700">
<div class="h-2 rounded-full bg-blue-500" style="width: {student.progressPercentage}%"></div>
</div>
</div>
</div>
</td>
<td class="min-w-[140px] px-4 py-3 whitespace-nowrap">
</div>
</Table.Cell>
<Table.Cell class="min-w-[150px] px-4 py-3">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{student.exercisesSubmitted} out of {student.totalExercises}
{student.lessonsCompleted}/{student.totalLessons}
</span>
</td>
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
<Badge
class={student.averageGrade >= 80
? 'bg-green-600'
: student.averageGrade >= 60
? 'bg-blue-600'
: 'bg-red-600'}
>
{student.averageGrade}%
</Badge>
</td>
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
<span class="text-sm text-gray-500 dark:text-gray-400">
{student.lastSeen}
</span>
</td>
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
<button
class="rounded bg-blue-600 px-3 py-1.5 text-sm font-medium whitespace-nowrap text-white transition-colors hover:bg-blue-700"
onclick={() => GotoFullProfile(student)}
>
{$t('analytics.view_details')}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="w-20 flex-shrink-0">
<div class="h-2 rounded-full bg-gray-200 dark:bg-neutral-700">
<div class="h-2 rounded-full bg-blue-500" style="width: {student.progressPercentage}%"></div>
</div>
</div>
</div>
</Table.Cell>
<Table.Cell class="min-w-[140px] px-4 py-3">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{student.exercisesSubmitted} out of {student.totalExercises}
</span>
</Table.Cell>
<Table.Cell class="min-w-[120px] px-4 py-3">
<Badge
class={student.averageGrade >= 80
? 'bg-green-600'
: student.averageGrade >= 60
? 'bg-blue-600'
: 'bg-red-600'}
>
{student.averageGrade}%
</Badge>
</Table.Cell>
<Table.Cell class="min-w-[120px] px-4 py-3">
<span class="text-sm text-gray-500 dark:text-gray-400">
{student.lastSeen}
</span>
</Table.Cell>
<Table.Cell class="min-w-[120px] px-4 py-3">
<Button variant="outline" size="sm" onclick={() => GotoFullProfile(student)}>
{$t('analytics.view_details')}
</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<!-- Pagination Controls -->
@@ -225,7 +225,7 @@
</div>
{/if}
<div class="questions px-6 pt-6">
{#each $questionnaire.questions as question, index}
{#each $questionnaire.questions as question, index (question.id)}
{#if !question.deletedAt}
<QuestionContainer
key={String(question.id ?? `new-${index}`)}
@@ -272,7 +272,7 @@
{getQuestionTypeLabel(question?.questionType)}
</Select.Trigger>
<Select.Content>
{#each QUESTION_TYPES as type}
{#each QUESTION_TYPES as type, i (type.key)}
{#if $isFreePlan && PREMIUM_QUESTION_TYPE_KEYS.has(type.key)}
<button
type="button"
@@ -19,8 +19,6 @@
return acc;
}, 0);
}
$inspect('$questionnaire', $questionnaire);
</script>
<div class="mb-5 {!preview ? 'px-6' : 'px-2'}">
@@ -78,17 +76,15 @@
{$t('course.navItem.lessons.exercises.all_exercises.description.heading')}
</p>
{#key $questionnaire.description}
<TextEditor
content={$questionnaire.description || ''}
onChange={(html) => {
$questionnaire.isDescriptionDirty = true;
$questionnaire.description = html;
}}
editorClass="max-h-[100px]"
placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')}
/>
{/key}
<TextEditor
content={$questionnaire.description || ''}
onChange={(html) => {
$questionnaire.isDescriptionDirty = true;
$questionnaire.description = html;
}}
editorClass="max-h-[100px]"
placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')}
/>
</div>
{/if}
</QuestionContainer>
@@ -85,15 +85,12 @@
</div>
<!-- Students Overview Table -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-neutral-800">
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{$t('analytics.student_progress_overview')}
</h3>
</div>
<div class="p-6">
<StudentTable students={courseAnalytics?.students || []} />
</div>
<div class="space-y-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{$t('analytics.student_progress_overview')}
</h3>
<StudentTable students={courseAnalytics?.students || []} />
</div>
</div>
</div>
@@ -357,6 +357,11 @@
"deleted": "Tokengodkendelse fjernet.",
"activated": "Tokengodkendelse er aktiveret.",
"deactivated": "Tokengodkendelse deaktiveret."
},
"automation": {
"created": "Automatiseringsnøgle oprettet. Gem den nu, fordi den fulde hemmelighed kun vises én gang.",
"rotated": "Automatiseringsnøglen drejet. Gem den nye hemmelighed nu, fordi den ikke vil blive vist igen.",
"revoked": "Automatiseringsnøgle tilbagekaldt."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Bliv en tidlig adopter",
"unlock": "Lås op for ubegrænsede funktioner og invester i vores fremtid",
"upgrade": "Opgrader nu",
"tags": "Tags"
"tags": "Tags",
"automation": "Automatisering"
},
"media_manager": {
"page_title": "Media Manager - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "N/A"
}
},
"invite": {
"organization": {
"page_title": "Tilmeld dig {orgName} på ClassroomIO",
"role_label": "Rolle: {role}",
"email_label": "Inviteret e-mail: {email}",
"join_button": "Deltag i organisationen",
"messages": {
"email_mismatch": "Du er logget ind med en anden e-mail end denne invitation.",
"expired": "Dette invitationslink er udløbet.",
"revoked": "Dette invitationslink er blevet tilbagekaldt.",
"accepted": "Denne invitation er allerede blevet brugt.",
"invalid": "Dette invitationslink er ikke gyldigt.",
"joined": "Tilsluttet organisation",
"join_failed": "Kunne ikke deltage i organisationen"
}
}
},
"automation": {
"title": "Automatisering",
"subtitle": "Forbind MCP-klienter nu, med Zapier og den offentlige API, der følger på det samme nøglesystem med organisationsomfang.",
"coming_soon": "Kommer snart",
"mcp": {
"architecture": {
"title": "Arkitektur",
"description": "MCP-klienten og ClassroomIO API er separate tillidsgrænser. Den org-omfattede automatiseringsnøgle autentificerer klienten, og API'et forbliver kilden til sandhed for validering og vedholdenhed."
},
"flows": {
"title": "Typiske flows",
"description": "Agenten ejer ræsonnement og struktur. ClassroomIO accepterer kun normaliserede kurser, gemmer kladder og udgiver efter anmodning.",
"prompt_title": "Spørg om udkast",
"prompt_description": "Brugeren beder om et kursus, agenten strukturerer det, og 'create_course_draft' gemmer det i ClassroomIO før ethvert udgivelsestrin.",
"pdf_title": "PDF til udkast",
"pdf_description": "Agenten læser PDF'en eksternt, omstrukturerer den i sektioner og lektioner og sender derefter den normaliserede kladde til ClassroomIO.",
"review_title": "Anmeldelse for at offentliggøre",
"review_description": "Brugeren beder om ændringer, agenten opdaterer kladden, og publicering sker kun efter eksplicit godkendelse."
},
"setup": {
"title": "Klient opsætning",
"description": "Brug én nøgle med organisationsomfang pr. integration. Hvis en nøgle roteres eller tilbagekaldes, holder klienten op med at arbejde, indtil den nye hemmelighed er konfigureret.",
"claude_code_description": "Registrerer `@cio/mcp` med Claude Code ved hjælp af den genererede org-omfangede nøgle.",
"codex_description": "Tilføjer ClassroomIO MCP-serveren til Codex med den samme org-omfattede nøgle.",
"cursor_description": "Indsæt dette i `~/.cursor/mcp.json` eller dit arbejdsområde MCP-konfiguration."
},
"keys": {
"title": "MCP nøgler",
"description": "Disse nøgler autentificerer den aktuelle organisation, når en MCP-klient ringer til ClassroomIO gennem `@cio/mcp`.",
"generate": "Generer MCP-nøgle",
"scope_badge": "Udkast opret/læs/opdater/udgiv",
"secret_once": "Denne hemmelighed vises én gang. Kopier det til din MCP-klient nu."
}
},
"keys": {
"active": "Aktiv",
"revoked": "Tilbagekaldt",
"rotate": "Roter",
"revoke": "Tilbagekald",
"empty_title": "Ingen MCP-nøgler endnu",
"empty_description": "Opret den første org-omfattede MCP-nøgle til at forbinde Claude Code, Codex eller Cursor.",
"created_at_label": "Oprettet:",
"last_used_label": "Sidst brugt:",
"never_used": "Aldrig",
"scopes_label": "Omfang:",
"revoke_confirm": "Vil du tilbagekalde denne automatiseringsnøgle?",
"rotate_confirm": "Drej denne automatiseringsnøgle?"
},
"clients": {
"claude_code": "Claude kode",
"codex": "Codex",
"cursor": "Markør"
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Skrivebeskyttet adgang",
"disabled_description": "Kun organisationsadministratorer kan oprette, rotere eller tilbagekalde automatiseringsnøgler. Lærere kan gennemgå opsætningsinstruktionerne her."
},
"overview": {
"title": "Automatiseringsadgang",
"description": "Opret org-omfattede automatiseringsnøgler, tilslut understøttede klienter, og hold al maskinadgang bundet tilbage til denne organisation."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Token-Authentifizierung entfernt.",
"activated": "Token-Authentifizierung aktiviert.",
"deactivated": "Token-Authentifizierung deaktiviert."
},
"automation": {
"created": "Automatisierungsschlüssel erstellt. Speichern Sie es jetzt, da das vollständige Geheimnis nur einmal angezeigt wird.",
"rotated": "Automatisierungsschlüssel gedreht. Speichern Sie das neue Geheimnis jetzt, da es nicht mehr angezeigt wird.",
"revoked": "Automatisierungsschlüssel widerrufen."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Werden Sie ein Early Adopter",
"unlock": "Schalten Sie unbegrenzte Funktionen frei und investieren Sie in unsere Zukunft",
"upgrade": "Jetzt upgraden",
"tags": "Schlagworte"
"tags": "Schlagworte",
"automation": "Automatisierung"
},
"media_manager": {
"page_title": "Medienmanager - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "N/A"
}
},
"invite": {
"organization": {
"page_title": "Treten Sie {orgName} auf ClassroomIO bei",
"role_label": "Rolle: {role}",
"email_label": "Eingeladene E-Mail: {email}",
"join_button": "Treten Sie der Organisation bei",
"messages": {
"email_mismatch": "Sie sind mit einer anderen E-Mail-Adresse als dieser Einladung angemeldet.",
"expired": "Dieser Einladungslink ist abgelaufen.",
"revoked": "Dieser Einladungslink wurde widerrufen.",
"accepted": "Diese Einladung wurde bereits verwendet.",
"invalid": "Dieser Einladungslink ist ungültig.",
"joined": "Der Organisation beigetreten",
"join_failed": "Der Beitritt zur Organisation ist fehlgeschlagen"
}
}
},
"automation": {
"title": "Automatisierung",
"subtitle": "Verbinden Sie jetzt MCP-Clients, wobei Zapier und die öffentliche API auf demselben organisationsweiten Schlüsselsystem folgen.",
"coming_soon": "Kommt bald",
"mcp": {
"architecture": {
"title": "Architektur",
"description": "Der MCP-Client und die ClassroomIO-API sind separate Vertrauensgrenzen. Der organisationsweite Automatisierungsschlüssel authentifiziert den Client und die API bleibt die Quelle der Wahrheit für Validierung und Persistenz."
},
"flows": {
"title": "Typische Strömungen",
"description": "Der Agent besitzt Argumentation und Struktur. ClassroomIO akzeptiert nur normalisierte Kursnutzlasten, speichert Entwürfe und veröffentlicht auf Anfrage.",
"prompt_title": "Aufforderung zum Entwurf",
"prompt_description": "Der Benutzer fragt nach einem Kurs, der Agent strukturiert ihn und „create_course_draft“ speichert ihn dann vor jedem Veröffentlichungsschritt in ClassroomIO.",
"pdf_title": "PDF zum Entwurf",
"pdf_description": "Der Agent liest das PDF extern, strukturiert es in Abschnitte und Lektionen um und sendet dann den normalisierten Entwurf an ClassroomIO.",
"review_title": "Rezension zur Veröffentlichung",
"review_description": "Der Benutzer bittet um Änderungen, der Agent aktualisiert den Entwurf und die Veröffentlichung erfolgt erst nach ausdrücklicher Genehmigung."
},
"setup": {
"title": "Client-Setup",
"description": "Verwenden Sie einen organisationsweiten Schlüssel pro Integration. Wenn ein Schlüssel rotiert oder widerrufen wird, funktioniert der Client nicht mehr, bis das neue Geheimnis konfiguriert ist.",
"claude_code_description": "Registriert „@cio/mcp“ bei Claude Code unter Verwendung des generierten organisationsweit gültigen Schlüssels.",
"codex_description": "Fügt den ClassroomIO MCP-Server mit demselben organisationsweit gültigen Schlüssel zu Codex hinzu.",
"cursor_description": "Fügen Sie dies in „~/.cursor/mcp.json“ oder Ihre Workspace-MCP-Konfiguration ein."
},
"keys": {
"title": "MCP-Schlüssel",
"description": "Diese Schlüssel authentifizieren die aktuelle Organisation, wenn ein MCP-Client ClassroomIO über „@cio/mcp“ aufruft.",
"generate": "MCP-Schlüssel generieren",
"scope_badge": "Entwurf erstellen/lesen/aktualisieren/veröffentlichen",
"secret_once": "Dieses Geheimnis wird einmal gezeigt. Kopieren Sie es jetzt in Ihren MCP-Client."
}
},
"keys": {
"active": "Aktiv",
"revoked": "Widerrufen",
"rotate": "Drehen",
"revoke": "Widerrufen",
"empty_title": "Noch keine MCP-Schlüssel",
"empty_description": "Erstellen Sie den ersten organisationsweit gültigen MCP-Schlüssel, um Claude Code, Codex oder Cursor zu verbinden.",
"created_at_label": "Erstellt:",
"last_used_label": "Zuletzt verwendet:",
"never_used": "Niemals",
"scopes_label": "Geltungsbereiche:",
"revoke_confirm": "Diesen Automatisierungsschlüssel widerrufen?",
"rotate_confirm": "Diesen Automatisierungsschlüssel drehen?"
},
"clients": {
"claude_code": "Claude Code",
"codex": "Kodex",
"cursor": "Cursor"
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Nur lesender Zugriff",
"disabled_description": "Nur Organisationsadministratoren können Automatisierungsschlüssel erstellen, rotieren oder widerrufen. Lehrer können die Einrichtungsanweisungen hier einsehen."
},
"overview": {
"title": "Automatisierungszugriff",
"description": "Erstellen Sie organisationsweite Automatisierungsschlüssel, verbinden Sie unterstützte Clients und halten Sie den gesamten Maschinenzugriff an diese Organisation gebunden."
}
}
}
@@ -643,6 +643,7 @@
"audience": "Audience",
"media": "Media",
"setup": "Setup",
"automation": "Automation",
"help": "Help",
"settings": "Settings",
"early_adopter": "Become an Early Adopter",
@@ -2110,6 +2111,74 @@
"enterprise_required": "This feature requires an Enterprise plan."
},
"automation": {
"title": "Automation",
"subtitle": "Connect MCP clients now, with Zapier and the public API following on the same org-scoped key system.",
"coming_soon": "Coming soon",
"overview": {
"title": "Automation access",
"description": "Create org-scoped automation keys, connect supported clients, and keep all machine access tied back to this organization."
},
"permissions": {
"disabled_title": "Read-only access",
"disabled_description": "Only organization admins can create, rotate, or revoke automation keys. Teachers can review the setup instructions here."
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapier",
"api": "API"
},
"clients": {
"claude_code": "Claude Code",
"codex": "Codex",
"cursor": "Cursor"
},
"keys": {
"active": "Active",
"revoked": "Revoked",
"rotate": "Rotate",
"revoke": "Revoke",
"empty_title": "No MCP keys yet",
"empty_description": "Create the first org-scoped MCP key to connect Claude Code, Codex, or Cursor.",
"created_at_label": "Created:",
"last_used_label": "Last used:",
"never_used": "Never",
"scopes_label": "Scopes:",
"revoke_confirm": "Revoke this automation key?",
"rotate_confirm": "Rotate this automation key?"
},
"mcp": {
"keys": {
"title": "MCP keys",
"description": "These keys authenticate the current organization when an MCP client calls ClassroomIO through `@cio/mcp`.",
"generate": "Generate MCP key",
"scope_badge": "Draft create/read/update/publish",
"secret_once": "This secret is shown once. Copy it into your MCP client now."
},
"setup": {
"title": "Client setup",
"description": "Use one org-scoped key per integration. If a key is rotated or revoked, the client stops working until the new secret is configured.",
"claude_code_description": "Registers `@cio/mcp` with Claude Code using the generated org-scoped key.",
"codex_description": "Adds the ClassroomIO MCP server to Codex with the same org-scoped key.",
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config."
},
"flows": {
"title": "Typical flows",
"description": "The agent owns reasoning and structure. ClassroomIO only accepts normalized course payloads, saves drafts, and publishes on request.",
"prompt_title": "Prompt to draft",
"prompt_description": "The user asks for a course, the agent structures it, then `create_course_draft` saves it into ClassroomIO before any publish step.",
"pdf_title": "PDF to draft",
"pdf_description": "The agent reads the PDF externally, restructures it into sections and lessons, then sends the normalized draft into ClassroomIO.",
"review_title": "Review to publish",
"review_description": "The user asks for changes, the agent updates the draft, and publish only happens after explicit approval."
},
"architecture": {
"title": "Architecture",
"description": "The MCP client and ClassroomIO API are separate trust boundaries. The org-scoped automation key authenticates the client, and the API remains the source of truth for validation and persistence."
}
}
},
"lms_navigation": {
"home": "Home",
"my_learning": "My Learning",
@@ -2151,6 +2220,11 @@
"success_update": "Updated successfully",
"success_delete": "Deleted successfully",
"error_update": "Failed to update",
"automation": {
"created": "Automation key created. Save it now because the full secret is only shown once.",
"rotated": "Automation key rotated. Save the new secret now because it will not be shown again.",
"revoked": "Automation key revoked."
},
"token_auth": {
"created": "Signing secret created. Save it now—it won't be shown again.",
"rotated": "Signing secret rotated. Save the new secret—it won't be shown again.",
@@ -2390,6 +2464,24 @@
"course_tag": "Specialization"
},
"invite": {
"organization": {
"page_title": "Join {orgName} on ClassroomIO",
"role_label": "Role: {role}",
"email_label": "Invited email: {email}",
"join_button": "Join Organization",
"messages": {
"email_mismatch": "You are logged in with a different email from this invite.",
"expired": "This invite link has expired.",
"revoked": "This invite link has been revoked.",
"accepted": "This invite has already been used.",
"invalid": "This invite link is not valid.",
"joined": "Joined organization",
"join_failed": "Failed to join organization"
}
}
},
"content": {
"heading": "Internationalization in SvelteKit",
"toggle_label": "Select Preferred Language"
@@ -357,6 +357,11 @@
"deleted": "Se eliminó la autenticación de token.",
"activated": "Autenticación de token habilitada.",
"deactivated": "Autenticación de token deshabilitada."
},
"automation": {
"created": "Clave de automatización creada. Guárdalo ahora porque el secreto completo sólo se muestra una vez.",
"rotated": "Tecla de automatización girada. Guarde el nuevo secreto ahora porque no se volverá a mostrar.",
"revoked": "Clave de automatización revocada."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Conviértase en uno de los primeros en adoptar",
"unlock": "Desbloquee funciones ilimitadas e invierta en nuestro futuro",
"upgrade": "Actualiza ahora",
"tags": "Etiquetas"
"tags": "Etiquetas",
"automation": "Automatización"
},
"media_manager": {
"page_title": "Administrador de medios - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "N/A"
}
},
"invite": {
"organization": {
"page_title": "Únase a {orgName} en ClassroomIO",
"role_label": "Rol: {rol}",
"email_label": "Correo electrónico invitado: {email}",
"join_button": "Unirse a la organización",
"messages": {
"email_mismatch": "Has iniciado sesión con un correo electrónico diferente al de esta invitación.",
"expired": "Este enlace de invitación ha caducado.",
"revoked": "Este enlace de invitación ha sido revocado.",
"accepted": "Esta invitación ya ha sido utilizada.",
"invalid": "Este enlace de invitación no es válido.",
"joined": "Organización unida",
"join_failed": "No se pudo unir a la organización"
}
}
},
"automation": {
"title": "Automatización",
"subtitle": "Conecte los clientes de MCP ahora, con Zapier y la API pública siguiendo el mismo sistema de claves con ámbito de organización.",
"coming_soon": "Próximamente",
"mcp": {
"architecture": {
"title": "Arquitectura",
"description": "El cliente MCP y la API ClassroomIO son límites de confianza separados. La clave de automatización con ámbito de organización autentica al cliente y la API sigue siendo la fuente de verdad para la validación y la persistencia."
},
"flows": {
"title": "Flujos típicos",
"description": "El agente posee razonamiento y estructura. ClassroomIO solo acepta cargas útiles de cursos normalizados, guarda borradores y publica a pedido.",
"prompt_title": "Solicitar borrador",
"prompt_description": "El usuario solicita un curso, el agente lo estructura y luego `create_course_draft` lo guarda en ClassroomIO antes de cualquier paso de publicación.",
"pdf_title": "PDF para borrador",
"pdf_description": "El agente lee el PDF externamente, lo reestructura en secciones y lecciones y luego envía el borrador normalizado a ClassroomIO.",
"review_title": "Revisar para publicar",
"review_description": "El usuario solicita cambios, el agente actualiza el borrador y la publicación solo se realiza después de una aprobación explícita."
},
"setup": {
"title": "Configuración del cliente",
"description": "Utilice una clave con ámbito de organización por integración. Si se rota o revoca una clave, el cliente deja de funcionar hasta que se configura el nuevo secreto.",
"claude_code_description": "Registra `@cio/mcp` con Claude Code utilizando la clave de alcance organizacional generada.",
"codex_description": "Agrega el servidor ClassroomIO MCP al Codex con la misma clave de ámbito de organización.",
"cursor_description": "Pegue esto en `~/.cursor/mcp.json` o en la configuración MCP de su espacio de trabajo."
},
"keys": {
"title": "Claves MCP",
"description": "Estas claves autentican la organización actual cuando un cliente MCP llama a ClassroomIO a través de `@cio/mcp`.",
"generate": "Generar clave MCP",
"scope_badge": "Borrador de creación/lectura/actualización/publicación",
"secret_once": "Este secreto se muestra una vez. Cópielo en su cliente MCP ahora."
}
},
"keys": {
"active": "Activo",
"revoked": "Revocado",
"rotate": "rotar",
"revoke": "Revocar",
"empty_title": "Aún no hay claves MCP",
"empty_description": "Cree la primera clave MCP con ámbito de organización para conectar Claude Code, Codex o Cursor.",
"created_at_label": "Creado:",
"last_used_label": "Último usado:",
"never_used": "nunca",
"scopes_label": "Alcances:",
"revoke_confirm": "¿Revocar esta clave de automatización?",
"rotate_confirm": "¿Girar esta clave de automatización?"
},
"clients": {
"claude_code": "Código Claude",
"codex": "Códice",
"cursor": "Cursor"
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Acceso de solo lectura",
"disabled_description": "Solo los administradores de la organización pueden crear, rotar o revocar claves de automatización. Los profesores pueden revisar las instrucciones de configuración aquí."
},
"overview": {
"title": "Acceso a la automatización",
"description": "Cree claves de automatización específicas de la organización, conecte clientes compatibles y mantenga todo el acceso a las máquinas vinculado a esta organización."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Authentification du jeton supprimée.",
"activated": "Authentification par jeton activée.",
"deactivated": "Authentification du jeton désactivée."
},
"automation": {
"created": "Clé d'automatisation créée. Enregistrez-le maintenant car le secret complet n'est affiché qu'une seule fois.",
"rotated": "Clé d'automatisation tournée. Enregistrez le nouveau secret maintenant car il ne sera plus affiché.",
"revoked": "Clé d'automatisation révoquée."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Devenez un adopteur précoce",
"unlock": "Débloquez des fonctionnalités illimitées et investissez dans notre avenir",
"upgrade": "Mettre à niveau maintenant",
"tags": "Balises"
"tags": "Balises",
"automation": "Automation"
},
"media_manager": {
"page_title": "Gestionnaire de médias - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "N/D"
}
},
"invite": {
"organization": {
"page_title": "Rejoignez {orgName} sur ClassroomIO",
"role_label": "Rôle : {rôle}",
"email_label": "E-mail invité : {email}",
"join_button": "Rejoindre l'organisation",
"messages": {
"email_mismatch": "Vous êtes connecté avec un e-mail différent de celui de cette invitation.",
"expired": "Ce lien d'invitation a expiré.",
"revoked": "Ce lien d'invitation a été révoqué.",
"accepted": "Cette invitation a déjà été utilisée.",
"invalid": "Ce lien d'invitation n'est pas valide.",
"joined": "Organisation rejointe",
"join_failed": "Impossible de rejoindre l'organisation"
}
}
},
"automation": {
"title": "Automatisation",
"subtitle": "Connectez les clients MCP maintenant, avec Zapier et l'API publique qui suivent sur le même système de clés à l'échelle de l'organisation.",
"coming_soon": "Bientôt disponible",
"mcp": {
"architecture": {
"title": "Architecture",
"description": "Le client MCP et l'API ClassroomIO sont des limites de confiance distinctes. La clé d'automatisation à l'échelle de l'organisation authentifie le client et l'API reste la source de vérité pour la validation et la persistance."
},
"flows": {
"title": "Flux typiques",
"description": "L'agent possède le raisonnement et la structure. ClassroomIO n'accepte que les charges utiles de cours normalisées, enregistre les brouillons et publie sur demande.",
"prompt_title": "Invite à rédiger",
"prompt_description": "L'utilisateur demande un cours, l'agent le structure, puis `create_course_draft` l'enregistre dans ClassroomIO avant toute étape de publication.",
"pdf_title": "PDF à rédiger",
"pdf_description": "L'agent lit le PDF en externe, le restructure en sections et leçons, puis envoie le brouillon normalisé dans ClassroomIO.",
"review_title": "Revue à publier",
"review_description": "L'utilisateur demande des modifications, l'agent met à jour le brouillon et la publication n'a lieu qu'après approbation explicite."
},
"setup": {
"title": "Configuration du client",
"description": "Utilisez une clé à l’échelle de l’organisation par intégration. Si une clé est alternée ou révoquée, le client cesse de fonctionner jusqu'à ce que le nouveau secret soit configuré.",
"claude_code_description": "Enregistre `@cio/mcp` auprès de Claude Code à l'aide de la clé générée à l'échelle de l'organisation.",
"codex_description": "Ajoute le serveur ClassroomIO MCP au Codex avec la même clé de portée organisationnelle.",
"cursor_description": "Collez-le dans `~/.cursor/mcp.json` ou dans la configuration MCP de votre espace de travail."
},
"keys": {
"title": "Clés MCP",
"description": "Ces clés authentifient l'organisation actuelle lorsqu'un client MCP appelle ClassroomIO via `@cio/mcp`.",
"generate": "Générer une clé MCP",
"scope_badge": "Brouillon créer/lire/mettre à jour/publier",
"secret_once": "Ce secret est montré une fois. Copiez-le dans votre client MCP maintenant."
}
},
"keys": {
"active": "Actif",
"revoked": "Révoqué",
"rotate": "Rotation",
"revoke": "Révoquer",
"empty_title": "Pas encore de clés MCP",
"empty_description": "Créez la première clé MCP à l'échelle de l'organisation pour connecter Claude Code, Codex ou Cursor.",
"created_at_label": "Créé :",
"last_used_label": "Dernière utilisation :",
"never_used": "Jamais",
"scopes_label": "Portées :",
"revoke_confirm": "Révoquer cette clé d'automatisation ?",
"rotate_confirm": "Faire pivoter cette clé d'automatisation ?"
},
"clients": {
"claude_code": "Claude Code",
"codex": "Codex",
"cursor": "Curseur"
},
"tabs": {
"mcp": "PCM",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Accès en lecture seule",
"disabled_description": "Seuls les administrateurs de l'organisation peuvent créer, alterner ou révoquer des clés d'automatisation. Les enseignants peuvent consulter les instructions de configuration ici."
},
"overview": {
"title": "Accès à l'automatisation",
"description": "Créez des clés d'automatisation à l'échelle de l'organisation, connectez les clients pris en charge et conservez tous les accès aux machines liés à cette organisation."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "टोकन प्रमाणीकरण हटा दिया गया.",
"activated": "टोकन प्रमाणीकरण सक्षम.",
"deactivated": "टोकन प्रमाणीकरण अक्षम किया गया."
},
"automation": {
"created": "स्वचालन कुंजी बनाई गई. इसे अभी सहेजें क्योंकि पूरा रहस्य केवल एक बार दिखाया गया है।",
"rotated": "स्वचालन कुंजी घुमाई गई. नया रहस्य अभी सहेजें क्योंकि यह दोबारा नहीं दिखाया जाएगा।",
"revoked": "स्वचालन कुंजी निरस्त कर दी गई."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "प्रारंभिक दत्तक ग्रहणकर्ता बनें",
"unlock": "असीमित सुविधाएँ अनलॉक करें और हमारे भविष्य में निवेश करें",
"upgrade": "अभी अपग्रेड करें",
"tags": "टैग"
"tags": "टैग",
"automation": "स्वचालन"
},
"media_manager": {
"page_title": "मीडिया मैनेजर - क्लासरूमआईओ",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "एन/ए"
}
},
"invite": {
"organization": {
"page_title": "ClassroomIO पर {orgName} से जुड़ें",
"role_label": "भूमिका: {भूमिका}",
"email_label": "आमंत्रित ईमेल: {ईमेल}",
"join_button": "संगठन से जुड़ें",
"messages": {
"email_mismatch": "आप इस आमंत्रण से भिन्न ईमेल से लॉग इन हैं।",
"expired": "यह आमंत्रण लिंक समाप्त हो गया है.",
"revoked": "यह आमंत्रण लिंक निरस्त कर दिया गया है.",
"accepted": "इस आमंत्रण का उपयोग पहले ही किया जा चुका है.",
"invalid": "यह आमंत्रण लिंक मान्य नहीं है.",
"joined": "संगठन से जुड़े",
"join_failed": "संगठन में शामिल होने में विफल"
}
}
},
"automation": {
"title": "स्वचालन",
"subtitle": "एक ही ऑर्ग-स्कोप्ड कुंजी सिस्टम पर जैपियर और सार्वजनिक एपीआई के साथ अब एमसीपी क्लाइंट को कनेक्ट करें।",
"coming_soon": "जल्द आ रहा है",
"mcp": {
"architecture": {
"title": "वास्तुकला",
"description": "एमसीपी क्लाइंट और क्लासरूमआईओ एपीआई अलग-अलग ट्रस्ट सीमाएँ हैं। ऑर्ग-स्कोप्ड ऑटोमेशन कुंजी क्लाइंट को प्रमाणित करती है, और एपीआई सत्यापन और दृढ़ता के लिए सत्य का स्रोत बना रहता है।"
},
"flows": {
"title": "विशिष्ट प्रवाह",
"description": "एजेंट के पास तर्क और संरचना होती है। क्लासरूमआईओ केवल सामान्यीकृत पाठ्यक्रम पेलोड स्वीकार करता है, ड्राफ्ट सहेजता है, और अनुरोध पर प्रकाशित करता है।",
"prompt_title": "मसौदा तैयार करने के लिए संकेत दें",
"prompt_description": "उपयोगकर्ता एक पाठ्यक्रम के लिए पूछता है, एजेंट इसकी संरचना करता है, फिर `create_course_draft` किसी भी प्रकाशन चरण से पहले इसे ClassroomIO में सहेजता है।",
"pdf_title": "ड्राफ्ट करने के लिए पीडीएफ",
"pdf_description": "एजेंट पीडीएफ को बाहरी रूप से पढ़ता है, इसे अनुभागों और पाठों में पुनर्गठित करता है, फिर सामान्यीकृत ड्राफ्ट को क्लासरूमआईओ में भेजता है।",
"review_title": "प्रकाशित करने हेतु समीक्षा करें",
"review_description": "उपयोगकर्ता परिवर्तन मांगता है, एजेंट ड्राफ्ट को अपडेट करता है, और प्रकाशन केवल स्पष्ट अनुमोदन के बाद ही होता है।"
},
"setup": {
"title": "क्लाइंट सेटअप",
"description": "प्रति एकीकरण एक ऑर्ग-स्कोप्ड कुंजी का उपयोग करें। यदि कोई कुंजी घुमाई जाती है या निरस्त की जाती है, तो क्लाइंट नया रहस्य कॉन्फ़िगर होने तक काम करना बंद कर देता है।",
"claude_code_description": "उत्पन्न ऑर्ग-स्कोप्ड कुंजी का उपयोग करके क्लाउड कोड के साथ `@cio/mcp` को पंजीकृत करता है।",
"codex_description": "समान ऑर्ग-स्कोप्ड कुंजी के साथ क्लासरूमआईओ एमसीपी सर्वर को कोडेक्स में जोड़ता है।",
"cursor_description": "इसे `~/.cursor/mcp.json` या अपने कार्यक्षेत्र MCP कॉन्फ़िगरेशन में पेस्ट करें।"
},
"keys": {
"title": "एमसीपी कुंजी",
"description": "जब कोई MCP क्लाइंट `@cio/mcp` के माध्यम से ClassroomIO को कॉल करता है तो ये कुंजियाँ वर्तमान संगठन को प्रमाणित करती हैं।",
"generate": "MCP कुंजी जनरेट करें",
"scope_badge": "ड्राफ्ट बनाएं/पढ़ें/अद्यतन करें/प्रकाशित करें",
"secret_once": "यह रहस्य एक बार दिखाया गया है। अब इसे अपने MCP क्लाइंट में कॉपी करें।"
}
},
"keys": {
"active": "सक्रिय",
"revoked": "निरस्त किया गया",
"rotate": "घुमाएँ",
"revoke": "निरस्त करें",
"empty_title": "अभी तक कोई MCP कुंजी नहीं है",
"empty_description": "क्लाउड कोड, कोडेक्स, या कर्सर को कनेक्ट करने के लिए पहली ऑर्ग-स्कोप्ड एमसीपी कुंजी बनाएं।",
"created_at_label": "बनाया गया:",
"last_used_label": "अंतिम बार उपयोग किया गया:",
"never_used": "कभी नहीं",
"scopes_label": "दायरा:",
"revoke_confirm": "इस स्वचालन कुंजी को निरस्त करें?",
"rotate_confirm": "इस स्वचालन कुंजी को घुमाएँ?"
},
"clients": {
"claude_code": "क्लाउड कोड",
"codex": "कोडेक्स",
"cursor": "कर्सर"
},
"tabs": {
"mcp": "एम.सी.पी",
"zapier": "जैपियर",
"api": "एपीआई"
},
"permissions": {
"disabled_title": "केवल पढ़ने योग्य पहुंच",
"disabled_description": "केवल संगठन व्यवस्थापक ही स्वचालन कुंजियाँ बना सकते हैं, घुमा सकते हैं या निरस्त कर सकते हैं। शिक्षक यहां सेटअप निर्देशों की समीक्षा कर सकते हैं।"
},
"overview": {
"title": "स्वचालन पहुंच",
"description": "ऑर्ग-स्कोप्ड ऑटोमेशन कुंजियाँ बनाएँ, समर्थित क्लाइंट्स को कनेक्ट करें, और सभी मशीन एक्सेस को इस संगठन से जोड़े रखें।"
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Autoryzacja tokenem została usunięta.",
"activated": "Autoryzacja tokenem włączona.",
"deactivated": "Autoryzacja tokenem wyłączona."
},
"automation": {
"created": "Utworzono klucz automatyzacji. Zapisz go teraz, ponieważ pełny sekret zostanie wyświetlony tylko raz.",
"rotated": "Klucz automatyki obrócony. Zapisz teraz nowy sekret, ponieważ nie będzie on wyświetlany ponownie.",
"revoked": "Klucz automatyzacji unieważniony."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Zostań wczesnym użytkownikiem",
"unlock": "Odblokuj nieograniczone funkcje i inwestuj w naszą przyszłość",
"upgrade": "Uaktualnij teraz",
"tags": "Tagi"
"tags": "Tagi",
"automation": "Automatyzacja"
},
"media_manager": {
"page_title": "Menedżer multimediów - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "Nie dotyczy"
}
},
"invite": {
"organization": {
"page_title": "Dołącz do organizacji {orgName} w ClassroomIO",
"role_label": "Rola: {rola}",
"email_label": "Zaproszony e-mail: {email}",
"join_button": "Dołącz do organizacji",
"messages": {
"email_mismatch": "Jesteś zalogowany przy użyciu innego adresu e-mail niż to zaproszenie.",
"expired": "Ten link z zaproszeniem wygasł.",
"revoked": "Ten link z zaproszeniem został odwołany.",
"accepted": "To zaproszenie zostało już wykorzystane.",
"invalid": "Ten link z zaproszeniem jest nieprawidłowy.",
"joined": "Dołączył do organizacji",
"join_failed": "Nie udało się dołączyć do organizacji"
}
}
},
"automation": {
"title": "Automatyzacja",
"subtitle": "Połącz teraz klientów MCP, korzystając z Zapiera i publicznego interfejsu API w tym samym systemie kluczy o zasięgu organizacji.",
"coming_soon": "Już wkrótce",
"mcp": {
"architecture": {
"title": "Architektura",
"description": "Klient MCP i interfejs API ClassroomIO stanowią oddzielne granice zaufania. Klucz automatyzacji o zasięgu organizacji uwierzytelnia klienta, a interfejs API pozostaje źródłem prawdy w celu sprawdzenia poprawności i trwałości."
},
"flows": {
"title": "Typowe przepływy",
"description": "Agent jest właścicielem rozumowania i struktury. ClassroomIO akceptuje tylko znormalizowane ładunki kursów, zapisuje wersje robocze i publikuje na żądanie.",
"prompt_title": "Monituj o wersję roboczą",
"prompt_description": "Użytkownik prosi o kurs, agent tworzy go, a następnie „create_course_draft” zapisuje go w ClassroomIO przed jakimkolwiek etapem publikacji.",
"pdf_title": "PDF do wersji roboczej",
"pdf_description": "Agent czyta plik PDF zewnętrznie, dzieli go na sekcje i lekcje, a następnie wysyła znormalizowaną wersję roboczą do ClassroomIO.",
"review_title": "Recenzja do opublikowania",
"review_description": "Użytkownik prosi o zmiany, agent aktualizuje wersję roboczą, a publikacja następuje dopiero po wyraźnej zgodzie."
},
"setup": {
"title": "Konfiguracja klienta",
"description": "Użyj jednego klucza o zasięgu organizacji na integrację. Jeśli klucz zostanie obrócony lub unieważniony, klient przestaje działać do czasu skonfigurowania nowego klucza tajnego.",
"claude_code_description": "Rejestruje `@cio/mcp` w Claude Code przy użyciu wygenerowanego klucza o zasięgu organizacji.",
"codex_description": "Dodaje serwer ClassroomIO MCP do Codexu z tym samym kluczem o zasięgu organizacji.",
"cursor_description": "Wklej to do `~/.cursor/mcp.json` lub konfiguracji MCP w obszarze roboczym."
},
"keys": {
"title": "Klucze MCP",
"description": "Te klucze uwierzytelniają bieżącą organizację, gdy klient MCP wywołuje ClassroomIO poprzez `@cio/mcp`.",
"generate": "Wygeneruj klucz MCP",
"scope_badge": "Utwórz/odczytaj/aktualizuj/publikuj wersję roboczą",
"secret_once": "Sekret ten zostaje pokazany raz. Skopiuj go teraz do swojego klienta MCP."
}
},
"keys": {
"active": "Aktywny",
"revoked": "Unieważnione",
"rotate": "Obróć",
"revoke": "Odwołaj",
"empty_title": "Nie ma jeszcze kluczy MCP",
"empty_description": "Utwórz pierwszy klucz MCP o zasięgu organizacji, aby połączyć Claude Code, Codex lub Cursor.",
"created_at_label": "Utworzono:",
"last_used_label": "Ostatnio używane:",
"never_used": "Nigdy",
"scopes_label": "Zakresy:",
"revoke_confirm": "Unieważnić ten klucz automatyzacji?",
"rotate_confirm": "Obrócić ten klucz automatyzacji?"
},
"clients": {
"claude_code": "Claude'a Koda",
"codex": "Kodeks",
"cursor": "Kursor"
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapiera",
"api": "API"
},
"permissions": {
"disabled_title": "Dostęp tylko do odczytu",
"disabled_description": "Tylko administratorzy organizacji mogą tworzyć, obracać i unieważniać klucze automatyzacji. Nauczyciele mogą zapoznać się z instrukcjami konfiguracji tutaj."
},
"overview": {
"title": "Dostęp do automatyki",
"description": "Twórz klucze automatyzacji na poziomie organizacji, łącz obsługiwanych klientów i powiązuj cały dostęp do maszyn z tą organizacją."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Autenticação de token removida.",
"activated": "Autenticação de token habilitada.",
"deactivated": "Autenticação de token desativada."
},
"automation": {
"created": "Chave de automação criada. Salve agora porque o segredo completo só é mostrado uma vez.",
"rotated": "Chave de automação girada. Salve o novo segredo agora porque ele não será mostrado novamente.",
"revoked": "Chave de automação revogada."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Torne-se um dos primeiros a adotar",
"unlock": "Desbloqueie recursos ilimitados e invista em nosso futuro",
"upgrade": "Atualize agora",
"tags": "Etiquetas"
"tags": "Etiquetas",
"automation": "Automação"
},
"media_manager": {
"page_title": "Gerenciador de mídia - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "N/A"
}
},
"invite": {
"organization": {
"page_title": "Junte-se a {orgName} no ClassroomIO",
"role_label": "Função: {função}",
"email_label": "E-mail convidado: {email}",
"join_button": "Junte-se à organização",
"messages": {
"email_mismatch": "Você está conectado com um e-mail diferente deste convite.",
"expired": "Este link de convite expirou.",
"revoked": "Este link de convite foi revogado.",
"accepted": "Este convite já foi usado.",
"invalid": "Este link de convite não é válido.",
"joined": "Organização aderida",
"join_failed": "Falha ao ingressar na organização"
}
}
},
"automation": {
"title": "Automação",
"subtitle": "Conecte clientes MCP agora, com o Zapier e a API pública seguindo o mesmo sistema de chaves com escopo organizacional.",
"coming_soon": "Em breve",
"mcp": {
"architecture": {
"title": "Arquitetura",
"description": "O cliente MCP e a API ClassroomIO são limites de confiança separados. A chave de automação no escopo organizacional autentica o cliente, e a API continua sendo a fonte da verdade para validação e persistência."
},
"flows": {
"title": "Fluxos típicos",
"description": "O agente possui raciocínio e estrutura. ClassroomIO aceita apenas cargas de curso normalizadas, salva rascunhos e publica mediante solicitação.",
"prompt_title": "Solicitar rascunho",
"prompt_description": "O usuário solicita um curso, o agente o estrutura e, em seguida, `create_course_draft` o salva no ClassroomIO antes de qualquer etapa de publicação.",
"pdf_title": "PDF para rascunho",
"pdf_description": "O agente lê o PDF externamente, reestrutura-o em seções e lições e, em seguida, envia o rascunho normalizado para o ClassroomIO.",
"review_title": "Revise para publicar",
"review_description": "O usuário solicita alterações, o agente atualiza o rascunho e a publicação só acontece após aprovação explícita."
},
"setup": {
"title": "Configuração do cliente",
"description": "Use uma chave com escopo organizacional por integração. Se uma chave for rotacionada ou revogada, o cliente para de funcionar até que o novo segredo seja configurado.",
"claude_code_description": "Registra `@cio/mcp` com Claude Code usando a chave gerada com escopo organizacional.",
"codex_description": "Adiciona o servidor ClassroomIO MCP ao Codex com a mesma chave com escopo organizacional.",
"cursor_description": "Cole isso em `~/.cursor/mcp.json` ou na configuração do MCP do seu espaço de trabalho."
},
"keys": {
"title": "Chaves MCP",
"description": "Essas chaves autenticam a organização atual quando um cliente MCP chama ClassroomIO por meio de `@cio/mcp`.",
"generate": "Gerar chave MCP",
"scope_badge": "Rascunho criar/ler/atualizar/publicar",
"secret_once": "Este segredo é mostrado uma vez. Copie-o para o seu cliente MCP agora."
}
},
"keys": {
"active": "Ativo",
"revoked": "Revogado",
"rotate": "Girar",
"revoke": "Revogar",
"empty_title": "Nenhuma chave MCP ainda",
"empty_description": "Crie a primeira chave MCP com escopo organizacional para conectar Claude Code, Codex ou Cursor.",
"created_at_label": "Criado:",
"last_used_label": "Usado pela última vez:",
"never_used": "Nunca",
"scopes_label": "Escopos:",
"revoke_confirm": "Revogar esta chave de automação?",
"rotate_confirm": "Girar esta chave de automação?"
},
"clients": {
"claude_code": "Código Claude",
"codex": "Códice",
"cursor": "Cursor"
},
"tabs": {
"mcp": "PCM",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Acesso somente leitura",
"disabled_description": "Somente administradores da organização podem criar, alternar ou revogar chaves de automação. Os professores podem revisar as instruções de configuração aqui."
},
"overview": {
"title": "Acesso de automação",
"description": "Crie chaves de automação no escopo da organização, conecte clientes compatíveis e mantenha todo o acesso às máquinas vinculado a esta organização."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Авторизация по токену удалена.",
"activated": "Авторизация по токену включена.",
"deactivated": "Авторизация по токену отключена."
},
"automation": {
"created": "Ключ автоматизации создан. Сохраните его сейчас, потому что полный секрет отображается только один раз.",
"rotated": "Ключ автоматики повернут. Сохраните новый секрет сейчас, потому что он больше не будет отображаться.",
"revoked": "Ключ автоматизации отозван."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Станьте ранним пользователем",
"unlock": "Разблокируйте неограниченные возможности и инвестируйте в наше будущее",
"upgrade": "Обновите сейчас",
"tags": "Теги"
"tags": "Теги",
"automation": "Автоматизация"
},
"media_manager": {
"page_title": "Медиа-менеджер — ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "Н/Д"
}
},
"invite": {
"organization": {
"page_title": "Присоединяйтесь к {orgName} в ClassroomIO.",
"role_label": "Роль: {роль}",
"email_label": "Приглашенный адрес электронной почты: {email}",
"join_button": "Присоединиться к организации",
"messages": {
"email_mismatch": "Вы вошли в систему с другим адресом электронной почты, указанным в этом приглашении.",
"expired": "Срок действия этой ссылки для приглашения истек.",
"revoked": "Эта ссылка для приглашения была отозвана.",
"accepted": "Это приглашение уже использовано.",
"invalid": "Эта ссылка для приглашения недействительна.",
"joined": "Присоединившаяся организация",
"join_failed": "Не удалось присоединиться к организации"
}
}
},
"automation": {
"title": "Автоматизация",
"subtitle": "Подключите клиентов MCP прямо сейчас, используя Zapier и общедоступный API в одной и той же системе ключей на уровне организации.",
"coming_soon": "Скоро",
"mcp": {
"architecture": {
"title": "Архитектура",
"description": "Клиент MCP и API ClassroomIO представляют собой отдельные границы доверия. Ключ автоматизации на уровне организации аутентифицирует клиента, а API остается источником достоверных данных для проверки и сохранения."
},
"flows": {
"title": "Типичные потоки",
"description": "Агент владеет рассуждениями и структурой. ClassroomIO принимает только нормализованные полезные нагрузки курса, сохраняет черновики и публикует по запросу.",
"prompt_title": "Подскажите черновик",
"prompt_description": "Пользователь запрашивает курс, агент структурирует его, затем `create_course_draft` сохраняет его в ClassroomIO перед любым шагом публикации.",
"pdf_title": "PDF для черновика",
"pdf_description": "Агент читает PDF-файл извне, реструктурирует его на разделы и уроки, а затем отправляет нормализованный черновик в ClassroomIO.",
"review_title": "Отзыв для публикации",
"review_description": "Пользователь запрашивает изменения, агент обновляет черновик, а публикация происходит только после явного одобрения."
},
"setup": {
"title": "Настройка клиента",
"description": "Используйте один ключ на уровне организации для каждой интеграции. Если ключ заменен или отозван, клиент перестает работать до тех пор, пока не будет настроен новый секрет.",
"claude_code_description": "Регистрирует `@cio/mcp` в Claude Code, используя сгенерированный ключ на уровне организации.",
"codex_description": "Добавляет сервер ClassroomIO MCP в Кодекс с тем же ключом на уровне организации.",
"cursor_description": "Вставьте это в `~/.cursor/mcp.json` или в конфигурацию MCP вашей рабочей области."
},
"keys": {
"title": "Ключи MCP",
"description": "Эти ключи аутентифицируют текущую организацию, когда клиент MCP вызывает ClassroomIO через @cio/mcp.",
"generate": "Создать ключ MCP",
"scope_badge": "Черновик создать/прочитать/обновить/опубликовать",
"secret_once": "Этот секрет раскрывается один раз. Скопируйте его в свой клиент MCP сейчас."
}
},
"keys": {
"active": "Активный",
"revoked": "Отозван",
"rotate": "Поворот",
"revoke": "Отозвать",
"empty_title": "Ключей MCP пока нет",
"empty_description": "Создайте первый ключ MCP на уровне организации для подключения Claude Code, Codex или Cursor.",
"created_at_label": "Создано:",
"last_used_label": "Последнее использование:",
"never_used": "Никогда",
"scopes_label": "Области применения:",
"revoke_confirm": "Отозвать этот ключ автоматизации?",
"rotate_confirm": "Повернуть этот ключ автоматизации?"
},
"clients": {
"claude_code": "Клод Код",
"codex": "Кодекс",
"cursor": "Курсор"
},
"tabs": {
"mcp": "МКП",
"zapier": "Запир",
"api": "API"
},
"permissions": {
"disabled_title": "Доступ только для чтения",
"disabled_description": "Только администраторы организации могут создавать, менять или отзывать ключи автоматизации. Учителя могут ознакомиться с инструкциями по настройке здесь."
},
"overview": {
"title": "Автоматизация доступа",
"description": "Создавайте ключи автоматизации на уровне организации, подключайте поддерживаемых клиентов и сохраняйте привязку всего доступа к машинам к этой организации."
}
}
}
@@ -357,6 +357,11 @@
"deleted": "Đã xóa xác thực mã thông báo.",
"activated": "Đã bật xác thực mã thông báo.",
"deactivated": "Xác thực mã thông báo bị vô hiệu hóa."
},
"automation": {
"created": "Đã tạo khóa tự động hóa. Hãy lưu lại ngay vì bí mật đầy đủ chỉ được hiển thị một lần.",
"rotated": "Phím tự động đã được xoay. Hãy lưu lại bí mật mới ngay bây giờ vì nó sẽ không được hiển thị lại.",
"revoked": "Khóa tự động hóa đã bị thu hồi."
}
},
"explore": {
@@ -1823,7 +1828,8 @@
"early_adopter": "Trở thành người áp dụng sớm",
"unlock": "Mở khóa các tính năng không giới hạn và đầu tư vào tương lai của chúng tôi",
"upgrade": "Nâng cấp ngay",
"tags": "Thẻ"
"tags": "Thẻ",
"automation": "Tự động hóa"
},
"media_manager": {
"page_title": "Quản lý truyền thông - ClassroomIO",
@@ -2445,5 +2451,89 @@
"common": {
"not_available": "không áp dụng"
}
},
"invite": {
"organization": {
"page_title": "Tham gia cùng {orgName} trên ClassroomIO",
"role_label": "Vai trò: {vai trò}",
"email_label": "Email được mời: {email}",
"join_button": "Tham gia tổ chức",
"messages": {
"email_mismatch": "Bạn đã đăng nhập bằng một email khác với lời mời này.",
"expired": "Liên kết mời này đã hết hạn.",
"revoked": "Liên kết mời này đã bị thu hồi.",
"accepted": "Lời mời này đã được sử dụng.",
"invalid": "Liên kết mời này không hợp lệ.",
"joined": "Đã tham gia tổ chức",
"join_failed": "Không thể tham gia tổ chức"
}
}
},
"automation": {
"title": "Tự động hóa",
"subtitle": "Kết nối các máy khách MCP ngay bây giờ, với Zapier và API công khai trên cùng một hệ thống khóa trong phạm vi tổ chức.",
"coming_soon": "Sắp có",
"mcp": {
"architecture": {
"title": "Kiến trúc",
"description": "Ứng dụng khách MCP và API ClassroomIO là các ranh giới tin cậy riêng biệt. Khóa tự động hóa trong phạm vi tổ chức sẽ xác thực ứng dụng khách và API vẫn là nguồn thông tin chính xác để xác thực và duy trì."
},
"flows": {
"title": "Dòng chảy điển hình",
"description": "Tác nhân sở hữu lý luận và cấu trúc. ClassroomIO chỉ chấp nhận tải trọng khóa học được chuẩn hóa, lưu bản nháp và xuất bản theo yêu cầu.",
"prompt_title": "Nhắc soạn thảo",
"prompt_description": "Người dùng yêu cầu một khóa học, nhân viên sẽ cấu trúc khóa học đó, sau đó `create_course_draft` lưu nó vào ClassroomIO trước bất kỳ bước xuất bản nào.",
"pdf_title": "PDF thành bản nháp",
"pdf_description": "Nhân viên đọc bản PDF bên ngoài, cấu trúc lại thành các phần và bài học, sau đó gửi bản nháp đã chuẩn hóa vào ClassroomIO.",
"review_title": "Xem xét để xuất bản",
"review_description": "Người dùng yêu cầu thay đổi, người đại diện cập nhật bản nháp và việc xuất bản chỉ diễn ra sau khi được phê duyệt rõ ràng."
},
"setup": {
"title": "Thiết lập máy khách",
"description": "Sử dụng một khóa trong phạm vi tổ chức cho mỗi lần tích hợp. Nếu một khóa bị xoay hoặc bị thu hồi, máy khách sẽ ngừng hoạt động cho đến khi bí mật mới được định cấu hình.",
"claude_code_description": "Đăng ký `@cio/mcp` với Mã Claude bằng cách sử dụng khóa trong phạm vi tổ chức được tạo.",
"codex_description": "Thêm máy chủ ClassroomIO MCP vào Codex bằng cùng một khóa trong phạm vi tổ chức.",
"cursor_description": "Dán cái này vào `~/.cursor/mcp.json` hoặc cấu hình MCP không gian làm việc của bạn."
},
"keys": {
"title": "Khóa MCP",
"description": "Các khóa này xác thực tổ chức hiện tại khi máy khách MCP gọi ClassroomIO thông qua `@cio/mcp`.",
"generate": "Tạo khóa MCP",
"scope_badge": "Bản nháp tạo/đọc/cập nhật/xuất bản",
"secret_once": "Bí mật này được hiển thị một lần. Sao chép nó vào ứng dụng khách MCP của bạn ngay bây giờ."
}
},
"keys": {
"active": "Đang hoạt động",
"revoked": "Đã thu hồi",
"rotate": "Xoay",
"revoke": "Thu hồi",
"empty_title": "Chưa có khóa MCP",
"empty_description": "Tạo khóa MCP trong phạm vi tổ chức đầu tiên để kết nối Mã Claude, Codex hoặc Con trỏ.",
"created_at_label": "Đã tạo:",
"last_used_label": "Sử dụng lần cuối:",
"never_used": "không bao giờ",
"scopes_label": "Phạm vi:",
"revoke_confirm": "Thu hồi khóa tự động hóa này?",
"rotate_confirm": "Xoay phím tự động hóa này?"
},
"clients": {
"claude_code": "Mã Claude",
"codex": "Codex",
"cursor": "Con trỏ"
},
"tabs": {
"mcp": "MCP",
"zapier": "Zapier",
"api": "API"
},
"permissions": {
"disabled_title": "Quyền truy cập chỉ đọc",
"disabled_description": "Chỉ quản trị viên tổ chức mới có thể tạo, xoay hoặc thu hồi khóa tự động hóa. Thầy cô có thể xem lại hướng dẫn thiết lập tại đây."
},
"overview": {
"title": "Truy cập tự động hóa",
"description": "Tạo khóa tự động hóa trong phạm vi tổ chức, kết nối các máy khách được hỗ trợ và giữ tất cả quyền truy cập của máy được gắn chặt với tổ chức này."
}
}
}
@@ -6,7 +6,7 @@
let { data } = $props();
</script>
<Page.Root class="mx-auto flex w-[90%] px-4 md:max-w-2xl lg:max-w-3xl">
<Page.Root class="mx-auto flex md:max-w-3xl lg:max-w-4xl">
<Page.Header>
<Page.HeaderContent>
<Page.Title>
@@ -37,22 +37,22 @@
function getBlockedInviteMessage(): string {
if (isInviteEmailMismatch) {
return 'You are logged in with a different email from this invite.';
return t.get('invite.organization.messages.email_mismatch');
}
if (inviteStatus === 'EXPIRED') {
return 'This invite link has expired.';
return t.get('invite.organization.messages.expired');
}
if (inviteStatus === 'REVOKED') {
return 'This invite link has been revoked.';
return t.get('invite.organization.messages.revoked');
}
if (inviteStatus === 'ACCEPTED') {
return 'This invite has already been used.';
return t.get('invite.organization.messages.accepted');
}
return 'This invite link is not valid.';
return t.get('invite.organization.messages.invalid');
}
async function handleSubmit() {
@@ -81,14 +81,16 @@
if (!result.success || !result.data) {
const failed = result as { error?: string; message?: string };
snackbar.error(failed.error ?? failed.message ?? 'Failed to join organization');
snackbar.error(failed.error ?? failed.message ?? t.get('invite.organization.messages.join_failed'));
return;
}
goto(resolve(result.data.redirectTo || '/org', {}));
snackbar.success('invite.organization.messages.joined');
window.location.href = result.data.redirectTo || '/org';
} catch (error) {
console.error('Failed to accept organization invite', error);
snackbar.error('Failed to join organization');
snackbar.error('invite.organization.messages.join_failed');
} finally {
loading = false;
}
@@ -103,14 +105,18 @@
</script>
<svelte:head>
<title>Join {data.invite.organization.name} on ClassroomIO</title>
<title>{$t('invite.organization.page_title', { orgName: data.invite.organization.name })}</title>
</svelte:head>
<AuthUI isLogin={false} {handleSubmit} isLoading={loading} showOnlyContent={true} showLogo={true}>
<div class="mt-0 w-full">
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.invite.organization.name}</h3>
<p class="text-center text-sm font-light dark:text-white">Role: {data.invite.invite.roleLabel}</p>
<p class="mt-1 text-center text-sm font-light dark:text-white">Invited email: {data.invite.invite.email}</p>
<p class="text-center text-sm font-light dark:text-white">
{$t('invite.organization.role_label', { role: data.invite.invite.roleLabel })}
</p>
<p class="mt-1 text-center text-sm font-light dark:text-white">
{$t('invite.organization.email_label', { email: data.invite.invite.email })}
</p>
{#if !canJoinOrganization}
<p class="mt-3 text-center text-sm text-red-500">{getBlockedInviteMessage()}</p>
@@ -118,7 +124,9 @@
</div>
<div class="my-4 flex w-full flex-col items-center justify-center gap-3">
<Button type="submit" disabled={!canJoinOrganization || loading} {loading}>Join Organization</Button>
<Button type="submit" disabled={!canJoinOrganization || loading} {loading}>
{$t('invite.organization.join_button')}
</Button>
<p class="ui:text-muted-foreground text-center text-sm">
{$t('login.already_have_account')}
<a class="ui:text-primary hover:underline" href={resolve(`/login?${loginParams}`, {})}> {$t('login.login')}</a>
@@ -144,7 +144,7 @@
<Skeleton class="h-10 w-full rounded-md" />
{/each}
{:else}
{#each enrollments as enrollment (enrollment.id)}
{#each enrollments as enrollment, i (i)}
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Avatar.Root class="h-6 w-6">
+4 -4
View File
@@ -29,8 +29,8 @@ preload="auto"
autoPictureInPicture
class="rounded-md"
src="https://assets.cdn.clsrio.com/blog/organisation-student-analytics.mp4"
> </video>
>
</video>
## Course Student Analytics
@@ -52,8 +52,8 @@ autoPictureInPicture
class="rounded-md"
src="https://assets.cdn.clsrio.com/blog/course-students-analytics.mp4"
controls
> </video>
>
</video>
## What's Next?
@@ -31,46 +31,46 @@
data-chart={chartId}
data-slot="chart"
class={cn(
'ui:flex ui:justify-center ui:overflow-visible ui:text-xs aspect-video',
'ui:flex ui:justify-center ui:overflow-visible ui:text-xs ui:aspect-video',
// Overrides
//
// Stroke around dots/marks when hovering
'[&_.stroke-white]:stroke-transparent',
'ui:[&_.stroke-white]:stroke-transparent',
// override the default stroke color of lines
'[&_.lc-line]:stroke-border/50',
'ui:[&_.lc-line]:stroke-border/50',
// by default, layerchart shows a line intersecting the point when hovering, this hides that
'[&_.lc-highlight-line]:stroke-0',
'ui:[&_.lc-highlight-line]:stroke-0',
// by default, when you hover a point on a stacked series chart, it will drop the opacity
// of the other series, this overrides that
'[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs [&_.lc-text-svg]:overflow-visible',
'ui:[&_.lc-area-path]:opacity-100 ui:[&_.lc-highlight-line]:opacity-100 ui:[&_.lc-highlight-point]:opacity-100 ui:[&_.lc-spline-path]:opacity-100 ui:[&_.lc-text]:text-xs ui:[&_.lc-text-svg]:overflow-visible',
// We don't want the little tick lines between the axis labels and the chart, so we remove
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
// chart.
'[&_.lc-axis-tick]:stroke-0',
'ui:[&_.lc-axis-tick]:stroke-0',
// We don't want to display the rule on the x/y axis, as there is already going to be
// a grid line there and rule ends up overlapping the marks because it is rendered after
// the marks
'[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
'[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border',
'[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border',
'ui:[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 ui:[&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
'ui:[&_.lc-grid-x-radial-line]:stroke-border ui:[&_.lc-grid-x-radial-circle]:stroke-border',
'ui:[&_.lc-grid-y-radial-line]:stroke-border ui:[&_.lc-grid-y-radial-circle]:stroke-border',
// Legend adjustments
'[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
'[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
'[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
'ui:[&_.lc-legend-swatch-button]:items-center ui:[&_.lc-legend-swatch-button]:gap-1.5',
'ui:[&_.lc-legend-swatch-group]:items-center ui:[&_.lc-legend-swatch-group]:gap-4',
'ui:[&_.lc-legend-swatch]:size-2.5 ui:[&_.lc-legend-swatch]:rounded-[2px]',
// Labels
'[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
'ui:[&_.lc-labels-text:not([fill])]:fill-foreground ui:[&_text]:stroke-transparent',
// Tick labels on th x/y axes
'[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
'[&_.lc-tooltip-rects-g]:fill-transparent',
'[&_.lc-layout-svg-g]:fill-transparent',
'[&_.lc-root-container]:w-full',
'ui:[&_.lc-axis-tick-label]:fill-muted-foreground ui:[&_.lc-axis-tick-label]:font-normal',
'ui:[&_.lc-tooltip-rects-g]:fill-transparent',
'ui:[&_.lc-layout-svg-g]:fill-transparent',
'ui:[&_.lc-root-container]:w-full',
className
)}
{...restProps}
File diff suppressed because one or more lines are too long