Front Matter
Field | Value |
|---|---|
Title | W1.3 — Manual Skills |
Author | scorevi |
Reviewers | @dyorgie, @teruterubozuuu |
Created | 2026-02-09 |
Status | Approved (In Production) |
Introduction & Goals
Problem Summary
Creators need a way to tag quests and adventures with specific skills for better categorization, searchability, and learning outcome tracking. Additionally, creators need to filter their content library by skills to quickly find relevant quests.
Goals
Store skills as a
TEXT[]column on quests and adventures tablesProvide inline editing for skills on the quest overview page
Validate skills with max count (20) and max length (50 chars)
Implement case-insensitive deduplication to prevent duplicates like "React" and "react"
Enable skills-based filtering in Content Library and My Content widget
Display skills as visual badges on content cards
Non-Goals
Skill hierarchy/taxonomy (predefined skill tree)
Skill certification or validation
AI-suggested skills (separate feature W1.2)
Global skill database with autocomplete (skills are free-form text)
Glossary
Term | Definition |
|---|---|
Skill | A free-form text label categorizing learning outcomes (e.g., "Python", "Data Analysis") |
Skills Filter | UI component allowing multi-select filtering of content by skills |
Deduplication | Case-insensitive removal of duplicate skills (e.g., "React" + "react" → "React") |
High-Level Architecture
System Diagram

Skills Filter Flow

Technologies Used
Category | Technology |
|---|---|
Database | Supabase PostgreSQL (TEXT[] column, GIN index) |
Validation | Zod (schema-level deduplication + constraints) |
Frontend | React hooks ( |
UI Components | Shadcn/UI (Popover, Checkbox, Badge, Input) |
Detailed Design & Implementation
Data Model / Schema
Database Schema
-- quests tableALTER TABLE quests ADD COLUMN IF NOT EXISTS skills TEXT[] DEFAULT '{}'; CREATE INDEX IF NOT EXISTS idx_quests_skills ON quests USING GIN (skills); -- adventures tableALTER TABLE adventures ADD COLUMN IF NOT EXISTS skills TEXT[] DEFAULT '{}'; CREATE INDEX IF NOT EXISTS idx_adventures_skills ON adventures USING GIN (skills);
Zod Validation Schema
// shared/schemas/questSchema.ts // createQuestSchema.skillsskills: z .array( z.string() .max(50, "Each skill must be 50 characters or less") .min(1, "Skill name cannot be empty") ) .transform((skills) => { // Case-insensitive deduplication const seen = new Set<string>(); return skills.filter((skill) => { const normalized = skill.toLowerCase(); if (seen.has(normalized)) return false; seen.add(normalized); return true; }); }) .refine((skills) => skills.length <= 20, { message: "Maximum 20 skills allowed per quest" }) .default([])
Constraint | Value | Error Message |
|---|---|---|
Max skills | 20 | "Maximum 20 skills allowed per quest" |
Max length per skill | 50 chars | "Each skill must be 50 characters or less" |
Min length per skill | 1 char | "Skill name cannot be empty" |
Deduplication | Case-insensitive | Warning shown to user |
API Specification
Skills are updated via the existing quest update endpoint:
Endpoint | Method | Purpose |
|---|---|---|
| PATCH | Update quest including skills array |
Request Body:
{ "quest_id": "uuid", "skills": ["Python", "Data Analysis", "Machine Learning"]}
Logic & Workflows
Frontend Validation (validateSkills)
// app/quest-editor/[questID]/(sections)/overview/page.tsx const validateSkills = (skillsString: string) => { const rawSkills = skillsString.split(',').map(s => s.trim()).filter(s => s.length > 0); const { deduplicated: skills, removedDuplicates } = deduplicateCaseInsensitive(rawSkills); const errors: string[] = []; const warnings: string[] = []; // Warn about duplicates (not blocking) if (removedDuplicates.length > 0) { warnings.push(`Duplicate skills removed: ${removedDuplicates.join(', ')}`); } // Validation rules if (skills.length > 20) errors.push("Maximum 20 skills allowed"); if (skills.some(s => s.length > 50)) errors.push("Each skill must be 50 characters or less"); if (skills.some(s => !/^[a-zA-Z0-9\s-]+$/.test(s))) { errors.push("Skills can only contain letters, numbers, spaces, and hyphens"); } return { valid: errors.length === 0, errors, warnings, skills };};
Skills Filter Hook (useSkillsFilter)
// hooks/creator/useSkillsFilter.ts export function useSkillsFilter(items: ContentListObject[]) { const [selectedSkills, setSelectedSkills] = useState<string[]>([]); // Extract unique skills sorted alphabetically const availableSkills = useMemo(() => { const skillsSet = new Set<string>(); items.forEach((item) => { item.skills?.forEach((skill) => skillsSet.add(skill.trim())); }); return Array.from(skillsSet).sort((a, b) => a.localeCompare(b)); }, [items]); // Filter items by selected skills (OR logic) const filterBySkills = useCallback((itemsToFilter) => { if (selectedSkills.length === 0) return itemsToFilter; return itemsToFilter.filter((item) => selectedSkills.some((skill) => item.skills?.includes(skill)) ); }, [selectedSkills]); return { availableSkills, selectedSkills, filterBySkills, toggleSkill, removeSkill, clearAllSkills };}
Key Files
File | Purpose |
|---|---|
| Shared hook for skills filtering |
| Content Library with skills filter |
| My Content widget with skills filter |
| Skills inline editing |
| Zod validation schemas |
| Database migration |
Infrastructure & Operations
Dependencies
Dependency | Purpose |
|---|---|
Supabase | PostgreSQL database with TEXT[] support |
Zod | Schema validation and transformation |
Shadcn/UI | Popover, Checkbox, Badge, Input components |
Monitoring & Alerting
No specific monitoring required. Skills are low-traffic metadata with no external API calls.
Deployment Plan
Pre-deployment: Run migration
20260209100000_add_skills_and_archived_status.sqlDeploy: Frontend components (no feature flags needed)
Verify: GIN indexes created for efficient filtering
Testing & Quality Assurance
Test Strategy
Test Type | Coverage |
|---|---|
Manual | Add/remove/edit skills, duplicate detection, filter functionality |
Unit |
|
Integration | Quest update API with skills array |
Manual QA Checklist
Add skills via comma-separated input
Verify duplicates are auto-removed (case-insensitive)
Warning displays for removed duplicates (amber text)
Error displays for validation failures (red text)
Skills filter opens in Content Library
Filter shows all unique skills from content
Selecting skills filters content (OR logic)
Clear all removes all filter selections
Badge X icons have aria-label for accessibility
Known Limitations
No skill taxonomy — Skills are free-form text, no predefined hierarchy
No autocomplete — No global skill database for suggestions
No per-user private skills — All skills are visible in filter across all content
Maintenance & Support
Troubleshooting
Symptom | Cause | Resolution |
|---|---|---|
Skills not saving | Validation error | Check for >20 skills or >50 char skill |
Duplicates not detected | Case mismatch | Fixed with case-insensitive dedup |
Filter shows no skills | No skills in content | Add skills to quests first |
Slow filtering | Missing GIN index | Run migration to create index |
Changelog
Version | Status | Description | Date |
|---|---|---|---|
1.0 | Approved | Initial skills column and editing | 2026-02-09 |
1.1 | Approved | Skills filter in Content Library | 2026-06-02 |
1.2 | Approved | Case-insensitive deduplication | 2026-06-02 |
1.3 | Approved | Shared | 2026-06-02 |
Document Version
1.3 - Approved, PR #560 merged with skills filter, deduplication, and shared hook, 2026-06-02