[1.3] Manual Skills

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

  1. Store skills as a TEXT[] column on quests and adventures tables

  2. Provide inline editing for skills on the quest overview page

  3. Validate skills with max count (20) and max length (50 chars)

  4. Implement case-insensitive deduplication to prevent duplicates like "React" and "react"

  5. Enable skills-based filtering in Content Library and My Content widget

  6. 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

image.png

Skills Filter Flow

image.png

Technologies Used

Category

Technology

Database

Supabase PostgreSQL (TEXT[] column, GIN index)

Validation

Zod (schema-level deduplication + constraints)

Frontend

React hooks (useSkillsFilter, useSkillsFilterMultiple)

UI Components

Shadcn/UI (Popover, Checkbox, Badge, Input)


Detailed Design & Implementation

Data Model / Schema

Database Schema

-- quests table
ALTER TABLE quests
ADD COLUMN IF NOT EXISTS skills TEXT[] DEFAULT '{}';
 
CREATE INDEX IF NOT EXISTS idx_quests_skills ON quests USING GIN (skills);
 
-- adventures table
ALTER 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.skills
skills: 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

/api/creator/update-quest

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

hooks/creator/useSkillsFilter.ts

Shared hook for skills filtering

app/creator/content-library/page.tsx

Content Library with skills filter

components/creator/MyContent.tsx

My Content widget with skills filter

app/quest-editor/[questID]/(sections)/overview/page.tsx

Skills inline editing

shared/schemas/questSchema.ts

Zod validation schemas

lib/supabase/migrations/20260209100000_add_skills_and_archived_status.sql

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

  1. Pre-deployment: Run migration 20260209100000_add_skills_and_archived_status.sql

  2. Deploy: Frontend components (no feature flags needed)

  3. 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

useSkillsFilter hook, validateSkills function

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

  1. No skill taxonomy — Skills are free-form text, no predefined hierarchy

  2. No autocomplete — No global skill database for suggestions

  3. 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 useSkillsFilter hook, a11y fixes

2026-06-02

Document Version

1.3 - Approved, PR #560 merged with skills filter, deduplication, and shared hook, 2026-06-02


Was this article helpful?