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,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { listEntities, createEntity } from '@/lib/vault'
|
|
3
|
+
import { routineSchema } from '@/lib/vault/schemas'
|
|
4
|
+
import type { Routine } from '@/types'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const results = await listEntities<Routine>('routines', routineSchema)
|
|
8
|
+
return NextResponse.json(results.map(r => r.data))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
const body = await request.json()
|
|
13
|
+
const now = new Date().toISOString()
|
|
14
|
+
const data = {
|
|
15
|
+
...body,
|
|
16
|
+
days_of_week: body.days_of_week ?? [1, 2, 3, 4, 5, 6, 7],
|
|
17
|
+
trigger_type: body.trigger_type ?? 'time',
|
|
18
|
+
energy_required: body.energy_required ?? 'medium',
|
|
19
|
+
streak: 0,
|
|
20
|
+
longest_streak: 0,
|
|
21
|
+
is_active: true,
|
|
22
|
+
position: body.position ?? 0,
|
|
23
|
+
created_at: now,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entity = await createEntity<Routine>('routines', data)
|
|
27
|
+
return NextResponse.json(entity, { status: 201 })
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getEntity, updateEntity, deleteEntity } from '@/lib/vault'
|
|
3
|
+
import { taskSchema } from '@/lib/vault/schemas'
|
|
4
|
+
import type { Task } from '@/types'
|
|
5
|
+
|
|
6
|
+
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const result = await getEntity<Task>('tasks', id, taskSchema)
|
|
9
|
+
if (!result) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
10
|
+
return NextResponse.json({ ...result.data, _content: result.content ?? '' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
|
+
const { id } = await params
|
|
15
|
+
const body = await request.json()
|
|
16
|
+
const { content: bodyContent, ...updates } = body
|
|
17
|
+
const finalUpdates = { ...updates, updated_at: new Date().toISOString() }
|
|
18
|
+
const result = await updateEntity<Task>('tasks', id, finalUpdates, bodyContent)
|
|
19
|
+
if (!result) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
20
|
+
return NextResponse.json(result)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
const { id } = await params
|
|
25
|
+
const deleted = await deleteEntity('tasks', id)
|
|
26
|
+
if (!deleted) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
27
|
+
return NextResponse.json({ success: true })
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { listEntities, createEntity } from '@/lib/vault'
|
|
3
|
+
import { taskSchema } from '@/lib/vault/schemas'
|
|
4
|
+
import type { Task } from '@/types'
|
|
5
|
+
|
|
6
|
+
export async function GET(request: Request) {
|
|
7
|
+
const { searchParams } = new URL(request.url)
|
|
8
|
+
const projectId = searchParams.get('project_id')
|
|
9
|
+
const view = searchParams.get('view')
|
|
10
|
+
const sort = searchParams.get('sort')
|
|
11
|
+
|
|
12
|
+
const results = await listEntities<Task>('tasks', taskSchema)
|
|
13
|
+
let tasks = results.map(r => ({ ...r.data, _content: r.content ?? '' }))
|
|
14
|
+
|
|
15
|
+
if (projectId) {
|
|
16
|
+
tasks = tasks.filter(t => t.project_id === projectId)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (view) {
|
|
20
|
+
const today = new Date().toISOString().split('T')[0]
|
|
21
|
+
switch (view) {
|
|
22
|
+
case 'inbox':
|
|
23
|
+
tasks = tasks.filter(t => !t.due_date && t.status !== 'done')
|
|
24
|
+
break
|
|
25
|
+
case 'today':
|
|
26
|
+
tasks = tasks.filter(t => t.due_date?.slice(0, 10) === today && t.status !== 'done')
|
|
27
|
+
break
|
|
28
|
+
case 'upcoming':
|
|
29
|
+
tasks = tasks.filter(t => t.due_date && t.due_date.slice(0, 10) > today && t.status !== 'done')
|
|
30
|
+
break
|
|
31
|
+
case 'incomplete':
|
|
32
|
+
tasks = tasks.filter(t => t.status !== 'done')
|
|
33
|
+
break
|
|
34
|
+
case 'done':
|
|
35
|
+
tasks = tasks.filter(t => t.status === 'done')
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sort === 'due_date') {
|
|
41
|
+
tasks.sort((a, b) => (a.due_date ?? '9999').localeCompare(b.due_date ?? '9999'))
|
|
42
|
+
} else if (sort === 'priority') {
|
|
43
|
+
const order: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }
|
|
44
|
+
tasks.sort((a, b) => (order[a.priority] ?? 2) - (order[b.priority] ?? 2))
|
|
45
|
+
} else {
|
|
46
|
+
tasks.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(tasks)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(request: Request) {
|
|
53
|
+
const body = await request.json()
|
|
54
|
+
const now = new Date().toISOString()
|
|
55
|
+
const data = {
|
|
56
|
+
...body,
|
|
57
|
+
status: body.status ?? 'backlog',
|
|
58
|
+
priority: body.priority ?? 'medium',
|
|
59
|
+
position: body.position ?? 0,
|
|
60
|
+
created_at: now,
|
|
61
|
+
updated_at: now,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entity = await createEntity<Task>('tasks', data)
|
|
65
|
+
return NextResponse.json(entity, { status: 201 })
|
|
66
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { FOLDERS } from '@/lib/vault/config'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import matter from 'gray-matter'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const now = new Date()
|
|
9
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
10
|
+
const dirPath = FOLDERS.timing
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const files = await fs.readdir(dirPath)
|
|
14
|
+
const mdFiles = files.filter(f => f.endsWith('.md'))
|
|
15
|
+
|
|
16
|
+
// Find all matching periods, prefer the one with latest start (boundary = new month)
|
|
17
|
+
let bestMatch: Record<string, unknown> | null = null
|
|
18
|
+
let bestStart = ''
|
|
19
|
+
|
|
20
|
+
for (const file of mdFiles) {
|
|
21
|
+
const raw = await fs.readFile(path.join(dirPath, file), 'utf-8')
|
|
22
|
+
const { data } = matter(raw)
|
|
23
|
+
|
|
24
|
+
const start = data.period_start instanceof Date
|
|
25
|
+
? data.period_start.toISOString().slice(0, 10)
|
|
26
|
+
: String(data.period_start || '')
|
|
27
|
+
const end = data.period_end instanceof Date
|
|
28
|
+
? data.period_end.toISOString().slice(0, 10)
|
|
29
|
+
: String(data.period_end || '')
|
|
30
|
+
|
|
31
|
+
if (start && end && today >= start && today <= end) {
|
|
32
|
+
if (!bestMatch || start > bestStart) {
|
|
33
|
+
bestMatch = data
|
|
34
|
+
bestStart = start
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (bestMatch) {
|
|
40
|
+
const data = bestMatch
|
|
41
|
+
const actionItems = typeof data.action_guide === 'string'
|
|
42
|
+
? data.action_guide.split(/[,、]/).map((s: string) => s.trim()).filter(Boolean)
|
|
43
|
+
: Array.isArray(data.action_guide) ? data.action_guide : []
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
month: data.month_name || '',
|
|
47
|
+
pillar: data.month_hanja || '',
|
|
48
|
+
heavenly_stem: (data.month_hanja as string)?.[0] || '',
|
|
49
|
+
earthly_branch: (data.month_hanja as string)?.[1] || '',
|
|
50
|
+
rating: data.rating || 3,
|
|
51
|
+
theme: data.theme || '',
|
|
52
|
+
insight: data.key_insight || '',
|
|
53
|
+
action_guide: actionItems,
|
|
54
|
+
caution: data.caution ? (Array.isArray(data.caution) ? data.caution : [data.caution]) : [],
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return NextResponse.json(null, { status: 404 })
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('[timing/current]', err instanceof Error ? err.message : err)
|
|
61
|
+
return NextResponse.json(null, { status: 404 })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import OpenAI from 'openai'
|
|
3
|
+
import { governorCheck, governorRecord, estimateCost } from '@/lib/llm-governor'
|
|
4
|
+
|
|
5
|
+
const openai = new OpenAI()
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PROMPT =
|
|
8
|
+
'당신은 Eve입니다. Drake의 EA(Executive Assistant)이자 Chief of Staff. 나긋나긋한 톤, 속은 칼. 응답은 한국어로, 3문장 이하로 간결하게. 음성 응답이므로 마크다운 없이 자연스러운 말투로.'
|
|
9
|
+
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const { message, history } = (await req.json()) as {
|
|
13
|
+
message: string
|
|
14
|
+
history: Array<{ role: 'user' | 'assistant'; content: string }>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!message) {
|
|
18
|
+
return NextResponse.json({ error: 'message required' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Governor check
|
|
22
|
+
const check = governorCheck('voice/chat', message, 'gpt-4o')
|
|
23
|
+
if (!check.ok) {
|
|
24
|
+
console.error(`[voice/chat] Governor blocked: ${check.reason}`)
|
|
25
|
+
return NextResponse.json({ error: check.reason }, { status: 429 })
|
|
26
|
+
}
|
|
27
|
+
if (check.cached) {
|
|
28
|
+
return NextResponse.json({ reply: (check.result as { reply: string }).reply })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
|
32
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
33
|
+
...history.slice(-10),
|
|
34
|
+
{ role: 'user', content: message },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const completion = await openai.chat.completions.create({
|
|
38
|
+
model: 'gpt-4o',
|
|
39
|
+
messages,
|
|
40
|
+
max_tokens: 256,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const reply = completion.choices[0]?.message?.content ?? ''
|
|
44
|
+
governorRecord('voice/chat', estimateCost('gpt-4o', message.length), undefined, { reply })
|
|
45
|
+
return NextResponse.json({ reply })
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const msg = e instanceof Error ? e.message : 'chat failed'
|
|
48
|
+
return NextResponse.json({ error: msg }, { status: 500 })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import OpenAI from 'openai'
|
|
3
|
+
|
|
4
|
+
const openai = new OpenAI()
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const formData = await req.formData()
|
|
9
|
+
const audio = formData.get('audio')
|
|
10
|
+
if (!(audio instanceof File)) {
|
|
11
|
+
return NextResponse.json({ error: 'audio file required' }, { status: 400 })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const transcription = await openai.audio.transcriptions.create({
|
|
15
|
+
model: 'whisper-1',
|
|
16
|
+
file: audio,
|
|
17
|
+
language: 'ko',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return NextResponse.json({ text: transcription.text })
|
|
21
|
+
} catch (e) {
|
|
22
|
+
const msg = e instanceof Error ? e.message : 'transcription failed'
|
|
23
|
+
return NextResponse.json({ error: msg }, { status: 500 })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import OpenAI from 'openai'
|
|
3
|
+
|
|
4
|
+
const openai = new OpenAI()
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const { text } = (await req.json()) as { text: string }
|
|
9
|
+
if (!text) {
|
|
10
|
+
return new Response(JSON.stringify({ error: 'text required' }), {
|
|
11
|
+
status: 400,
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const mp3 = await openai.audio.speech.create({
|
|
17
|
+
model: 'tts-1',
|
|
18
|
+
voice: 'nova',
|
|
19
|
+
input: text,
|
|
20
|
+
response_format: 'mp3',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return new Response(mp3.body, {
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'audio/mpeg',
|
|
26
|
+
'Cache-Control': 'no-store',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const msg = e instanceof Error ? e.message : 'tts failed'
|
|
31
|
+
return new Response(JSON.stringify({ error: msg }), {
|
|
32
|
+
status: 500,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
export default function RootError({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string }
|
|
10
|
+
reset: () => void
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
console.error('[RootError]', error)
|
|
14
|
+
}, [error])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen flex flex-col items-center justify-center gap-4 p-8 bg-black text-white">
|
|
18
|
+
<h2 className="text-lg font-bold text-red-500">Application Error</h2>
|
|
19
|
+
<p className="text-xs text-red-400 bg-red-950/30 p-4 rounded-lg max-w-2xl overflow-auto">
|
|
20
|
+
{error.message}
|
|
21
|
+
</p>
|
|
22
|
+
<button
|
|
23
|
+
onClick={reset}
|
|
24
|
+
className="px-4 py-2 rounded bg-white text-black text-sm font-semibold"
|
|
25
|
+
>
|
|
26
|
+
다시 시도
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--color-background: var(--background);
|
|
9
|
+
--color-foreground: var(--foreground);
|
|
10
|
+
--font-sans: var(--font-geist-sans);
|
|
11
|
+
--font-mono: var(--font-geist-mono);
|
|
12
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
13
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
14
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
15
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
16
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
17
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
18
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
19
|
+
--color-sidebar: var(--sidebar);
|
|
20
|
+
--color-chart-5: var(--chart-5);
|
|
21
|
+
--color-chart-4: var(--chart-4);
|
|
22
|
+
--color-chart-3: var(--chart-3);
|
|
23
|
+
--color-chart-2: var(--chart-2);
|
|
24
|
+
--color-chart-1: var(--chart-1);
|
|
25
|
+
--color-ring: var(--ring);
|
|
26
|
+
--color-input: var(--input);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-destructive: var(--destructive);
|
|
29
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
30
|
+
--color-accent: var(--accent);
|
|
31
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
32
|
+
--color-muted: var(--muted);
|
|
33
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
34
|
+
--color-secondary: var(--secondary);
|
|
35
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
36
|
+
--color-primary: var(--primary);
|
|
37
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
38
|
+
--color-popover: var(--popover);
|
|
39
|
+
--color-card-foreground: var(--card-foreground);
|
|
40
|
+
--color-card: var(--card);
|
|
41
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
42
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
43
|
+
--radius-lg: var(--radius);
|
|
44
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
45
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
46
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
47
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
48
|
+
|
|
49
|
+
/* Gatsaeng semantic colors */
|
|
50
|
+
--color-gatsaeng-amber: #f5a623;
|
|
51
|
+
--color-gatsaeng-teal: #00d4aa;
|
|
52
|
+
--color-gatsaeng-purple: #7c5cbf;
|
|
53
|
+
--color-gatsaeng-red: #ff5470;
|
|
54
|
+
--color-mc-header-light: #f6f8fa;
|
|
55
|
+
--color-mc-header: #010409;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Light theme */
|
|
59
|
+
:root {
|
|
60
|
+
--radius: 0.5rem;
|
|
61
|
+
--background: #ffffff;
|
|
62
|
+
--foreground: #1c2128;
|
|
63
|
+
--card: #f6f8fa;
|
|
64
|
+
--card-foreground: #1c2128;
|
|
65
|
+
--popover: #ffffff;
|
|
66
|
+
--popover-foreground: #1c2128;
|
|
67
|
+
--primary: #0969da;
|
|
68
|
+
--primary-foreground: #ffffff;
|
|
69
|
+
--secondary: #eaeef2;
|
|
70
|
+
--secondary-foreground: #1c2128;
|
|
71
|
+
--muted: #eaeef2;
|
|
72
|
+
--muted-foreground: #656d76;
|
|
73
|
+
--accent: #eaeef2;
|
|
74
|
+
--accent-foreground: #1c2128;
|
|
75
|
+
--destructive: #cf222e;
|
|
76
|
+
--border: #d0d7de;
|
|
77
|
+
--input: #d0d7de;
|
|
78
|
+
--ring: #0969da;
|
|
79
|
+
--chart-1: #d4850a;
|
|
80
|
+
--chart-2: #0a8f6f;
|
|
81
|
+
--chart-3: #6639a6;
|
|
82
|
+
--chart-4: #0969da;
|
|
83
|
+
--chart-5: #cf222e;
|
|
84
|
+
--sidebar: #f6f8fa;
|
|
85
|
+
--sidebar-foreground: #1c2128;
|
|
86
|
+
--sidebar-primary: #0969da;
|
|
87
|
+
--sidebar-primary-foreground: #ffffff;
|
|
88
|
+
--sidebar-accent: #eaeef2;
|
|
89
|
+
--sidebar-accent-foreground: #1c2128;
|
|
90
|
+
--sidebar-border: #d0d7de;
|
|
91
|
+
--sidebar-ring: #0969da;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Mission Control dark theme */
|
|
95
|
+
.dark {
|
|
96
|
+
--background: #0d1117;
|
|
97
|
+
--foreground: #c9d1d9;
|
|
98
|
+
--card: #161b22;
|
|
99
|
+
--card-foreground: #c9d1d9;
|
|
100
|
+
--popover: #161b22;
|
|
101
|
+
--popover-foreground: #c9d1d9;
|
|
102
|
+
--primary: #58a6ff;
|
|
103
|
+
--primary-foreground: #0d1117;
|
|
104
|
+
--secondary: #21262d;
|
|
105
|
+
--secondary-foreground: #c9d1d9;
|
|
106
|
+
--muted: #21262d;
|
|
107
|
+
--muted-foreground: #8b949e;
|
|
108
|
+
--accent: #21262d;
|
|
109
|
+
--accent-foreground: #c9d1d9;
|
|
110
|
+
--destructive: #ff5470;
|
|
111
|
+
--border: #30363d;
|
|
112
|
+
--input: #30363d;
|
|
113
|
+
--ring: #58a6ff;
|
|
114
|
+
--chart-1: #f5a623;
|
|
115
|
+
--chart-2: #00d4aa;
|
|
116
|
+
--chart-3: #7c5cbf;
|
|
117
|
+
--chart-4: #58a6ff;
|
|
118
|
+
--chart-5: #ff5470;
|
|
119
|
+
--sidebar: #0d1117;
|
|
120
|
+
--sidebar-foreground: #c9d1d9;
|
|
121
|
+
--sidebar-primary: #58a6ff;
|
|
122
|
+
--sidebar-primary-foreground: #0d1117;
|
|
123
|
+
--sidebar-accent: #161b22;
|
|
124
|
+
--sidebar-accent-foreground: #c9d1d9;
|
|
125
|
+
--sidebar-border: #30363d;
|
|
126
|
+
--sidebar-ring: #58a6ff;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@layer base {
|
|
130
|
+
* {
|
|
131
|
+
@apply border-border outline-ring/50;
|
|
132
|
+
}
|
|
133
|
+
body {
|
|
134
|
+
@apply bg-background text-foreground;
|
|
135
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Pretendard", Helvetica, Arial, sans-serif;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Scrollbar styling */
|
|
140
|
+
::-webkit-scrollbar {
|
|
141
|
+
width: 6px;
|
|
142
|
+
height: 6px;
|
|
143
|
+
}
|
|
144
|
+
::-webkit-scrollbar-track {
|
|
145
|
+
background: transparent;
|
|
146
|
+
}
|
|
147
|
+
::-webkit-scrollbar-thumb {
|
|
148
|
+
background: var(--border);
|
|
149
|
+
border-radius: 3px;
|
|
150
|
+
}
|
|
151
|
+
::-webkit-scrollbar-thumb:hover {
|
|
152
|
+
background: var(--muted-foreground);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ─── react-grid-layout ─── */
|
|
156
|
+
.react-grid-item.react-grid-placeholder {
|
|
157
|
+
background: var(--primary);
|
|
158
|
+
opacity: 0.08;
|
|
159
|
+
border-radius: 0.75rem;
|
|
160
|
+
border: 2px dashed var(--primary);
|
|
161
|
+
transition: all 150ms ease;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.react-grid-item > .react-resizable-handle {
|
|
165
|
+
position: absolute;
|
|
166
|
+
width: 20px;
|
|
167
|
+
height: 20px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.react-grid-item > .react-resizable-handle::after {
|
|
171
|
+
content: '';
|
|
172
|
+
position: absolute;
|
|
173
|
+
right: 5px;
|
|
174
|
+
bottom: 5px;
|
|
175
|
+
width: 6px;
|
|
176
|
+
height: 6px;
|
|
177
|
+
border-right: 2px solid var(--muted-foreground);
|
|
178
|
+
border-bottom: 2px solid var(--muted-foreground);
|
|
179
|
+
opacity: 0;
|
|
180
|
+
transition: opacity 150ms ease;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.react-grid-item:hover > .react-resizable-handle::after {
|
|
184
|
+
opacity: 0.5;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.react-grid-item > .react-resizable-handle-se {
|
|
188
|
+
bottom: 0;
|
|
189
|
+
right: 0;
|
|
190
|
+
cursor: se-resize;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.react-grid-item.cssTransforms {
|
|
194
|
+
transition: transform 150ms ease;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Nova-style card close button — visible on hover (scoped to drag handle) */
|
|
198
|
+
.react-grid-item:hover .card-drag-handle button {
|
|
199
|
+
opacity: 1 !important;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ─── Nova heading utility ─── */
|
|
203
|
+
.nova-heading {
|
|
204
|
+
font-family: var(--font-mono);
|
|
205
|
+
font-weight: 700;
|
|
206
|
+
text-transform: uppercase;
|
|
207
|
+
letter-spacing: 0.15em;
|
|
208
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Geist, Geist_Mono } from 'next/font/google'
|
|
3
|
+
import { Providers } from './providers'
|
|
4
|
+
import './globals.css'
|
|
5
|
+
|
|
6
|
+
const geistSans = Geist({
|
|
7
|
+
variable: '--font-geist-sans',
|
|
8
|
+
subsets: ['latin'],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const geistMono = Geist_Mono({
|
|
12
|
+
variable: '--font-geist-mono',
|
|
13
|
+
subsets: ['latin'],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: 'GatsaengOS — Mission Control',
|
|
18
|
+
description: '뇌과학 기반 습관 & 생산성 시스템',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: Readonly<{
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
}>) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="ko" suppressHydrationWarning>
|
|
28
|
+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
|
29
|
+
<Providers>{children}</Providers>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export default function LoginPage() {
|
|
7
|
+
const [username, setUsername] = useState('')
|
|
8
|
+
const [password, setPassword] = useState('')
|
|
9
|
+
const [error, setError] = useState('')
|
|
10
|
+
const [loading, setLoading] = useState(false)
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
|
|
13
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
14
|
+
e.preventDefault()
|
|
15
|
+
setLoading(true)
|
|
16
|
+
setError('')
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch('/api/auth/login', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ username, password }),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
router.push('/')
|
|
27
|
+
router.refresh()
|
|
28
|
+
} else {
|
|
29
|
+
const data = await res.json()
|
|
30
|
+
setError(data.error || 'Login failed')
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
setError('Network error')
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="min-h-screen flex items-center justify-center bg-black">
|
|
41
|
+
<div className="w-full max-w-sm p-8 rounded-2xl border border-zinc-800 bg-zinc-950">
|
|
42
|
+
<div className="mb-8 text-center">
|
|
43
|
+
<div className="text-3xl mb-2">🧭</div>
|
|
44
|
+
<h1 className="text-xl font-semibold text-white">GatsaengOS</h1>
|
|
45
|
+
<p className="text-zinc-500 text-sm mt-1">갓생 미션 컨트롤</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
49
|
+
<div>
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
placeholder="Username"
|
|
53
|
+
value={username}
|
|
54
|
+
onChange={e => setUsername(e.target.value)}
|
|
55
|
+
required
|
|
56
|
+
autoComplete="username"
|
|
57
|
+
className="w-full px-4 py-3 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-400 transition"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<input
|
|
62
|
+
type="password"
|
|
63
|
+
placeholder="Password"
|
|
64
|
+
value={password}
|
|
65
|
+
onChange={e => setPassword(e.target.value)}
|
|
66
|
+
required
|
|
67
|
+
autoComplete="current-password"
|
|
68
|
+
className="w-full px-4 py-3 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-400 transition"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{error && (
|
|
73
|
+
<p className="text-red-400 text-sm text-center">{error}</p>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="submit"
|
|
78
|
+
disabled={loading}
|
|
79
|
+
className="w-full py-3 rounded-lg bg-white text-black font-semibold hover:bg-zinc-200 transition disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{loading ? '...' : 'Login'}
|
|
82
|
+
</button>
|
|
83
|
+
</form>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { ThemeProvider } from 'next-themes'
|
|
5
|
+
import { TooltipProvider } from '@/components/ui/tooltip'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
|
|
8
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
9
|
+
const [queryClient] = useState(() => new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
staleTime: 3 * 60 * 1000,
|
|
13
|
+
refetchOnWindowFocus: false,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
|
20
|
+
<QueryClientProvider client={queryClient}>
|
|
21
|
+
<TooltipProvider>
|
|
22
|
+
{children}
|
|
23
|
+
</TooltipProvider>
|
|
24
|
+
</QueryClientProvider>
|
|
25
|
+
</ThemeProvider>
|
|
26
|
+
)
|
|
27
|
+
}
|