* feat(course): add reset student course progress action Give course admins and tutors a destructive reset-progress flow on the student people detail page. The backend clears learner progress for one (courseId, memberId) pair in a single transaction while keeping the groupmember enrolled, and logs an audit summary. The dashboard shows a confirmation dialog with impact counts from analytics before deleting. Co-authored-by: Rotimi Best <[email protected]> * fix(i18n): add localized reset progress strings for all locales Co-authored-by: Rotimi Best <[email protected]> * fix(course): scope reset progress to course data only Stop deleting org-level community questions and answers during course progress reset. Fix course newsfeed deletion order so comments on a student's posts are removed before the posts themselves. Restrict reset to student members, move analytics loading to the person detail layout, and rename impact copy to course newsfeed only. Co-authored-by: Rotimi Best <[email protected]> * fix(course): address Greptile review on reset progress Make progressImpact opt-in via includeProgressImpact query param so analytics requests avoid extra DB work unless the reset dialog needs it. Count only student-authored course newsfeed posts and comments in the impact summary to avoid double-counting replies on the student's posts. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
Validation Schemas
Shared validation schemas for ClassroomIO workspace using Zod with convention-based translations.
Structure
Each entity has its own folder with entity-specific schemas:
validation/
├── course/
│ ├── course.ts # Course validation schemas
│ └── index.ts # Re-exports
├── mail/
│ ├── mail.ts # Email validation schemas
│ └── index.ts # Re-exports
├── constants.ts # Shared constants
├── index.ts # Main entry point
├── README.md # This file
└── USAGE.md # Detailed usage guide
Quick Start
Import Schemas
// Import from main module
import { ZCourseClone, TCourseClone } from '@cio/utils/validation';
// Import from entity module
import { ZEmailData } from '@cio/utils/validation/mail';
Backend Usage (API)
Schemas are used without custom error messages - just validation rules:
import { ZCourseClone } from '@cio/utils/validation';
import { zValidator } from '@hono/zod-validator';
export const cloneRouter = new Hono().post('/', zValidator('json', ZCourseClone), async (c) => {
const data = c.req.valid('json'); // Fully typed!
// ... handle validated data
});
Frontend Usage (Dashboard)
Use the error mapper to get translated messages:
import { ZEmailData } from '@cio/utils/validation/mail';
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
let formData = { to: '', subject: '', content: '' };
let errors: Record<string, string> = {};
function handleSubmit() {
const result = ZEmailData.safeParse(formData);
if (!result.success) {
// Automatically maps to translations using convention
errors = mapZodErrorsToTranslations(result.error);
return;
}
// Use validated data
doSomething(result.data);
}
Translation Convention
We use convention-based translation keys to avoid hardcoding messages in schemas.
How It Works
Schemas are clean with no custom error messages:
// ✅ Clean schema (in @cio/utils)
export const ZEmailData = z.object({
to: z.string().email(), // No message
subject: z.string().min(1), // No message
content: z.string().min(10) // No message
});
Frontend maps errors to translations using this convention:
validations.{fieldName}.{errorCode}
Example Translation Keys
| Field | Validation | Error Code | Translation Key |
|---|---|---|---|
to |
.email() |
invalid_string |
validations.to.invalid_email |
subject |
.min(1) |
too_small |
validations.subject.too_small |
username |
.min(3) |
too_small |
validations.username.too_small |
username |
.max(20) |
too_big |
validations.username.too_big |
Translation File Example
{
"validations": {
"to": {
"invalid_email": "Invalid email address",
"invalid_type": "Email is required"
},
"subject": {
"too_small": "Subject is required"
},
"username": {
"too_small": "Username must be at least 3 characters",
"too_big": "Username cannot exceed 20 characters"
},
"generic": {
"required": "This field is required",
"invalid_email": "Invalid email address",
"too_small_string": "Must be at least {min} characters"
}
}
}
Benefits
✅ Single Source of Truth - Schema defined once, used everywhere
✅ No Duplication - Don't reimplement schemas in frontend
✅ Flexible - Add specific translations or use generic fallbacks
✅ Multilingual - Easy to translate without touching schemas
✅ Maintainable - Change schema once, translations adapt automatically
Common Error Codes
| Zod Error | When It Occurs |
|---|---|
too_small |
String too short, number too small, array too few items |
too_big |
String too long, number too large, array too many items |
invalid_email |
Invalid email format (from .email()) |
invalid_url |
Invalid URL format (from .url()) |
invalid_type |
Wrong type (e.g., undefined when string expected) |
invalid_enum_value |
Value not in allowed options |
Naming Convention
- Schemas: Prefixed with
Z(e.g.,ZCourseClone) - Types: Prefixed with
T(e.g.,TCourseClone) - Translation Keys: Follow pattern
validations.{field}.{errorCode}
Adding New Validations
1. Create Schema in Utils Package
// packages/utils/src/validation/lesson/lesson.ts
import * as z from 'zod';
/**
* Lesson Create Validation Schema
* Used for creating new lessons
*
* Translation keys:
* - validations.title.too_small
* - validations.duration.too_small
*/
export const ZLessonCreate = z.object({
title: z.string().min(1), // Clean - no message
duration: z.number().min(1) // Clean - no message
});
export type TLessonCreate = z.infer<typeof ZLessonCreate>;
// validation/lesson/index.ts
export * from './lesson';
2. Export from Main Module
// packages/utils/src/validation/index.ts
export * from './course';
export * from './mail';
export * from './lesson'; // Add this
3. Use in API
// apps/api/src/routes/lesson/create.ts
import { ZLessonCreate } from '@cio/utils/validation/lesson';
import { zValidator } from '@hono/zod-validator';
export const lessonRouter = new Hono().post('/', zValidator('json', ZLessonCreate), async (c) => {
const data = c.req.valid('json');
// ... create lesson
});
4. Add Translations in Frontend
// apps/dashboard/src/lib/utils/translations/en.json
{
"validations": {
"title": {
"too_small": "Title is required",
"too_big": "Title is too long"
},
"duration": {
"too_small": "Duration must be at least 1 minute"
}
}
}
5. Use in Frontend Form
// apps/dashboard/src/lib/components/LessonForm.svelte
import { ZLessonCreate } from '@cio/utils/validation/lesson';
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
let formData = { title: '', duration: 0 };
let errors = {};
function handleSubmit() {
const result = ZLessonCreate.safeParse(formData);
if (!result.success) {
errors = mapZodErrorsToTranslations(result.error);
// errors.title = "Title is required" (translated)
return;
}
createLesson(result.data);
}
Best Practices
1. Keep Schemas Clean - No Error Messages
Schemas should contain only validation rules, no custom messages:
// ✅ Good - Clean schema
export const ZUserProfile = z.object({
email: z.string().email(),
username: z.string().min(3).max(20)
});
// ❌ Bad - Hardcoded messages
export const ZUserProfile = z.object({
email: z.string().email('Invalid email'), // Don't do this
username: z.string().min(3, 'Too short') // Don't do this
});
// ❌ Bad - Translated messages in schema
export const ZUserProfile = z.object({
email: z.string().email(t.get('validations.email')) // Don't do this
});
Why? Schemas are shared between backend (no translations) and frontend (with translations). Keep them clean and universal.
2. Always Export Types
export const ZCourseClone = z.object({
/* ... */
});
export type TCourseClone = z.infer<typeof ZCourseClone>;
3. Document Schemas with Translation Keys
Help frontend developers know which translation keys to add:
/**
* User Profile Validation Schema
* Used for updating user profile information
*
* Translation keys (convention-based):
* - validations.email.invalid_email
* - validations.username.too_small
* - validations.username.too_big
* - validations.bio.too_big
*/
export const ZUserProfile = z.object({
email: z.string().email(),
username: z.string().min(3).max(20),
bio: z.string().max(500).optional()
});
export type TUserProfile = z.infer<typeof ZUserProfile>;
4. Use Descriptive Field Names
Field names become part of translation keys, so use clear names:
// ✅ Good - Clear field names
z.object({
email: z.string().email(),
fullName: z.string().min(5),
phoneNumber: z.string().min(10)
});
// Translation keys: validations.email.*, validations.fullName.*, validations.phoneNumber.*
// ❌ Bad - Unclear field names
z.object({
e: z.string().email(),
fn: z.string().min(5),
ph: z.string().min(10)
});
// Translation keys: validations.e.*, validations.fn.*, validations.ph.* (unclear!)
5. Group Related Validations
Keep related schemas together in the same entity folder:
// validation/user/user.ts
export const ZUserCreate = z.object({
/* ... */
});
export const ZUserUpdate = z.object({
/* ... */
});
export const ZUserProfile = z.object({
/* ... */
});
6. Don't Duplicate Schemas
Never reimplement schemas in the frontend:
// ❌ Bad - Reimplementing in frontend
// apps/dashboard/src/schemas/user.ts
export const userSchema = z.object({
email: z.string().email(t.get('validations.email.invalid'))
// ... duplicating from @cio/utils
});
// ✅ Good - Import and use error mapper
import { ZUserProfile } from '@cio/utils/validation/user';
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
const result = ZUserProfile.safeParse(data);
if (!result.success) {
errors = mapZodErrorsToTranslations(result.error);
}