Skip to content

Commit 71ca53c

Browse files
Copilotankurrera
andcommitted
Add Skills system with database schema, hooks, and UI components
Co-authored-by: ankurrera <186232326+ankurrera@users.noreply.github.com>
1 parent fd36011 commit 71ca53c

16 files changed

Lines changed: 1734 additions & 2 deletions

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Profile from "./pages/Profile";
1010
import Routines from "./pages/Routines";
1111
import ActiveWorkoutSession from "./pages/ActiveWorkoutSession";
1212
import Habits from "./pages/Habits";
13+
import Skills from "./pages/Skills";
1314
import NotFound from "./pages/NotFound";
1415
import TestHeroCard from "./pages/TestHeroCard";
1516

@@ -29,6 +30,7 @@ const App = () => (
2930
<Route path="/routines" element={<Routines />} />
3031
<Route path="/workout/:routineId" element={<ActiveWorkoutSession />} />
3132
<Route path="/habits" element={<Habits />} />
33+
<Route path="/skills" element={<Skills />} />
3234
<Route path="/test-hero-card" element={<TestHeroCard />} />
3335
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
3436
<Route path="*" element={<NotFound />} />
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { useState } from "react";
2+
import { Characteristic, useCharacteristics } from "@/hooks/useCharacteristics";
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { calculateLevelProgress } from "@/lib/levelCalculation";
6+
import { Edit2, Trash2, Save, X, Star } from "lucide-react";
7+
8+
interface CharacteristicCardProps {
9+
characteristic: Characteristic;
10+
}
11+
12+
const CharacteristicCard = ({ characteristic }: CharacteristicCardProps) => {
13+
const { updateCharacteristic, deleteCharacteristic } = useCharacteristics();
14+
const [isEditing, setIsEditing] = useState(false);
15+
const [editName, setEditName] = useState(characteristic.name);
16+
const [editIcon, setEditIcon] = useState(characteristic.icon);
17+
const [editXP, setEditXP] = useState(characteristic.xp.toString());
18+
19+
const progress = calculateLevelProgress(characteristic.xp);
20+
21+
const handleSave = () => {
22+
updateCharacteristic.mutate({
23+
id: characteristic.id,
24+
name: editName,
25+
icon: editIcon,
26+
xp: parseInt(editXP) || 0,
27+
});
28+
setIsEditing(false);
29+
};
30+
31+
const handleCancel = () => {
32+
setEditName(characteristic.name);
33+
setEditIcon(characteristic.icon);
34+
setEditXP(characteristic.xp.toString());
35+
setIsEditing(false);
36+
};
37+
38+
const handleDelete = () => {
39+
if (window.confirm(`Delete characteristic "${characteristic.name}"?`)) {
40+
deleteCharacteristic.mutate(characteristic.id);
41+
}
42+
};
43+
44+
return (
45+
<div className="system-panel p-4 space-y-3">
46+
{/* Header */}
47+
<div className="flex items-start justify-between">
48+
<div className="flex items-center gap-3 flex-1">
49+
{isEditing ? (
50+
<>
51+
<Input
52+
value={editIcon}
53+
onChange={(e) => setEditIcon(e.target.value)}
54+
className="w-12 text-xl text-center bg-input border-border"
55+
maxLength={2}
56+
/>
57+
<Input
58+
value={editName}
59+
onChange={(e) => setEditName(e.target.value)}
60+
className="flex-1 bg-input border-border"
61+
placeholder="Name"
62+
/>
63+
</>
64+
) : (
65+
<>
66+
<div className="text-2xl">{characteristic.icon}</div>
67+
<div className="flex-1">
68+
<h3 className="font-normal text-foreground">
69+
{characteristic.name}
70+
</h3>
71+
<div className="flex items-center gap-2 mt-1">
72+
<span className="text-xs text-muted-foreground">
73+
Level {characteristic.level}
74+
</span>
75+
<Star className="w-3 h-3 text-primary/70 fill-primary/70" />
76+
</div>
77+
</div>
78+
</>
79+
)}
80+
</div>
81+
82+
{/* Actions */}
83+
<div className="flex items-center gap-1">
84+
{isEditing ? (
85+
<>
86+
<Button
87+
variant="ghost"
88+
size="icon"
89+
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50"
90+
onClick={handleSave}
91+
>
92+
<Save className="w-4 h-4" />
93+
</Button>
94+
<Button
95+
variant="ghost"
96+
size="icon"
97+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
98+
onClick={handleCancel}
99+
>
100+
<X className="w-4 h-4" />
101+
</Button>
102+
</>
103+
) : (
104+
<>
105+
<Button
106+
variant="ghost"
107+
size="icon"
108+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
109+
onClick={() => setIsEditing(true)}
110+
>
111+
<Edit2 className="w-3 h-3" />
112+
</Button>
113+
<Button
114+
variant="ghost"
115+
size="icon"
116+
className="h-8 w-8 text-muted-foreground hover:text-red-600"
117+
onClick={handleDelete}
118+
>
119+
<Trash2 className="w-3 h-3" />
120+
</Button>
121+
</>
122+
)}
123+
</div>
124+
</div>
125+
126+
{/* XP Input (when editing) */}
127+
{isEditing && (
128+
<div>
129+
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-1 block">
130+
XP
131+
</label>
132+
<Input
133+
type="number"
134+
value={editXP}
135+
onChange={(e) => setEditXP(e.target.value)}
136+
className="bg-input border-border"
137+
placeholder="0"
138+
/>
139+
</div>
140+
)}
141+
142+
{/* XP Progress Bar */}
143+
<div className="space-y-2">
144+
<div className="flex items-center justify-between text-xs text-muted-foreground">
145+
<span>{progress.xpInCurrentLevel} / {progress.xpNeededForNextLevel} XP</span>
146+
<span>{Math.round(progress.progressPercentage)}%</span>
147+
</div>
148+
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
149+
<div
150+
className="h-full bg-primary/70 transition-all duration-300"
151+
style={{ width: `${Math.min(progress.progressPercentage, 100)}%` }}
152+
/>
153+
</div>
154+
</div>
155+
</div>
156+
);
157+
};
158+
159+
export default CharacteristicCard;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useState } from "react";
2+
import { Characteristic, useCharacteristics } from "@/hooks/useCharacteristics";
3+
import { Button } from "@/components/ui/button";
4+
import { Plus } from "lucide-react";
5+
import CharacteristicCard from "./CharacteristicCard";
6+
import CreateCharacteristicForm from "./CreateCharacteristicForm";
7+
8+
interface CharacteristicsPanelProps {
9+
characteristics: Characteristic[];
10+
}
11+
12+
const CharacteristicsPanel = ({ characteristics }: CharacteristicsPanelProps) => {
13+
const [isCreating, setIsCreating] = useState(false);
14+
15+
return (
16+
<div className="space-y-4">
17+
{/* Header */}
18+
<div className="flex items-center justify-between">
19+
<h2 className="text-lg font-normal text-muted-foreground">
20+
Characteristics
21+
</h2>
22+
<Button
23+
variant="ghost"
24+
size="sm"
25+
onClick={() => setIsCreating(true)}
26+
className="text-muted-foreground hover:text-foreground"
27+
>
28+
<Plus className="w-4 h-4 mr-1" />
29+
New
30+
</Button>
31+
</div>
32+
33+
{/* Create Form */}
34+
{isCreating && (
35+
<CreateCharacteristicForm onClose={() => setIsCreating(false)} />
36+
)}
37+
38+
{/* Characteristics List */}
39+
<div className="space-y-3">
40+
{characteristics.length === 0 && !isCreating && (
41+
<div className="system-panel p-8 text-center">
42+
<p className="text-sm text-muted-foreground mb-3">
43+
No characteristics yet
44+
</p>
45+
<Button
46+
variant="outline"
47+
size="sm"
48+
onClick={() => setIsCreating(true)}
49+
className="border-border hover:bg-muted"
50+
>
51+
<Plus className="w-4 h-4 mr-2" />
52+
Create First Characteristic
53+
</Button>
54+
</div>
55+
)}
56+
57+
{characteristics.map((characteristic) => (
58+
<CharacteristicCard
59+
key={characteristic.id}
60+
characteristic={characteristic}
61+
/>
62+
))}
63+
</div>
64+
</div>
65+
);
66+
};
67+
68+
export default CharacteristicsPanel;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useState } from "react";
2+
import { useCharacteristics } from "@/hooks/useCharacteristics";
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { X } from "lucide-react";
6+
7+
interface CreateCharacteristicFormProps {
8+
onClose: () => void;
9+
}
10+
11+
const iconOptions = ["⭐", "💪", "🧠", "❤️", "⚡", "🎯", "🛡️", "⚔️", "🏃", "🎨"];
12+
13+
const CreateCharacteristicForm = ({ onClose }: CreateCharacteristicFormProps) => {
14+
const { createCharacteristic } = useCharacteristics();
15+
const [name, setName] = useState("");
16+
const [icon, setIcon] = useState("⭐");
17+
const [xp, setXP] = useState("0");
18+
19+
const handleSubmit = (e: React.FormEvent) => {
20+
e.preventDefault();
21+
if (!name.trim()) return;
22+
23+
createCharacteristic.mutate(
24+
{
25+
name: name.trim(),
26+
icon,
27+
xp: parseInt(xp) || 0,
28+
},
29+
{
30+
onSuccess: () => {
31+
onClose();
32+
},
33+
}
34+
);
35+
};
36+
37+
return (
38+
<div className="system-panel p-4 space-y-4 border-2 border-primary/20">
39+
<div className="flex items-center justify-between">
40+
<h3 className="text-sm font-normal text-foreground">
41+
New Characteristic
42+
</h3>
43+
<Button
44+
variant="ghost"
45+
size="icon"
46+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
47+
onClick={onClose}
48+
>
49+
<X className="w-4 h-4" />
50+
</Button>
51+
</div>
52+
53+
<form onSubmit={handleSubmit} className="space-y-4">
54+
{/* Icon Selection */}
55+
<div>
56+
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
57+
Icon
58+
</label>
59+
<div className="grid grid-cols-5 gap-2">
60+
{iconOptions.map((iconOption) => (
61+
<button
62+
key={iconOption}
63+
type="button"
64+
onClick={() => setIcon(iconOption)}
65+
className={`
66+
p-2 text-xl rounded border transition-all
67+
${
68+
icon === iconOption
69+
? "border-primary bg-primary/10"
70+
: "border-border bg-muted hover:border-primary/50"
71+
}
72+
`}
73+
>
74+
{iconOption}
75+
</button>
76+
))}
77+
</div>
78+
</div>
79+
80+
{/* Name */}
81+
<div>
82+
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
83+
Name
84+
</label>
85+
<Input
86+
value={name}
87+
onChange={(e) => setName(e.target.value)}
88+
placeholder="e.g., Strength, Intelligence, Agility"
89+
className="bg-input border-border"
90+
required
91+
/>
92+
</div>
93+
94+
{/* Initial XP */}
95+
<div>
96+
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
97+
Initial XP
98+
</label>
99+
<Input
100+
type="number"
101+
value={xp}
102+
onChange={(e) => setXP(e.target.value)}
103+
placeholder="0"
104+
className="bg-input border-border"
105+
min="0"
106+
/>
107+
</div>
108+
109+
{/* Actions */}
110+
<div className="flex gap-2 pt-2">
111+
<Button
112+
type="button"
113+
variant="outline"
114+
onClick={onClose}
115+
className="flex-1 border-border hover:bg-muted"
116+
>
117+
Cancel
118+
</Button>
119+
<Button
120+
type="submit"
121+
disabled={!name.trim() || createCharacteristic.isPending}
122+
className="flex-1 bg-primary hover:bg-primary/90 text-primary-foreground"
123+
>
124+
{createCharacteristic.isPending ? "Creating..." : "Create"}
125+
</Button>
126+
</div>
127+
</form>
128+
</div>
129+
);
130+
};
131+
132+
export default CreateCharacteristicForm;

0 commit comments

Comments
 (0)