create-arete-workspace 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/bin/arete.js +156 -0
- package/bin/create.js +111 -0
- package/lib/install-openclaw.js +50 -0
- package/lib/scaffold.js +213 -0
- package/lib/setup-wizard.js +88 -0
- package/lib/updater.js +130 -0
- package/package.json +34 -0
- package/packages/gatsaeng-os/README.md +36 -0
- package/packages/gatsaeng-os/components.json +23 -0
- package/packages/gatsaeng-os/eslint.config.mjs +18 -0
- package/packages/gatsaeng-os/next.config.ts +7 -0
- package/packages/gatsaeng-os/package.json +59 -0
- package/packages/gatsaeng-os/postcss.config.mjs +7 -0
- package/packages/gatsaeng-os/public/file.svg +1 -0
- package/packages/gatsaeng-os/public/globe.svg +1 -0
- package/packages/gatsaeng-os/public/next.svg +1 -0
- package/packages/gatsaeng-os/public/vercel.svg +1 -0
- package/packages/gatsaeng-os/public/window.svg +1 -0
- package/packages/gatsaeng-os/python/api_server.py +248 -0
- package/packages/gatsaeng-os/python/briefing.py +145 -0
- package/packages/gatsaeng-os/python/config.py +55 -0
- package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
- package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
- package/packages/gatsaeng-os/python/proactive.py +158 -0
- package/packages/gatsaeng-os/python/requirements.txt +11 -0
- package/packages/gatsaeng-os/python/run.py +28 -0
- package/packages/gatsaeng-os/python/scoring.py +44 -0
- package/packages/gatsaeng-os/python/streak.py +70 -0
- package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
- package/packages/gatsaeng-os/python/timing_engine.py +117 -0
- package/packages/gatsaeng-os/python/vault_io.py +423 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
- package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
- package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
- package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
- package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
- package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
- package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
- package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
- package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
- package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
- package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
- package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
- package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
- package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
- package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
- package/packages/gatsaeng-os/src/app/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
- package/packages/gatsaeng-os/src/app/globals.css +208 -0
- package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
- package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
- package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
- package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
- package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
- package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
- package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
- package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
- package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
- package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
- package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
- package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
- package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
- package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
- package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
- package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
- package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
- package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
- package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
- package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
- package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
- package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
- package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
- package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
- package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
- package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
- package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
- package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
- package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
- package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
- package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
- package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
- package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
- package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
- package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
- package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
- package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
- package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
- package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
- package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
- package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
- package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
- package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
- package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
- package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
- package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
- package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
- package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
- package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
- package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
- package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
- package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
- package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
- package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
- package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
- package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
- package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
- package/packages/gatsaeng-os/src/lib/date.ts +7 -0
- package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
- package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
- package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
- package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
- package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
- package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
- package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
- package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
- package/packages/gatsaeng-os/src/middleware.ts +34 -0
- package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
- package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
- package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
- package/packages/gatsaeng-os/src/types/index.ts +320 -0
- package/packages/gatsaeng-os/tsconfig.json +34 -0
- package/templates/scripts/forge_qa.sh.tmpl +237 -0
- package/templates/scripts/forge_ship.sh.tmpl +183 -0
- package/templates/scripts/session_indexer.py.tmpl +420 -0
- package/templates/scripts/tracer.py.tmpl +266 -0
- package/templates/workspace/AGENTS.md.tmpl +190 -0
- package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
- package/templates/workspace/MEMORY.md.tmpl +35 -0
- package/templates/workspace/SOUL.md.tmpl +258 -0
- package/templates/workspace/TOOLS.md.tmpl +28 -0
- package/templates/workspace/USER.md.tmpl +43 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { use, useState } from 'react'
|
|
4
|
+
import { useQuery } from '@tanstack/react-query'
|
|
5
|
+
import { useUpdateGoal, useDeleteGoal } from '@/hooks/useGoals'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { Badge } from '@/components/ui/badge'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import { Input } from '@/components/ui/input'
|
|
10
|
+
import { Label } from '@/components/ui/label'
|
|
11
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
12
|
+
import { ArrowLeft, Target, Plus, Minus, Brain, UserCircle, MapPin, Calendar, Flag, Trash2, Pencil, Check, X } from 'lucide-react'
|
|
13
|
+
import Link from 'next/link'
|
|
14
|
+
import { useRouter } from 'next/navigation'
|
|
15
|
+
import { useMilestonesWithDDay, useCreateMilestone, useUpdateMilestone, useDeleteMilestone } from '@/hooks/useMilestones'
|
|
16
|
+
import type { Goal, MilestoneWithDDay } from '@/types'
|
|
17
|
+
import { LIMITS } from '@/types'
|
|
18
|
+
|
|
19
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
20
|
+
annual: '연간',
|
|
21
|
+
quarterly: '분기',
|
|
22
|
+
monthly: '월간',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function GoalDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
26
|
+
const { id } = use(params)
|
|
27
|
+
const router = useRouter()
|
|
28
|
+
const [addAmount, setAddAmount] = useState(1)
|
|
29
|
+
const { data: milestones = [] } = useMilestonesWithDDay(id)
|
|
30
|
+
const createMilestone = useCreateMilestone()
|
|
31
|
+
const updateMilestone = useUpdateMilestone()
|
|
32
|
+
const deleteMilestone = useDeleteMilestone()
|
|
33
|
+
const [msDialogOpen, setMsDialogOpen] = useState(false)
|
|
34
|
+
const [editingMs, setEditingMs] = useState<MilestoneWithDDay | null>(null)
|
|
35
|
+
const [msProgressEdit, setMsProgressEdit] = useState<{ id: string; value: number } | null>(null)
|
|
36
|
+
|
|
37
|
+
const { data: goal, refetch } = useQuery({
|
|
38
|
+
queryKey: ['goal', id],
|
|
39
|
+
queryFn: async (): Promise<Goal> => {
|
|
40
|
+
const res = await fetch(`/api/goals/${id}`)
|
|
41
|
+
return res.json()
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const updateGoal = useUpdateGoal()
|
|
46
|
+
const deleteGoal = useDeleteGoal()
|
|
47
|
+
|
|
48
|
+
const handleDelete = () => {
|
|
49
|
+
if (!confirm('이 목표를 삭제하시겠습니까?')) return
|
|
50
|
+
deleteGoal.mutate(id, {
|
|
51
|
+
onSuccess: () => router.push('/goals'),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleUpdateProgress = (delta: number) => {
|
|
56
|
+
if (!goal) return
|
|
57
|
+
const newValue = Math.max(0, (goal.current_value ?? 0) + delta)
|
|
58
|
+
updateGoal.mutate(
|
|
59
|
+
{ id, current_value: newValue },
|
|
60
|
+
{ onSuccess: () => refetch() }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleMilestoneSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
const form = new FormData(e.currentTarget)
|
|
67
|
+
const payload = {
|
|
68
|
+
title: form.get('ms_title') as string,
|
|
69
|
+
target_value: Number(form.get('ms_target')) || 1,
|
|
70
|
+
unit: (form.get('ms_unit') as string) || '',
|
|
71
|
+
due_date: form.get('ms_due_date') as string,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (editingMs) {
|
|
75
|
+
await updateMilestone.mutateAsync({ id: editingMs.id, ...payload })
|
|
76
|
+
} else {
|
|
77
|
+
await createMilestone.mutateAsync({ goal_id: id, ...payload })
|
|
78
|
+
}
|
|
79
|
+
setMsDialogOpen(false)
|
|
80
|
+
setEditingMs(null)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleMsProgressSave = (msId: string, value: number) => {
|
|
84
|
+
updateMilestone.mutate(
|
|
85
|
+
{ id: msId, current_value: Math.max(0, value) },
|
|
86
|
+
{ onSuccess: () => setMsProgressEdit(null) }
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleMsDelete = (msId: string) => {
|
|
91
|
+
if (!confirm('이 마일스톤을 삭제하시겠습니까?')) return
|
|
92
|
+
deleteMilestone.mutate(msId)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!goal) {
|
|
96
|
+
return <div className="max-w-3xl mx-auto animate-pulse"><div className="h-48 bg-card rounded-lg" /></div>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const progress = goal.target_value
|
|
100
|
+
? Math.min(100, Math.round(((goal.current_value ?? 0) / goal.target_value) * 100))
|
|
101
|
+
: 0
|
|
102
|
+
|
|
103
|
+
const daysLeft = goal.due_date
|
|
104
|
+
? Math.ceil((new Date(goal.due_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
|
105
|
+
: null
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="max-w-3xl mx-auto">
|
|
109
|
+
<Link href="/goals" className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4">
|
|
110
|
+
<ArrowLeft className="w-3.5 h-3.5" />
|
|
111
|
+
목표 목록
|
|
112
|
+
</Link>
|
|
113
|
+
|
|
114
|
+
{/* Header */}
|
|
115
|
+
<div className="flex items-start justify-between mb-6">
|
|
116
|
+
<div className="flex items-center gap-3">
|
|
117
|
+
<div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ backgroundColor: goal.color + '20' }}>
|
|
118
|
+
<Target className="w-5 h-5" style={{ color: goal.color }} />
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<h1 className="text-2xl font-bold text-foreground">{goal.title}</h1>
|
|
122
|
+
<div className="flex items-center gap-2 mt-1">
|
|
123
|
+
<Badge variant="outline">{TYPE_LABELS[goal.type]}</Badge>
|
|
124
|
+
<Badge variant="outline" className={goal.status === 'active' ? 'border-gatsaeng-teal text-gatsaeng-teal' : ''}>{goal.status}</Badge>
|
|
125
|
+
{goal.core_value && <Badge variant="outline">{goal.core_value}</Badge>}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex items-center gap-3">
|
|
130
|
+
<Button
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="icon"
|
|
133
|
+
className="text-muted-foreground hover:text-gatsaeng-red"
|
|
134
|
+
onClick={handleDelete}
|
|
135
|
+
>
|
|
136
|
+
<Trash2 className="w-4 h-4" />
|
|
137
|
+
</Button>
|
|
138
|
+
{daysLeft !== null && (
|
|
139
|
+
<div className="text-right">
|
|
140
|
+
<div className={`text-2xl font-bold ${daysLeft <= 7 ? 'text-gatsaeng-red' : daysLeft <= 30 ? 'text-gatsaeng-amber' : 'text-foreground'}`}>
|
|
141
|
+
D{daysLeft > 0 ? `-${daysLeft}` : daysLeft === 0 ? '-Day' : `+${Math.abs(daysLeft)}`}
|
|
142
|
+
</div>
|
|
143
|
+
<div className="text-xs text-muted-foreground">{goal.due_date?.slice(0, 10)}</div>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Progress Ring + Controls */}
|
|
150
|
+
{goal.target_value && (
|
|
151
|
+
<Card className="mb-6">
|
|
152
|
+
<CardContent className="py-6">
|
|
153
|
+
<div className="flex items-center gap-8">
|
|
154
|
+
{/* SVG Ring */}
|
|
155
|
+
<div className="relative w-32 h-32 flex-shrink-0">
|
|
156
|
+
<svg className="w-32 h-32 -rotate-90" viewBox="0 0 128 128">
|
|
157
|
+
<circle cx="64" cy="64" r="56" fill="none" stroke="currentColor" className="text-muted" strokeWidth="8" />
|
|
158
|
+
<circle
|
|
159
|
+
cx="64" cy="64" r="56" fill="none"
|
|
160
|
+
stroke={goal.color}
|
|
161
|
+
strokeWidth="8"
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeDasharray={`${2 * Math.PI * 56}`}
|
|
164
|
+
strokeDashoffset={`${2 * Math.PI * 56 * (1 - progress / 100)}`}
|
|
165
|
+
className="transition-all duration-500"
|
|
166
|
+
/>
|
|
167
|
+
</svg>
|
|
168
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
169
|
+
<span className="text-2xl font-bold" style={{ color: goal.color }}>{progress}%</span>
|
|
170
|
+
<span className="text-[10px] text-muted-foreground">{goal.current_value ?? 0}/{goal.target_value}</span>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Controls */}
|
|
175
|
+
<div className="flex-1">
|
|
176
|
+
<div className="text-sm text-muted-foreground mb-3">
|
|
177
|
+
진행 기록 ({goal.unit ?? '단위'})
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
<Button
|
|
181
|
+
variant="outline"
|
|
182
|
+
size="icon"
|
|
183
|
+
className="h-10 w-10"
|
|
184
|
+
onClick={() => handleUpdateProgress(-addAmount)}
|
|
185
|
+
disabled={(goal.current_value ?? 0) === 0}
|
|
186
|
+
>
|
|
187
|
+
<Minus className="w-4 h-4" />
|
|
188
|
+
</Button>
|
|
189
|
+
<Input
|
|
190
|
+
type="number"
|
|
191
|
+
value={addAmount}
|
|
192
|
+
onChange={e => setAddAmount(Math.max(1, Number(e.target.value)))}
|
|
193
|
+
className="w-20 h-10 text-center"
|
|
194
|
+
min={1}
|
|
195
|
+
/>
|
|
196
|
+
<Button
|
|
197
|
+
size="icon"
|
|
198
|
+
className="h-10 w-10 bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black"
|
|
199
|
+
onClick={() => handleUpdateProgress(addAmount)}
|
|
200
|
+
>
|
|
201
|
+
<Plus className="w-4 h-4" />
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="text-xs text-muted-foreground mt-2">
|
|
205
|
+
{goal.target_value - (goal.current_value ?? 0) > 0
|
|
206
|
+
? `목표까지 ${goal.target_value - (goal.current_value ?? 0)} ${goal.unit ?? ''} 남음`
|
|
207
|
+
: '목표 달성!'}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</CardContent>
|
|
212
|
+
</Card>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Milestones */}
|
|
216
|
+
<Card className="mb-6">
|
|
217
|
+
<CardHeader className="pb-2">
|
|
218
|
+
<div className="flex items-center justify-between">
|
|
219
|
+
<CardTitle className="text-sm flex items-center gap-2">
|
|
220
|
+
<Flag className="w-4 h-4 text-gatsaeng-amber" />
|
|
221
|
+
마일스톤 ({milestones.length}/{LIMITS.MAX_MILESTONES_PER_GOAL})
|
|
222
|
+
</CardTitle>
|
|
223
|
+
<Dialog open={msDialogOpen} onOpenChange={(v) => { setMsDialogOpen(v); if (!v) setEditingMs(null) }}>
|
|
224
|
+
<DialogTrigger asChild>
|
|
225
|
+
<Button
|
|
226
|
+
variant="ghost"
|
|
227
|
+
size="sm"
|
|
228
|
+
className="h-7 text-xs"
|
|
229
|
+
disabled={milestones.length >= LIMITS.MAX_MILESTONES_PER_GOAL}
|
|
230
|
+
onClick={() => setEditingMs(null)}
|
|
231
|
+
>
|
|
232
|
+
<Plus className="w-3.5 h-3.5 mr-1" />
|
|
233
|
+
추가
|
|
234
|
+
</Button>
|
|
235
|
+
</DialogTrigger>
|
|
236
|
+
<DialogContent>
|
|
237
|
+
<DialogHeader>
|
|
238
|
+
<DialogTitle>{editingMs ? '마일스톤 수정' : '마일스톤 추가'}</DialogTitle>
|
|
239
|
+
</DialogHeader>
|
|
240
|
+
<form onSubmit={handleMilestoneSubmit} className="space-y-4">
|
|
241
|
+
<div>
|
|
242
|
+
<Label>제목</Label>
|
|
243
|
+
<Input name="ms_title" required placeholder="1차 중간점검" defaultValue={editingMs?.title ?? ''} />
|
|
244
|
+
</div>
|
|
245
|
+
<div className="grid grid-cols-2 gap-4">
|
|
246
|
+
<div>
|
|
247
|
+
<Label>목표치</Label>
|
|
248
|
+
<Input name="ms_target" type="number" required min={1} placeholder="50" defaultValue={editingMs?.target_value ?? ''} />
|
|
249
|
+
</div>
|
|
250
|
+
<div>
|
|
251
|
+
<Label>단위</Label>
|
|
252
|
+
<Input name="ms_unit" placeholder={goal.unit || '회, 권, 개'} defaultValue={editingMs?.unit ?? goal.unit ?? ''} />
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div>
|
|
256
|
+
<Label>마감일</Label>
|
|
257
|
+
<Input name="ms_due_date" type="date" required defaultValue={editingMs?.due_date?.slice(0, 10) ?? ''} />
|
|
258
|
+
</div>
|
|
259
|
+
<Button
|
|
260
|
+
type="submit"
|
|
261
|
+
className="w-full bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black"
|
|
262
|
+
disabled={createMilestone.isPending || updateMilestone.isPending}
|
|
263
|
+
>
|
|
264
|
+
{editingMs ? '수정' : '추가'}
|
|
265
|
+
</Button>
|
|
266
|
+
</form>
|
|
267
|
+
</DialogContent>
|
|
268
|
+
</Dialog>
|
|
269
|
+
</div>
|
|
270
|
+
</CardHeader>
|
|
271
|
+
<CardContent>
|
|
272
|
+
{milestones.length === 0 ? (
|
|
273
|
+
<p className="text-sm text-muted-foreground text-center py-3">
|
|
274
|
+
아직 마일스톤이 없습니다. 목표를 단계별로 나눠보세요.
|
|
275
|
+
</p>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="space-y-3">
|
|
278
|
+
{milestones.map(m => {
|
|
279
|
+
const msProgress = m.target_value > 0
|
|
280
|
+
? Math.min(100, Math.round((m.current_value / m.target_value) * 100))
|
|
281
|
+
: 0
|
|
282
|
+
const isEditing = msProgressEdit?.id === m.id
|
|
283
|
+
return (
|
|
284
|
+
<div key={m.id} className="group flex items-center gap-3">
|
|
285
|
+
<div className={`text-sm font-bold font-mono min-w-[50px] text-right ${
|
|
286
|
+
m.d_day <= 0 ? 'text-gatsaeng-red' : m.d_day <= 7 ? 'text-gatsaeng-red' : m.d_day <= 30 ? 'text-gatsaeng-amber' : 'text-foreground'
|
|
287
|
+
}`}>
|
|
288
|
+
{m.d_day === 0 ? 'D-Day' : m.d_day > 0 ? `D-${m.d_day}` : `D+${Math.abs(m.d_day)}`}
|
|
289
|
+
</div>
|
|
290
|
+
<div className="flex-1 min-w-0">
|
|
291
|
+
<div className="text-sm text-foreground truncate">{m.title}</div>
|
|
292
|
+
{isEditing ? (
|
|
293
|
+
<div className="flex items-center gap-1 mt-1">
|
|
294
|
+
<Input
|
|
295
|
+
type="number"
|
|
296
|
+
className="h-6 w-16 text-xs"
|
|
297
|
+
value={msProgressEdit.value}
|
|
298
|
+
onChange={e => setMsProgressEdit({ id: m.id, value: Number(e.target.value) })}
|
|
299
|
+
min={0}
|
|
300
|
+
max={m.target_value}
|
|
301
|
+
autoFocus
|
|
302
|
+
onKeyDown={e => {
|
|
303
|
+
if (e.key === 'Enter') handleMsProgressSave(m.id, msProgressEdit.value)
|
|
304
|
+
if (e.key === 'Escape') setMsProgressEdit(null)
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
<span className="text-[10px] text-muted-foreground">/ {m.target_value} {m.unit}</span>
|
|
308
|
+
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => handleMsProgressSave(m.id, msProgressEdit.value)}>
|
|
309
|
+
<Check className="w-3 h-3 text-gatsaeng-teal" />
|
|
310
|
+
</Button>
|
|
311
|
+
<Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => setMsProgressEdit(null)}>
|
|
312
|
+
<X className="w-3 h-3" />
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
) : (
|
|
316
|
+
<div
|
|
317
|
+
className="text-[10px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
|
|
318
|
+
onClick={() => setMsProgressEdit({ id: m.id, value: m.current_value })}
|
|
319
|
+
>
|
|
320
|
+
{m.current_value}/{m.target_value} {m.unit} · {m.due_date.slice(0, 10)}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
<div className="w-16 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
325
|
+
<div
|
|
326
|
+
className="h-full rounded-full transition-all"
|
|
327
|
+
style={{ width: `${msProgress}%`, backgroundColor: goal.color }}
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
<span className="text-xs text-muted-foreground min-w-[30px] text-right">{msProgress}%</span>
|
|
331
|
+
{/* Edit/Delete buttons - visible on hover */}
|
|
332
|
+
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
333
|
+
<Button
|
|
334
|
+
variant="ghost"
|
|
335
|
+
size="icon"
|
|
336
|
+
className="h-6 w-6"
|
|
337
|
+
onClick={() => { setEditingMs(m); setMsDialogOpen(true) }}
|
|
338
|
+
>
|
|
339
|
+
<Pencil className="w-3 h-3" />
|
|
340
|
+
</Button>
|
|
341
|
+
<Button
|
|
342
|
+
variant="ghost"
|
|
343
|
+
size="icon"
|
|
344
|
+
className="h-6 w-6 hover:text-gatsaeng-red"
|
|
345
|
+
onClick={() => handleMsDelete(m.id)}
|
|
346
|
+
>
|
|
347
|
+
<Trash2 className="w-3 h-3" />
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
)
|
|
352
|
+
})}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</CardContent>
|
|
356
|
+
</Card>
|
|
357
|
+
|
|
358
|
+
{/* AI Diagnosis */}
|
|
359
|
+
{(goal.ai_diagnosis || goal.ai_direction) && (
|
|
360
|
+
<Card className="mb-6 border-l-4 border-l-primary">
|
|
361
|
+
<CardHeader className="pb-1">
|
|
362
|
+
<CardTitle className="text-sm flex items-center gap-2 text-primary">
|
|
363
|
+
<Brain className="w-4 h-4" />
|
|
364
|
+
AI 진단
|
|
365
|
+
</CardTitle>
|
|
366
|
+
</CardHeader>
|
|
367
|
+
<CardContent className="space-y-2">
|
|
368
|
+
{goal.ai_diagnosis && <p className="text-sm text-foreground">{goal.ai_diagnosis}</p>}
|
|
369
|
+
{goal.ai_direction && <p className="text-sm text-muted-foreground">{goal.ai_direction}</p>}
|
|
370
|
+
{goal.ai_next_review && (
|
|
371
|
+
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
372
|
+
<Calendar className="w-3 h-3" />
|
|
373
|
+
다음 리뷰: {goal.ai_next_review}
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
</CardContent>
|
|
377
|
+
</Card>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Neuroscience Cards */}
|
|
381
|
+
<div className="grid grid-cols-1 gap-4">
|
|
382
|
+
{goal.why_statement && (
|
|
383
|
+
<Card className="border-l-4" style={{ borderLeftColor: '#7c5cbf' }}>
|
|
384
|
+
<CardHeader className="pb-1">
|
|
385
|
+
<CardTitle className="text-sm flex items-center gap-2 text-gatsaeng-purple">
|
|
386
|
+
<Brain className="w-4 h-4" />
|
|
387
|
+
Why Statement
|
|
388
|
+
</CardTitle>
|
|
389
|
+
</CardHeader>
|
|
390
|
+
<CardContent>
|
|
391
|
+
<p className="text-sm text-foreground">{goal.why_statement}</p>
|
|
392
|
+
</CardContent>
|
|
393
|
+
</Card>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{goal.identity_statement && (
|
|
397
|
+
<Card className="border-l-4" style={{ borderLeftColor: '#58a6ff' }}>
|
|
398
|
+
<CardHeader className="pb-1">
|
|
399
|
+
<CardTitle className="text-sm flex items-center gap-2 text-primary">
|
|
400
|
+
<UserCircle className="w-4 h-4" />
|
|
401
|
+
Identity Statement
|
|
402
|
+
</CardTitle>
|
|
403
|
+
</CardHeader>
|
|
404
|
+
<CardContent>
|
|
405
|
+
<p className="text-sm text-foreground italic">“{goal.identity_statement}”</p>
|
|
406
|
+
</CardContent>
|
|
407
|
+
</Card>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{goal.when_where_how && (
|
|
411
|
+
<Card className="border-l-4" style={{ borderLeftColor: '#00d4aa' }}>
|
|
412
|
+
<CardHeader className="pb-1">
|
|
413
|
+
<CardTitle className="text-sm flex items-center gap-2 text-gatsaeng-teal">
|
|
414
|
+
<MapPin className="w-4 h-4" />
|
|
415
|
+
Implementation Intention
|
|
416
|
+
</CardTitle>
|
|
417
|
+
</CardHeader>
|
|
418
|
+
<CardContent>
|
|
419
|
+
<p className="text-sm text-foreground">{goal.when_where_how}</p>
|
|
420
|
+
</CardContent>
|
|
421
|
+
</Card>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useGoals, useCreateGoal } from '@/hooks/useGoals'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
import { Label } from '@/components/ui/label'
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
10
|
+
import { Badge } from '@/components/ui/badge'
|
|
11
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
12
|
+
import { Progress } from '@/components/ui/progress'
|
|
13
|
+
import { DDayBadge } from '@/components/tasks/DDayBadge'
|
|
14
|
+
import { Plus, Target } from 'lucide-react'
|
|
15
|
+
import Link from 'next/link'
|
|
16
|
+
|
|
17
|
+
const CORE_VALUES = ['성장', '자유', '관계', '건강', '재미', '안정', '창의']
|
|
18
|
+
|
|
19
|
+
export default function GoalsPage() {
|
|
20
|
+
const { data: goals, isLoading } = useGoals()
|
|
21
|
+
const createGoal = useCreateGoal()
|
|
22
|
+
const [open, setOpen] = useState(false)
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
const form = new FormData(e.currentTarget)
|
|
27
|
+
await createGoal.mutateAsync({
|
|
28
|
+
title: form.get('title') as string,
|
|
29
|
+
type: form.get('type') as string,
|
|
30
|
+
target_value: Number(form.get('target_value')) || undefined,
|
|
31
|
+
unit: (form.get('unit') as string) || undefined,
|
|
32
|
+
why_statement: (form.get('why_statement') as string) || undefined,
|
|
33
|
+
identity_statement: (form.get('identity_statement') as string) || undefined,
|
|
34
|
+
when_where_how: (form.get('when_where_how') as string) || undefined,
|
|
35
|
+
core_value: (form.get('core_value') as string) || undefined,
|
|
36
|
+
due_date: (form.get('due_date') as string) || undefined,
|
|
37
|
+
})
|
|
38
|
+
setOpen(false)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="max-w-4xl mx-auto">
|
|
43
|
+
<div className="flex items-center justify-between mb-6">
|
|
44
|
+
<div>
|
|
45
|
+
<h1 className="text-2xl font-bold text-foreground">목표</h1>
|
|
46
|
+
<p className="text-sm text-muted-foreground mt-1">Why로 시작하는 목표 관리</p>
|
|
47
|
+
</div>
|
|
48
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
49
|
+
<DialogTrigger asChild>
|
|
50
|
+
<Button className="bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black">
|
|
51
|
+
<Plus className="w-4 h-4 mr-2" /> 목표 추가
|
|
52
|
+
</Button>
|
|
53
|
+
</DialogTrigger>
|
|
54
|
+
<DialogContent className="max-w-lg">
|
|
55
|
+
<DialogHeader>
|
|
56
|
+
<DialogTitle>목표 만들기</DialogTitle>
|
|
57
|
+
</DialogHeader>
|
|
58
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
59
|
+
<div>
|
|
60
|
+
<Label>목표 제목</Label>
|
|
61
|
+
<Input name="title" required placeholder="영어 비즈니스 레벨 달성" />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="grid grid-cols-2 gap-4">
|
|
64
|
+
<div>
|
|
65
|
+
<Label>타입</Label>
|
|
66
|
+
<Select name="type" defaultValue="quarterly">
|
|
67
|
+
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
68
|
+
<SelectContent>
|
|
69
|
+
<SelectItem value="annual">연간</SelectItem>
|
|
70
|
+
<SelectItem value="quarterly">분기</SelectItem>
|
|
71
|
+
<SelectItem value="monthly">월간</SelectItem>
|
|
72
|
+
</SelectContent>
|
|
73
|
+
</Select>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<Label>마감일</Label>
|
|
77
|
+
<Input name="due_date" type="date" />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="grid grid-cols-2 gap-4">
|
|
81
|
+
<div>
|
|
82
|
+
<Label>목표치</Label>
|
|
83
|
+
<Input name="target_value" type="number" placeholder="100" />
|
|
84
|
+
</div>
|
|
85
|
+
<div>
|
|
86
|
+
<Label>단위</Label>
|
|
87
|
+
<Input name="unit" placeholder="시간, 권, 개 등" />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<Label>왜 이 목표인가요? (Why Statement)</Label>
|
|
92
|
+
<Input name="why_statement" placeholder="자유롭게 협업하고 커리어 선택지를 넓히기 위해" />
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<Label>어떤 사람이 되고 싶은가요? (Identity)</Label>
|
|
96
|
+
<Input name="identity_statement" placeholder="나는 영어로 생각하는 사람이다" />
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<Label>언제, 어디서, 어떻게? (Implementation Intention)</Label>
|
|
100
|
+
<Input name="when_where_how" placeholder="매일 아침 9시, 카페에서, 단어 20개 + 섀도잉 15분" />
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<Label>핵심 가치</Label>
|
|
104
|
+
<Select name="core_value">
|
|
105
|
+
<SelectTrigger><SelectValue placeholder="선택" /></SelectTrigger>
|
|
106
|
+
<SelectContent>
|
|
107
|
+
{CORE_VALUES.map(v => (
|
|
108
|
+
<SelectItem key={v} value={v}>{v}</SelectItem>
|
|
109
|
+
))}
|
|
110
|
+
</SelectContent>
|
|
111
|
+
</Select>
|
|
112
|
+
</div>
|
|
113
|
+
<Button type="submit" className="w-full bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black">
|
|
114
|
+
목표 생성
|
|
115
|
+
</Button>
|
|
116
|
+
</form>
|
|
117
|
+
</DialogContent>
|
|
118
|
+
</Dialog>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{isLoading ? (
|
|
122
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
123
|
+
{[1, 2, 3].map(i => <div key={i} className="h-40 bg-card rounded-lg animate-pulse" />)}
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
127
|
+
{(goals ?? []).map(goal => {
|
|
128
|
+
const progress = goal.target_value
|
|
129
|
+
? Math.round(((goal.current_value ?? 0) / goal.target_value) * 100)
|
|
130
|
+
: 0
|
|
131
|
+
return (
|
|
132
|
+
<Link key={goal.id} href={`/goals/${goal.id}`}>
|
|
133
|
+
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
|
134
|
+
<CardHeader className="pb-2">
|
|
135
|
+
<div className="flex items-center justify-between">
|
|
136
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
137
|
+
<Target className="w-4 h-4" style={{ color: goal.color }} />
|
|
138
|
+
<span className="truncate">{goal.title}</span>
|
|
139
|
+
</CardTitle>
|
|
140
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
141
|
+
{goal.due_date && <DDayBadge dueDate={goal.due_date} />}
|
|
142
|
+
<Badge variant="outline">{goal.type}</Badge>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
<CardContent>
|
|
147
|
+
{goal.target_value && (
|
|
148
|
+
<div className="mb-3">
|
|
149
|
+
<div className="flex justify-between text-xs text-muted-foreground mb-1">
|
|
150
|
+
<span>{goal.current_value ?? 0} / {goal.target_value} {goal.unit}</span>
|
|
151
|
+
<span>{progress}%</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
|
154
|
+
<div
|
|
155
|
+
className="h-full rounded-full transition-all"
|
|
156
|
+
style={{ width: `${progress}%`, backgroundColor: goal.color }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{goal.why_statement && (
|
|
162
|
+
<p className="text-xs text-muted-foreground">
|
|
163
|
+
Why: {goal.why_statement}
|
|
164
|
+
</p>
|
|
165
|
+
)}
|
|
166
|
+
{goal.core_value && (
|
|
167
|
+
<Badge className="mt-2 text-xs" variant="outline">{goal.core_value}</Badge>
|
|
168
|
+
)}
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
</Link>
|
|
172
|
+
)
|
|
173
|
+
})}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Header } from '@/components/layout/Header'
|
|
2
|
+
import { Sidebar } from '@/components/layout/Sidebar'
|
|
3
|
+
import { MobileBottomNav } from '@/components/layout/MobileBottomNav'
|
|
4
|
+
import { OnboardingGate } from '@/components/onboarding/OnboardingGate'
|
|
5
|
+
import { GlobalSearch } from '@/components/search/GlobalSearch'
|
|
6
|
+
|
|
7
|
+
export default function DashboardLayout({
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<OnboardingGate>
|
|
14
|
+
<div className="h-screen flex flex-col overflow-hidden">
|
|
15
|
+
<Header />
|
|
16
|
+
<div className="flex-1 flex overflow-hidden">
|
|
17
|
+
<div className="hidden md:block">
|
|
18
|
+
<Sidebar />
|
|
19
|
+
</div>
|
|
20
|
+
<main className="flex-1 overflow-y-auto p-4 md:p-6 pb-20 md:pb-6">
|
|
21
|
+
{children}
|
|
22
|
+
</main>
|
|
23
|
+
</div>
|
|
24
|
+
<MobileBottomNav />
|
|
25
|
+
<GlobalSearch />
|
|
26
|
+
</div>
|
|
27
|
+
</OnboardingGate>
|
|
28
|
+
)
|
|
29
|
+
}
|