T1/Phase 1: scaffold Vite SPA + Hono API + Bun worker
- apps/web: Vite + React 19 + TanStack Router/Query + shadcn/ui - apps/api: Hono + Bun on :3001 with /api/health, /api/auth/*, /mcp stubs - apps/worker: Bun worker stub, DB connection, graceful SIGTERM - apps/web-legacy/: old Next.js code moved aside (preserved for T2-T8 reference) - Dockerfiles: api (Bun), worker (Bun), spa (multi-stage Caddy) - Caddyfile: serves dist + reverse-proxies /api/* + /mcp to api - docker-compose.yml: 4-service target (api, spa, db, worker) - packages/db/src/client.ts: shared Drizzle client for api + worker - db/client.ts: root-level alias for convenience Parent: t_e1cbd87d -> t_24c9c3fd (T0)
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
'use client';
|
||||
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Code,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Undo,
|
||||
Redo,
|
||||
Heading1,
|
||||
Heading2,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ReportEditorProps {
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick,
|
||||
active,
|
||||
disabled,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn('h-8 w-8', active && 'bg-accent text-accent-foreground')}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: 'Start writing your report...',
|
||||
}),
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor: e }) => {
|
||||
onChange(e.getHTML());
|
||||
},
|
||||
onBlur: () => {
|
||||
onBlur?.();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && content !== editor.getHTML()) {
|
||||
editor.commands.setContent(content);
|
||||
}
|
||||
}, [content, editor]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Toolbar */}
|
||||
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
active={editor.isActive('heading', { level: 1 })}
|
||||
label="Heading 1"
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
active={editor.isActive('heading', { level: 2 })}
|
||||
label="Heading 2"
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-1 w-px bg-border" />
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
active={editor.isActive('bold')}
|
||||
label="Bold"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
active={editor.isActive('italic')}
|
||||
label="Italic"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
active={editor.isActive('strike')}
|
||||
label="Strikethrough"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
active={editor.isActive('code')}
|
||||
label="Code"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-1 w-px bg-border" />
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
active={editor.isActive('bulletList')}
|
||||
label="Bullet list"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
active={editor.isActive('orderedList')}
|
||||
label="Ordered list"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
active={editor.isActive('blockquote')}
|
||||
label="Blockquote"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-1 w-px bg-border" />
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
label="Undo"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
label="Redo"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
|
||||
{/* Editor content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<EditorContent editor={editor} className="tiptap-editor min-h-[400px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
'use client';
|
||||
|
||||
import { Calendar, Target, TrendingUp, Clock, FileBarChart } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: 'weekly' | 'monthly' | 'project' | 'habit' | 'custom';
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const templates: Template[] = [
|
||||
{
|
||||
id: 'weekly',
|
||||
name: 'Weekly Summary',
|
||||
description: 'Review your tasks, habits, and time from the past week',
|
||||
type: 'weekly',
|
||||
icon: Calendar,
|
||||
content: `
|
||||
<h1>Weekly Summary</h1>
|
||||
<h2>Overview</h2>
|
||||
<p>Week of [DATE_RANGE]</p>
|
||||
|
||||
<h2>Tasks Completed</h2>
|
||||
<ul>
|
||||
<li>[TASK_LIST]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Habits Tracked</h2>
|
||||
<ul>
|
||||
<li>[HABIT_SUMMARY]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Time Logged</h2>
|
||||
<p>Total: [TIME_TOTAL]</p>
|
||||
|
||||
<h2>Reflections</h2>
|
||||
<p>What went well this week?</p>
|
||||
<p>What could be improved?</p>
|
||||
<p>Goals for next week:</p>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'monthly',
|
||||
name: 'Monthly Review',
|
||||
description: 'Comprehensive review of the past month',
|
||||
type: 'monthly',
|
||||
icon: Calendar,
|
||||
content: `
|
||||
<h1>Monthly Review</h1>
|
||||
<h2>Month of [DATE_RANGE]</h2>
|
||||
|
||||
<h2>Key Achievements</h2>
|
||||
<ul>
|
||||
<li>[ACHIEVEMENTS]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Project Progress</h2>
|
||||
<ul>
|
||||
<li>[PROJECT_SUMMARY]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Habit Consistency</h2>
|
||||
<p>[HABIT_ANALYSIS]</p>
|
||||
|
||||
<h2>Time Distribution</h2>
|
||||
<p>[TIME_BREAKDOWN]</p>
|
||||
|
||||
<h2>Lessons Learned</h2>
|
||||
<p>What worked well?</p>
|
||||
<p>What didn't work?</p>
|
||||
|
||||
<h2>Next Month's Focus</h2>
|
||||
<p>Priorities:</p>
|
||||
<p>Goals:</p>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'project',
|
||||
name: 'Project Health',
|
||||
description: 'Status and progress report for a specific project',
|
||||
type: 'project',
|
||||
icon: Target,
|
||||
content: `
|
||||
<h1>Project Health Report</h1>
|
||||
<h2>[PROJECT_NAME]</h2>
|
||||
|
||||
<h2>Status Overview</h2>
|
||||
<p>Progress: [PROGRESS]%</p>
|
||||
<p>Tasks: [COMPLETED] / [TOTAL]</p>
|
||||
<p>Due: [DUE_DATE]</p>
|
||||
|
||||
<h2>Milestones</h2>
|
||||
<ul>
|
||||
<li>[MILESTONE_LIST]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Recent Activity</h2>
|
||||
<ul>
|
||||
<li>[RECENT_TASKS]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Risks & Blockers</h2>
|
||||
<p>[RISKS]</p>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li>[NEXT_STEPS]</li>
|
||||
</ul>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'habit',
|
||||
name: 'Habit Analysis',
|
||||
description: 'Deep dive into habit performance and trends',
|
||||
type: 'habit',
|
||||
icon: TrendingUp,
|
||||
content: `
|
||||
<h1>Habit Analysis</h1>
|
||||
<h2>Period: [DATE_RANGE]</h2>
|
||||
|
||||
<h2>Overall Performance</h2>
|
||||
<p>Completion rate: [COMPLETION_RATE]%</p>
|
||||
<p>Active streaks: [STREAK_COUNT]</p>
|
||||
|
||||
<h2>Top Performing Habits</h2>
|
||||
<ol>
|
||||
<li>[TOP_HABITS]</li>
|
||||
</ol>
|
||||
|
||||
<h2>Habits Needing Attention</h2>
|
||||
<ul>
|
||||
<li>[AT_RISK_HABITS]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Trends & Patterns</h2>
|
||||
<p>[TREND_ANALYSIS]</p>
|
||||
|
||||
<h2>Recommendations</h2>
|
||||
<ul>
|
||||
<li>[RECOMMENDATIONS]</li>
|
||||
</ul>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'time',
|
||||
name: 'Time Audit',
|
||||
description: 'Breakdown of where your time went',
|
||||
type: 'custom',
|
||||
icon: Clock,
|
||||
content: `
|
||||
<h1>Time Audit</h1>
|
||||
<h2>Period: [DATE_RANGE]</h2>
|
||||
|
||||
<h2>Total Time Tracked</h2>
|
||||
<p>[TOTAL_TIME]</p>
|
||||
|
||||
<h2>By Domain</h2>
|
||||
<ul>
|
||||
<li>[DOMAIN_BREAKDOWN]</li>
|
||||
</ul>
|
||||
|
||||
<h2>By Project</h2>
|
||||
<ul>
|
||||
<li>[PROJECT_BREAKDOWN]</li>
|
||||
</ul>
|
||||
|
||||
<h2>By Category</h2>
|
||||
<ul>
|
||||
<li>[CATEGORY_BREAKDOWN]</li>
|
||||
</ul>
|
||||
|
||||
<h2>Insights</h2>
|
||||
<p>Where did most time go?</p>
|
||||
<p>Was time aligned with priorities?</p>
|
||||
<p>Adjustments for next period:</p>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
interface ReportTemplatesProps {
|
||||
onSelect: (template: Template) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ReportTemplates({ onSelect, onCancel }: ReportTemplatesProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Choose a Template</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Start with a template or create from scratch
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3" role="list">
|
||||
{templates.map((template) => (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="cursor-pointer transition-shadow hover:shadow-md"
|
||||
onClick={() => onSelect(template)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onSelect(template);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="listitem"
|
||||
aria-label={`Use template: ${template.name}`}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-2">
|
||||
<template.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-base">{template.name}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{template.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Custom template */}
|
||||
<Card
|
||||
className="cursor-pointer border-dashed transition-shadow hover:shadow-md"
|
||||
onClick={() =>
|
||||
onSelect({
|
||||
id: 'custom',
|
||||
name: 'Custom Report',
|
||||
description: 'Start with a blank report',
|
||||
type: 'custom',
|
||||
icon: FileBarChart,
|
||||
content: '<h1>Custom Report</h1><p>Start writing...</p>',
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onSelect({
|
||||
id: 'custom',
|
||||
name: 'Custom Report',
|
||||
description: 'Start with a blank report',
|
||||
type: 'custom',
|
||||
icon: FileBarChart,
|
||||
content: '<h1>Custom Report</h1><p>Start writing...</p>',
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="listitem"
|
||||
aria-label="Start with a blank custom report"
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-muted p-2">
|
||||
<FileBarChart className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-base">Custom Report</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Start with a blank report</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user