From b50e0b91fe4740c0891ee0968e38ae44259f117a Mon Sep 17 00:00:00 2001 From: Chelsea Lee Date: Mon, 16 Feb 2026 06:38:49 -0600 Subject: [PATCH] feat(templates): add category-based organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added 'category' column to routine_templates table - Categorized all 12 templates into: Daily Routines, Getting Things Done, Health & Body, Errands - Added /api/templates/categories endpoint to list unique categories - Updated /api/templates to support filtering by category query param - Redesigned templates page with collapsible accordion sections by category - Categories are sorted in logical order (Daily → Work → Health → Errands) - All categories expanded by default for easy browsing --- api/routes/routine_templates.py | 27 +++- config/schema.sql | 1 + config/seed_templates.sql | 48 +++---- .../src/app/dashboard/templates/page.tsx | 129 +++++++++++++----- 4 files changed, 145 insertions(+), 60 deletions(-) diff --git a/api/routes/routine_templates.py b/api/routes/routine_templates.py index 0f0e803..006c1d7 100644 --- a/api/routes/routine_templates.py +++ b/api/routes/routine_templates.py @@ -33,16 +33,39 @@ def register(app): @app.route("/api/templates", methods=["GET"]) def api_listTemplates(): - """List all available templates.""" + """List all available templates. Optional query param: category""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 - templates = postgres.select("routine_templates", order_by="name") + + # Check for category filter + category = flask.request.args.get('category') + if category: + templates = postgres.select("routine_templates", where={"category": category}, order_by="name") + else: + templates = postgres.select("routine_templates", order_by="category, name") + for template in templates: steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position") template["step_count"] = len(steps) return flask.jsonify(templates), 200 + @app.route("/api/templates/categories", methods=["GET"]) + def api_listTemplateCategories(): + """List all unique template categories.""" + user_uuid = _auth(flask.request) + if not user_uuid: + return flask.jsonify({"error": "unauthorized"}), 401 + + # Get distinct categories + result = postgres.execute(""" + SELECT DISTINCT category FROM routine_templates + WHERE category IS NOT NULL + ORDER BY category + """) + categories = [row["category"] for row in result] + return flask.jsonify(categories), 200 + @app.route("/api/templates", methods=["POST"]) def api_createTemplate(): """Create a new template (admin only in production). Body: {name, description?, icon?}""" diff --git a/config/schema.sql b/config/schema.sql index 0c1940d..59ec320 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -90,6 +90,7 @@ CREATE TABLE IF NOT EXISTS routine_templates ( name VARCHAR(255) NOT NULL, description TEXT, icon VARCHAR(100), + category VARCHAR(50) DEFAULT 'Other', created_by_admin BOOLEAN DEFAULT FALSE ); diff --git a/config/seed_templates.sql b/config/seed_templates.sql index 1c0d714..b076e49 100644 --- a/config/seed_templates.sql +++ b/config/seed_templates.sql @@ -14,11 +14,11 @@ DELETE FROM routine_templates; -- The hardest transition of the day. First step is literally just -- sitting up — two-minute rule. Each step cues the next physically. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0001-0001-0001-000000000001', 'Morning Launch', 'Get from bed to ready. Starts small — just sit up.', - '☀️', true); + '☀️', 'Daily Routines', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0001-0001-0001-000000000001', 'a1b2c3d4-0001-0001-0001-000000000001', @@ -39,11 +39,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- The "where are my keys" routine. Externalizes the checklist -- so working memory doesn't have to hold it. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0002-0001-0001-000000000002', 'Leaving the House', 'Everything you need before you walk out the door.', - '🚪', true); + '🚪', 'Daily Routines', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0002-0001-0001-000000000002', 'a1b2c3d4-0002-0001-0001-000000000002', @@ -65,11 +65,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- "at point of performance"). Work block is shorter than classic -- pomodoro because ADHD sustained attention is shorter. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0003-0001-0001-000000000003', 'Focus Sprint', 'One focused work block. Set up your space first.', - '🎯', true); + '🎯', 'Getting Things Done', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0003-0001-0001-000000000003', 'a1b2c3d4-0003-0001-0001-000000000003', @@ -89,11 +89,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- stimulating, the brain won't stop, and the bed isn't "sleepy" -- enough without a transition. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0004-0001-0001-000000000004', 'Wind Down', 'Transition from awake-brain to sleep-brain.', - '🌙', true); + '🌙', 'Daily Routines', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0004-0001-0001-000000000004', 'a1b2c3d4-0004-0001-0001-000000000004', @@ -114,11 +114,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- Not a full cleaning session. Designed to be completable even on -- a bad executive function day. Each step is one area, one action. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0005-0001-0001-000000000005', 'Quick Tidy', 'A fast sweep through the house. Not deep cleaning — just enough.', - '✨', true); + '✨', 'Getting Things Done', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0005-0001-0001-000000000005', 'a1b2c3d4-0005-0001-0001-000000000005', @@ -137,11 +137,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- For when basic hygiene feels hard. No judgment. Every step is -- the minimum viable version — just enough to feel a little better. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0006-0001-0001-000000000006', 'Body Reset', 'Basic care for your body. Even partial counts.', - '🚿', true); + '🚿', 'Health & Body', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0006-0001-0001-000000000006', 'a1b2c3d4-0006-0001-0001-000000000006', @@ -162,11 +162,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- For when you're paralyzed and can't start anything. Pure -- two-minute-rule: the smallest possible actions to build momentum. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0007-0001-0001-000000000007', 'Unstuck', 'Can''t start anything? Start here. Tiny steps, real momentum.', - '🔓', true); + '🔓', 'Getting Things Done', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0007-0001-0001-000000000007', 'a1b2c3d4-0007-0001-0001-000000000007', @@ -187,11 +187,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- End-of-day prep so tomorrow morning isn't harder than it needs -- to be. Externalizes "things to remember" before sleep. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0008-0001-0001-000000000008', 'Evening Reset', 'Set tomorrow up to be a little easier.', - '🌆', true); + '🌆', 'Daily Routines', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0008-0001-0001-000000000008', 'a1b2c3d4-0008-0001-0001-000000000008', @@ -212,11 +212,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- Not "exercise." Movement. The entry point is putting shoes on, -- not "work out for 30 minutes." Anything beyond standing counts. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0009-0001-0001-000000000009', 'Move Your Body', 'Not a workout plan. Just movement. Any amount counts.', - '🏃', true); + '🏃', 'Health & Body', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0009-0001-0001-000000000009', 'a1b2c3d4-0009-0001-0001-000000000009', @@ -237,11 +237,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- Weekly prep. Slightly longer, but broken into small concrete -- chunks. Prevents the "where did the week go" spiral. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0010-0001-0001-000000000010', 'Sunday Reset', 'Set up the week ahead so Monday doesn''t ambush you.', - '📋', true); + '📋', 'Getting Things Done', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0010-0001-0001-000000000010', 'a1b2c3d4-0010-0001-0001-000000000010', @@ -264,11 +264,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- Not "meal prep for the week." One meal. Broken down so the -- activation energy is low and the sequence is externalized. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0011-0001-0001-000000000011', 'Cook a Meal', 'One meal, start to finish. Just follow the steps.', - '🍳', true); + '🍳', 'Health & Body', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0011-0001-0001-000000000011', 'a1b2c3d4-0011-0001-0001-000000000011', @@ -291,11 +291,11 @@ INSERT INTO routine_template_steps (id, template_id, name, instructions, step_ty -- Externalizes the "I have errands but I keep not doing them" -- problem. Forces the plan into concrete sequential steps. -INSERT INTO routine_templates (id, name, description, icon, created_by_admin) VALUES +INSERT INTO routine_templates (id, name, description, icon, category, created_by_admin) VALUES ('a1b2c3d4-0012-0001-0001-000000000012', 'Errand Run', 'Get out, do the things, come home. One trip.', - '🛒', true); + '🛒', 'Errands', true); INSERT INTO routine_template_steps (id, template_id, name, instructions, step_type, duration_minutes, position) VALUES ('b2c3d4e5-0012-0001-0001-000000000012', 'a1b2c3d4-0012-0001-0001-000000000012', diff --git a/synculous-client/src/app/dashboard/templates/page.tsx b/synculous-client/src/app/dashboard/templates/page.tsx index ff5b4f1..8b072cc 100644 --- a/synculous-client/src/app/dashboard/templates/page.tsx +++ b/synculous-client/src/app/dashboard/templates/page.tsx @@ -3,13 +3,14 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import api from '@/lib/api'; -import { CopyIcon, CheckIcon, FlameIcon } from '@/components/ui/Icons'; +import { CopyIcon, CheckIcon, FlameIcon, ChevronDownIcon, ChevronUpIcon } from '@/components/ui/Icons'; interface Template { id: string; name: string; description?: string; icon?: string; + category: string; step_count: number; } @@ -18,12 +19,16 @@ export default function TemplatesPage() { const [templates, setTemplates] = useState([]); const [isLoading, setIsLoading] = useState(true); const [cloningId, setCloningId] = useState(null); + const [expandedCategories, setExpandedCategories] = useState([]); useEffect(() => { const fetchTemplates = async () => { try { const data = await api.templates.list(); setTemplates(data); + // Expand all categories by default + const categories = [...new Set(data.map((t: Template) => t.category))]; + setExpandedCategories(categories); } catch (err) { console.error('Failed to fetch templates:', err); } finally { @@ -44,6 +49,35 @@ export default function TemplatesPage() { } }; + const toggleCategory = (category: string) => { + setExpandedCategories(prev => + prev.includes(category) + ? prev.filter(c => c !== category) + : [...prev, category] + ); + }; + + // Group templates by category + const groupedTemplates = templates.reduce((acc, template) => { + const category = template.category || 'Other'; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(template); + return acc; + }, {} as Record); + + // Define category order + const categoryOrder = ['Daily Routines', 'Getting Things Done', 'Health & Body', 'Errands', 'Other']; + const sortedCategories = Object.keys(groupedTemplates).sort((a, b) => { + const indexA = categoryOrder.indexOf(a); + const indexB = categoryOrder.indexOf(b); + if (indexA === -1 && indexB === -1) return a.localeCompare(b); + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + if (isLoading) { return (
@@ -66,41 +100,68 @@ export default function TemplatesPage() {

Templates will appear here when available

) : ( -
- {templates.map((template) => ( -
-
-
- {template.icon || '✨'} +
+ {sortedCategories.map((category) => ( +
+ {/* Category Header */} + + + {/* Templates List */} + {expandedCategories.includes(category) && ( +
+ {groupedTemplates[category].map((template) => ( +
+
+
+ {template.icon || '✨'} +
+
+

{template.name}

+ {template.description && ( +

{template.description}

+ )} +

{template.step_count} steps

+
+ +
+
+ ))}
- -
+ )}
))}