feat(templates): add category-based organization
- 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
This commit is contained in:
@@ -33,16 +33,39 @@ def register(app):
|
|||||||
|
|
||||||
@app.route("/api/templates", methods=["GET"])
|
@app.route("/api/templates", methods=["GET"])
|
||||||
def api_listTemplates():
|
def api_listTemplates():
|
||||||
"""List all available templates."""
|
"""List all available templates. Optional query param: category"""
|
||||||
user_uuid = _auth(flask.request)
|
user_uuid = _auth(flask.request)
|
||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return flask.jsonify({"error": "unauthorized"}), 401
|
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:
|
for template in templates:
|
||||||
steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position")
|
steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position")
|
||||||
template["step_count"] = len(steps)
|
template["step_count"] = len(steps)
|
||||||
return flask.jsonify(templates), 200
|
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"])
|
@app.route("/api/templates", methods=["POST"])
|
||||||
def api_createTemplate():
|
def api_createTemplate():
|
||||||
"""Create a new template (admin only in production). Body: {name, description?, icon?}"""
|
"""Create a new template (admin only in production). Body: {name, description?, icon?}"""
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ CREATE TABLE IF NOT EXISTS routine_templates (
|
|||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
icon VARCHAR(100),
|
icon VARCHAR(100),
|
||||||
|
category VARCHAR(50) DEFAULT 'Other',
|
||||||
created_by_admin BOOLEAN DEFAULT FALSE
|
created_by_admin BOOLEAN DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ DELETE FROM routine_templates;
|
|||||||
-- The hardest transition of the day. First step is literally just
|
-- The hardest transition of the day. First step is literally just
|
||||||
-- sitting up — two-minute rule. Each step cues the next physically.
|
-- 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',
|
('a1b2c3d4-0001-0001-0001-000000000001',
|
||||||
'Morning Launch',
|
'Morning Launch',
|
||||||
'Get from bed to ready. Starts small — just sit up.',
|
'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
|
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',
|
('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
|
-- The "where are my keys" routine. Externalizes the checklist
|
||||||
-- so working memory doesn't have to hold it.
|
-- 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',
|
('a1b2c3d4-0002-0001-0001-000000000002',
|
||||||
'Leaving the House',
|
'Leaving the House',
|
||||||
'Everything you need before you walk out the door.',
|
'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
|
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',
|
('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
|
-- "at point of performance"). Work block is shorter than classic
|
||||||
-- pomodoro because ADHD sustained attention is shorter.
|
-- 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',
|
('a1b2c3d4-0003-0001-0001-000000000003',
|
||||||
'Focus Sprint',
|
'Focus Sprint',
|
||||||
'One focused work block. Set up your space first.',
|
'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
|
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',
|
('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"
|
-- stimulating, the brain won't stop, and the bed isn't "sleepy"
|
||||||
-- enough without a transition.
|
-- 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',
|
('a1b2c3d4-0004-0001-0001-000000000004',
|
||||||
'Wind Down',
|
'Wind Down',
|
||||||
'Transition from awake-brain to sleep-brain.',
|
'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
|
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',
|
('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
|
-- Not a full cleaning session. Designed to be completable even on
|
||||||
-- a bad executive function day. Each step is one area, one action.
|
-- 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',
|
('a1b2c3d4-0005-0001-0001-000000000005',
|
||||||
'Quick Tidy',
|
'Quick Tidy',
|
||||||
'A fast sweep through the house. Not deep cleaning — just enough.',
|
'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
|
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',
|
('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
|
-- For when basic hygiene feels hard. No judgment. Every step is
|
||||||
-- the minimum viable version — just enough to feel a little better.
|
-- 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',
|
('a1b2c3d4-0006-0001-0001-000000000006',
|
||||||
'Body Reset',
|
'Body Reset',
|
||||||
'Basic care for your body. Even partial counts.',
|
'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
|
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',
|
('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
|
-- For when you're paralyzed and can't start anything. Pure
|
||||||
-- two-minute-rule: the smallest possible actions to build momentum.
|
-- 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',
|
('a1b2c3d4-0007-0001-0001-000000000007',
|
||||||
'Unstuck',
|
'Unstuck',
|
||||||
'Can''t start anything? Start here. Tiny steps, real momentum.',
|
'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
|
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',
|
('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
|
-- End-of-day prep so tomorrow morning isn't harder than it needs
|
||||||
-- to be. Externalizes "things to remember" before sleep.
|
-- 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',
|
('a1b2c3d4-0008-0001-0001-000000000008',
|
||||||
'Evening Reset',
|
'Evening Reset',
|
||||||
'Set tomorrow up to be a little easier.',
|
'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
|
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',
|
('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 "exercise." Movement. The entry point is putting shoes on,
|
||||||
-- not "work out for 30 minutes." Anything beyond standing counts.
|
-- 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',
|
('a1b2c3d4-0009-0001-0001-000000000009',
|
||||||
'Move Your Body',
|
'Move Your Body',
|
||||||
'Not a workout plan. Just movement. Any amount counts.',
|
'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
|
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',
|
('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
|
-- Weekly prep. Slightly longer, but broken into small concrete
|
||||||
-- chunks. Prevents the "where did the week go" spiral.
|
-- 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',
|
('a1b2c3d4-0010-0001-0001-000000000010',
|
||||||
'Sunday Reset',
|
'Sunday Reset',
|
||||||
'Set up the week ahead so Monday doesn''t ambush you.',
|
'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
|
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',
|
('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
|
-- Not "meal prep for the week." One meal. Broken down so the
|
||||||
-- activation energy is low and the sequence is externalized.
|
-- 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',
|
('a1b2c3d4-0011-0001-0001-000000000011',
|
||||||
'Cook a Meal',
|
'Cook a Meal',
|
||||||
'One meal, start to finish. Just follow the steps.',
|
'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
|
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',
|
('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"
|
-- Externalizes the "I have errands but I keep not doing them"
|
||||||
-- problem. Forces the plan into concrete sequential steps.
|
-- 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',
|
('a1b2c3d4-0012-0001-0001-000000000012',
|
||||||
'Errand Run',
|
'Errand Run',
|
||||||
'Get out, do the things, come home. One trip.',
|
'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
|
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',
|
('b2c3d4e5-0012-0001-0001-000000000012', 'a1b2c3d4-0012-0001-0001-000000000012',
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import api from '@/lib/api';
|
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 {
|
interface Template {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
category: string;
|
||||||
step_count: number;
|
step_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,12 +19,16 @@ export default function TemplatesPage() {
|
|||||||
const [templates, setTemplates] = useState<Template[]>([]);
|
const [templates, setTemplates] = useState<Template[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [cloningId, setCloningId] = useState<string | null>(null);
|
const [cloningId, setCloningId] = useState<string | null>(null);
|
||||||
|
const [expandedCategories, setExpandedCategories] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTemplates = async () => {
|
const fetchTemplates = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await api.templates.list();
|
const data = await api.templates.list();
|
||||||
setTemplates(data);
|
setTemplates(data);
|
||||||
|
// Expand all categories by default
|
||||||
|
const categories = [...new Set(data.map((t: Template) => t.category))];
|
||||||
|
setExpandedCategories(categories);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch templates:', err);
|
console.error('Failed to fetch templates:', err);
|
||||||
} finally {
|
} 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<string, Template[]>);
|
||||||
|
|
||||||
|
// 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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[50vh]">
|
<div className="flex items-center justify-center min-h-[50vh]">
|
||||||
@@ -66,11 +100,34 @@ export default function TemplatesPage() {
|
|||||||
<p className="text-gray-500 dark:text-gray-400 text-sm">Templates will appear here when available</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm">Templates will appear here when available</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4">
|
<div className="space-y-4">
|
||||||
{templates.map((template) => (
|
{sortedCategories.map((category) => (
|
||||||
|
<div key={category} className="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden">
|
||||||
|
{/* Category Header */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleCategory(category)}
|
||||||
|
className="w-full px-4 py-3 flex items-center justify-between bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="font-semibold text-gray-900 dark:text-gray-100">{category}</h2>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
({groupedTemplates[category].length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expandedCategories.includes(category) ? (
|
||||||
|
<ChevronUpIcon className="text-gray-500 dark:text-gray-400" size={20} />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="text-gray-500 dark:text-gray-400" size={20} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Templates List */}
|
||||||
|
{expandedCategories.includes(category) && (
|
||||||
|
<div className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
{groupedTemplates[category].map((template) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm"
|
className="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-14 h-14 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-xl flex items-center justify-center flex-shrink-0">
|
<div className="w-14 h-14 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
@@ -86,7 +143,7 @@ export default function TemplatesPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleClone(template.id)}
|
onClick={() => handleClone(template.id)}
|
||||||
disabled={cloningId === template.id}
|
disabled={cloningId === template.id}
|
||||||
className="bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 disabled:opacity-50"
|
className="bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 disabled:opacity-50 hover:bg-indigo-700 transition-colors"
|
||||||
>
|
>
|
||||||
{cloningId === template.id ? (
|
{cloningId === template.id ? (
|
||||||
<>
|
<>
|
||||||
@@ -106,5 +163,9 @@ export default function TemplatesPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user