this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add React frontend (designed with Gemini)

Components:
- Layout: Header with stats and refresh button
- DayList: Calendar view of days with sessions
- DayView: Day detail with projects and sessions
- BragSummary: Shareable daily summary with copy button
- ProjectCard: Expandable project with sessions
- SessionItem: Session details with accomplishments

Light mode design with Tailwind CSS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 32163547 e303a09c

+522
+20
src/web/app/App.tsx
··· 1 + import React from 'react'; 2 + import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 + import Layout from './components/Layout'; 4 + import DayList from './components/DayList'; 5 + import DayView from './components/DayView'; 6 + 7 + function App() { 8 + return ( 9 + <BrowserRouter> 10 + <Layout> 11 + <Routes> 12 + <Route path="/" element={<DayList />} /> 13 + <Route path="/day/:date" element={<DayView />} /> 14 + </Routes> 15 + </Layout> 16 + </BrowserRouter> 17 + ); 18 + } 19 + 20 + export default App;
+42
src/web/app/components/BragSummary.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { Copy, Check, Sparkles } from 'lucide-react'; 3 + 4 + interface Props { 5 + summary: string; 6 + } 7 + 8 + export default function BragSummary({ summary }: Props) { 9 + const [copied, setCopied] = useState(false); 10 + 11 + const handleCopy = () => { 12 + navigator.clipboard.writeText(summary); 13 + setCopied(true); 14 + setTimeout(() => setCopied(false), 2000); 15 + }; 16 + 17 + if (!summary) return null; 18 + 19 + return ( 20 + <div className="relative group rounded-2xl p-6 mb-8 bg-gradient-to-br from-indigo-50 to-blue-50 border border-indigo-100 shadow-sm"> 21 + <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity"> 22 + <button 23 + onClick={handleCopy} 24 + className="p-2 rounded-lg bg-white/80 hover:bg-white text-indigo-600 shadow-sm transition-all" 25 + title="Copy to clipboard" 26 + > 27 + {copied ? <Check size={18} /> : <Copy size={18} />} 28 + </button> 29 + </div> 30 + 31 + <div className="flex items-start gap-3"> 32 + <div className="mt-1 p-1.5 bg-indigo-100 text-indigo-600 rounded-md"> 33 + <Sparkles size={20} /> 34 + </div> 35 + <div className="prose prose-blue max-w-none"> 36 + <h3 className="text-sm font-semibold text-indigo-900 uppercase tracking-wider mb-2">Daily Summary</h3> 37 + <p className="text-slate-700 leading-relaxed whitespace-pre-wrap">{summary}</p> 38 + </div> 39 + </div> 40 + </div> 41 + ); 42 + }
+61
src/web/app/components/DayList.tsx
··· 1 + import React from 'react'; 2 + import { Link } from 'react-router-dom'; 3 + import { Calendar, ChevronRight, Layers, Clock } from 'lucide-react'; 4 + import { useDays } from '../hooks/useWorklog'; 5 + 6 + export default function DayList() { 7 + const { days, loading, error } = useDays(); 8 + 9 + if (loading) return <div className="text-center py-20 text-slate-400">Loading your history...</div>; 10 + if (error) return <div className="text-center py-20 text-red-500">Error: {error}</div>; 11 + 12 + if (days.length === 0) { 13 + return ( 14 + <div className="text-center py-20"> 15 + <Calendar size={48} className="mx-auto text-slate-300 mb-4" /> 16 + <h2 className="text-xl font-semibold text-slate-600 mb-2">No sessions yet</h2> 17 + <p className="text-slate-400">Run <code className="bg-slate-100 px-2 py-1 rounded">bun cli process</code> to process your Claude Code sessions.</p> 18 + </div> 19 + ); 20 + } 21 + 22 + return ( 23 + <div className="max-w-3xl mx-auto"> 24 + <h2 className="text-2xl font-bold text-slate-800 mb-6">Session History</h2> 25 + <div className="grid gap-4"> 26 + {days.map((day) => ( 27 + <Link 28 + key={day.date} 29 + to={`/day/${day.date}`} 30 + className="block group" 31 + > 32 + <div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm hover:shadow-md hover:border-blue-200 transition-all flex items-center justify-between"> 33 + <div className="flex items-center gap-4"> 34 + <div className="bg-blue-50 text-blue-600 p-3 rounded-lg group-hover:bg-blue-600 group-hover:text-white transition-colors"> 35 + <Calendar size={24} /> 36 + </div> 37 + <div> 38 + <h3 className="text-lg font-semibold text-slate-800"> 39 + {new Date(day.date).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} 40 + </h3> 41 + <div className="flex items-center gap-4 mt-1 text-sm text-slate-500"> 42 + <div className="flex items-center gap-1.5"> 43 + <Layers size={14} /> 44 + <span>{day.projectCount} Projects</span> 45 + </div> 46 + <div className="flex items-center gap-1.5"> 47 + <Clock size={14} /> 48 + <span>{day.sessionCount} Sessions</span> 49 + </div> 50 + </div> 51 + </div> 52 + </div> 53 + 54 + <ChevronRight className="text-gray-300 group-hover:text-blue-500 transition-colors" /> 55 + </div> 56 + </Link> 57 + ))} 58 + </div> 59 + </div> 60 + ); 61 + }
+41
src/web/app/components/DayView.tsx
··· 1 + import React from 'react'; 2 + import { useParams, Link } from 'react-router-dom'; 3 + import { ArrowLeft } from 'lucide-react'; 4 + import { useDayDetail } from '../hooks/useWorklog'; 5 + import BragSummary from './BragSummary'; 6 + import ProjectCard from './ProjectCard'; 7 + 8 + export default function DayView() { 9 + const { date } = useParams<{ date: string }>(); 10 + const { day, loading, error } = useDayDetail(date); 11 + 12 + if (loading) return <div className="text-center py-20 text-slate-400">Loading day details...</div>; 13 + if (error || !day) return <div className="text-center py-20 text-red-500">Error: {error || 'Day not found'}</div>; 14 + 15 + const formattedDate = new Date(day.date).toLocaleDateString(undefined, { 16 + weekday: 'long', 17 + year: 'numeric', 18 + month: 'long', 19 + day: 'numeric' 20 + }); 21 + 22 + return ( 23 + <div> 24 + <div className="mb-8"> 25 + <Link to="/" className="inline-flex items-center text-sm text-slate-500 hover:text-blue-600 mb-4 transition-colors"> 26 + <ArrowLeft size={16} className="mr-1" /> 27 + Back to History 28 + </Link> 29 + <h1 className="text-3xl font-bold text-slate-900">{formattedDate}</h1> 30 + </div> 31 + 32 + {day.bragSummary && <BragSummary summary={day.bragSummary} />} 33 + 34 + <div className="space-y-8"> 35 + {day.projects.map((project) => ( 36 + <ProjectCard key={project.path} project={project} /> 37 + ))} 38 + </div> 39 + </div> 40 + ); 41 + }
+60
src/web/app/components/Layout.tsx
··· 1 + import React from 'react'; 2 + import { Link } from 'react-router-dom'; 3 + import { RefreshCw, Activity, Calendar } from 'lucide-react'; 4 + import { useStats, useRefresh } from '../hooks/useWorklog'; 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + const { stats, refetch: refetchStats } = useStats(); 8 + const { refresh, refreshing } = useRefresh(); 9 + 10 + const handleRefresh = async () => { 11 + await refresh(); 12 + refetchStats(); 13 + window.location.reload(); 14 + }; 15 + 16 + return ( 17 + <div className="min-h-screen bg-gray-50 text-slate-900 font-sans"> 18 + <header className="bg-white border-b border-gray-200 sticky top-0 z-10 bg-opacity-90 backdrop-blur-sm"> 19 + <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> 20 + <Link to="/" className="flex items-center gap-2 group"> 21 + <div className="bg-blue-600 p-1.5 rounded-lg text-white group-hover:bg-blue-700 transition-colors"> 22 + <Calendar size={20} /> 23 + </div> 24 + <span className="font-bold text-xl tracking-tight text-slate-800">WorkLog</span> 25 + </Link> 26 + 27 + <div className="flex items-center gap-6"> 28 + {stats && ( 29 + <div className="hidden sm:flex gap-4 text-sm text-slate-500"> 30 + <div className="flex items-center gap-1.5"> 31 + <Activity size={16} className="text-blue-500" /> 32 + <span className="font-medium text-slate-700">{stats.totalSessions}</span> sessions 33 + </div> 34 + <div className="flex items-center gap-1.5"> 35 + <Calendar size={16} className="text-blue-500" /> 36 + <span className="font-medium text-slate-700">{stats.totalDays}</span> days 37 + </div> 38 + </div> 39 + )} 40 + 41 + <button 42 + onClick={handleRefresh} 43 + disabled={refreshing} 44 + className={`p-2 rounded-full hover:bg-gray-100 text-slate-500 transition-all ${ 45 + refreshing ? 'animate-spin text-blue-600 bg-blue-50' : '' 46 + }`} 47 + title="Refresh Data" 48 + > 49 + <RefreshCw size={20} /> 50 + </button> 51 + </div> 52 + </div> 53 + </header> 54 + 55 + <main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 56 + {children} 57 + </main> 58 + </div> 59 + ); 60 + }
+60
src/web/app/components/ProjectCard.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { ChevronDown, ChevronUp, Folder } from 'lucide-react'; 3 + import SessionItem from './SessionItem'; 4 + 5 + interface SessionDetail { 6 + sessionId: string; 7 + startTime: string; 8 + endTime: string; 9 + shortSummary: string; 10 + accomplishments: string[]; 11 + filesChanged: string[]; 12 + toolsUsed: string[]; 13 + } 14 + 15 + interface ProjectDetail { 16 + name: string; 17 + path: string; 18 + sessions: SessionDetail[]; 19 + } 20 + 21 + interface Props { 22 + project: ProjectDetail; 23 + } 24 + 25 + export default function ProjectCard({ project }: Props) { 26 + const [expanded, setExpanded] = useState(true); 27 + 28 + return ( 29 + <div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden mb-6"> 30 + <button 31 + onClick={() => setExpanded(!expanded)} 32 + className="w-full flex items-center justify-between p-6 bg-gray-50/50 hover:bg-gray-50 transition-colors text-left" 33 + > 34 + <div className="flex items-center gap-3"> 35 + <div className="p-2 bg-blue-100 text-blue-600 rounded-lg"> 36 + <Folder size={20} /> 37 + </div> 38 + <div> 39 + <h3 className="text-lg font-bold text-slate-800">{project.name}</h3> 40 + <p className="text-xs text-slate-500 font-mono mt-0.5">{project.path}</p> 41 + </div> 42 + </div> 43 + <div className="flex items-center gap-4 text-slate-500"> 44 + <span className="text-sm font-medium">{project.sessions.length} sessions</span> 45 + {expanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />} 46 + </div> 47 + </button> 48 + 49 + {expanded && ( 50 + <div className="p-6 pt-2"> 51 + <div className="mt-6 ml-2"> 52 + {project.sessions.map((session) => ( 53 + <SessionItem key={session.sessionId} session={session} /> 54 + ))} 55 + </div> 56 + </div> 57 + )} 58 + </div> 59 + ); 60 + }
+87
src/web/app/components/SessionItem.tsx
··· 1 + import React from 'react'; 2 + import { Clock, FileCode, Wrench } from 'lucide-react'; 3 + 4 + interface SessionDetail { 5 + sessionId: string; 6 + startTime: string; 7 + endTime: string; 8 + shortSummary: string; 9 + accomplishments: string[]; 10 + filesChanged: string[]; 11 + toolsUsed: string[]; 12 + } 13 + 14 + interface Props { 15 + session: SessionDetail; 16 + } 17 + 18 + export default function SessionItem({ session }: Props) { 19 + const start = new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 20 + const end = new Date(session.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 21 + 22 + const duration = Math.round((new Date(session.endTime).getTime() - new Date(session.startTime).getTime()) / 60000); 23 + 24 + return ( 25 + <div className="pl-6 border-l-2 border-gray-100 pb-8 last:pb-0 relative"> 26 + <div className="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-white border-2 border-blue-200" /> 27 + 28 + <div className="flex flex-col sm:flex-row sm:items-baseline gap-2 mb-3"> 29 + <div className="flex items-center gap-2 text-sm text-slate-500 font-medium"> 30 + <Clock size={14} /> 31 + <span>{start} - {end}</span> 32 + <span className="px-1.5 py-0.5 rounded-full bg-gray-100 text-xs text-gray-600">{duration} min</span> 33 + </div> 34 + </div> 35 + 36 + <div className="bg-white rounded-xl border border-gray-100 p-5 shadow-sm hover:shadow-md transition-shadow"> 37 + <p className="text-slate-800 font-medium text-lg mb-3">{session.shortSummary}</p> 38 + 39 + {session.accomplishments.length > 0 && ( 40 + <ul className="space-y-2 mb-4"> 41 + {session.accomplishments.map((acc, i) => ( 42 + <li key={i} className="flex items-start gap-2 text-slate-600 text-sm"> 43 + <span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-400 shrink-0" /> 44 + <span>{acc}</span> 45 + </li> 46 + ))} 47 + </ul> 48 + )} 49 + 50 + <div className="flex flex-wrap gap-4 mt-4 pt-4 border-t border-gray-50"> 51 + {session.filesChanged.length > 0 && ( 52 + <div className="flex flex-col gap-1.5"> 53 + <span className="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1"> 54 + <FileCode size={12} /> Files 55 + </span> 56 + <div className="flex flex-wrap gap-1.5"> 57 + {session.filesChanged.slice(0, 5).map((f, i) => ( 58 + <span key={i} className="text-xs px-2 py-1 bg-slate-50 text-slate-600 rounded border border-slate-100 font-mono"> 59 + {f.split('/').pop()} 60 + </span> 61 + ))} 62 + {session.filesChanged.length > 5 && ( 63 + <span className="text-xs px-2 py-1 text-slate-400">+{session.filesChanged.length - 5} more</span> 64 + )} 65 + </div> 66 + </div> 67 + )} 68 + 69 + {session.toolsUsed.length > 0 && ( 70 + <div className="flex flex-col gap-1.5"> 71 + <span className="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1"> 72 + <Wrench size={12} /> Tools 73 + </span> 74 + <div className="flex flex-wrap gap-1.5"> 75 + {session.toolsUsed.map((t, i) => ( 76 + <span key={i} className="text-xs px-2 py-1 bg-orange-50 text-orange-700 rounded border border-orange-100"> 77 + {t} 78 + </span> 79 + ))} 80 + </div> 81 + </div> 82 + )} 83 + </div> 84 + </div> 85 + </div> 86 + ); 87 + }
+7
src/web/app/globals.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + body { 6 + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 7 + }
+121
src/web/app/hooks/useWorklog.ts
··· 1 + import { useState, useEffect, useCallback } from 'react'; 2 + 3 + interface DayListItem { 4 + date: string; 5 + projectCount: number; 6 + sessionCount: number; 7 + bragSummary?: string; 8 + } 9 + 10 + interface SessionDetail { 11 + sessionId: string; 12 + startTime: string; 13 + endTime: string; 14 + shortSummary: string; 15 + accomplishments: string[]; 16 + filesChanged: string[]; 17 + toolsUsed: string[]; 18 + } 19 + 20 + interface ProjectDetail { 21 + name: string; 22 + path: string; 23 + sessions: SessionDetail[]; 24 + } 25 + 26 + interface DayDetail { 27 + date: string; 28 + bragSummary?: string; 29 + projects: ProjectDetail[]; 30 + stats: { 31 + totalSessions: number; 32 + totalTokens: number; 33 + }; 34 + } 35 + 36 + interface Stats { 37 + totalSessions: number; 38 + totalDays: number; 39 + totalProjects: number; 40 + } 41 + 42 + export function useDays() { 43 + const [days, setDays] = useState<DayListItem[]>([]); 44 + const [loading, setLoading] = useState(true); 45 + const [error, setError] = useState<string | null>(null); 46 + 47 + const fetchDays = useCallback(async () => { 48 + try { 49 + setLoading(true); 50 + const res = await fetch('/api/days'); 51 + if (!res.ok) throw new Error('Failed to fetch days'); 52 + const data = await res.json(); 53 + setDays(data); 54 + } catch (err) { 55 + setError(err instanceof Error ? err.message : 'Unknown error'); 56 + } finally { 57 + setLoading(false); 58 + } 59 + }, []); 60 + 61 + useEffect(() => { fetchDays(); }, [fetchDays]); 62 + 63 + return { days, loading, error, refetch: fetchDays }; 64 + } 65 + 66 + export function useDayDetail(date: string | undefined) { 67 + const [day, setDay] = useState<DayDetail | null>(null); 68 + const [loading, setLoading] = useState(true); 69 + const [error, setError] = useState<string | null>(null); 70 + 71 + useEffect(() => { 72 + if (!date) return; 73 + const fetchDay = async () => { 74 + try { 75 + setLoading(true); 76 + const res = await fetch(`/api/days/${date}`); 77 + if (!res.ok) throw new Error('Failed to fetch day details'); 78 + const data = await res.json(); 79 + setDay(data); 80 + } catch (err) { 81 + setError(err instanceof Error ? err.message : 'Unknown error'); 82 + } finally { 83 + setLoading(false); 84 + } 85 + }; 86 + fetchDay(); 87 + }, [date]); 88 + 89 + return { day, loading, error }; 90 + } 91 + 92 + export function useStats() { 93 + const [stats, setStats] = useState<Stats | null>(null); 94 + 95 + const fetchStats = useCallback(async () => { 96 + try { 97 + const res = await fetch('/api/stats'); 98 + if (res.ok) setStats(await res.json()); 99 + } catch (e) { 100 + console.error(e); 101 + } 102 + }, []); 103 + 104 + useEffect(() => { fetchStats(); }, [fetchStats]); 105 + return { stats, refetch: fetchStats }; 106 + } 107 + 108 + export function useRefresh() { 109 + const [refreshing, setRefreshing] = useState(false); 110 + 111 + const refresh = async () => { 112 + setRefreshing(true); 113 + try { 114 + await fetch('/api/refresh', { method: 'POST' }); 115 + } finally { 116 + setRefreshing(false); 117 + } 118 + }; 119 + 120 + return { refresh, refreshing }; 121 + }
+13
src/web/app/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Worklog</title> 7 + <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>" /> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="./main.tsx"></script> 12 + </body> 13 + </html>
+10
src/web/app/main.tsx
··· 1 + import React from 'react'; 2 + import ReactDOM from 'react-dom/client'; 3 + import App from './App'; 4 + import './globals.css'; 5 + 6 + ReactDOM.createRoot(document.getElementById('root')!).render( 7 + <React.StrictMode> 8 + <App /> 9 + </React.StrictMode> 10 + );