Next.js app — chat interface, auth, settings, PWA manifest

This commit is contained in:
2026-04-26 02:05:09 -04:00
parent 241acf2b52
commit 996c4e19a7
12 changed files with 1161 additions and 122 deletions
+125
View File
@@ -0,0 +1,125 @@
'use client';
import { useStore } from '@/lib/store';
import { api } from '@/lib/api';
function formatDate(iso: string): string {
const d = new Date(iso);
const now = new Date();
const diff = (now.getTime() - d.getTime()) / 1000;
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 172800) return 'yesterday';
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function groupConversations(convs: { id: string; title: string; updated_at: string; created_at: string; message_count: number }[]) {
const now = new Date();
const groups: Record<string, typeof convs> = { today: [], yesterday: [], week: [], older: [] };
convs.forEach(c => {
const diff = (now.getTime() - new Date(c.updated_at).getTime()) / 86400000;
if (diff < 1) groups.today.push(c);
else if (diff < 2) groups.yesterday.push(c);
else if (diff < 7) groups.week.push(c);
else groups.older.push(c);
});
return groups;
}
export default function Sidebar() {
const { conversations, currentId, setCurrentId, setMessages, setConversations, setSidebarOpen } = useStore();
async function newConversation() {
const conv = await api.newConversation();
const updated = await api.getConversations();
setConversations(updated);
setCurrentId(conv.id);
setMessages([]);
setSidebarOpen(false);
}
async function loadConversation(id: string) {
setCurrentId(id);
setSidebarOpen(false);
}
async function deleteConv(e: React.MouseEvent, id: string) {
e.stopPropagation();
if (!confirm('Delete this conversation?')) return;
await api.deleteConversation(id);
const updated = await api.getConversations();
setConversations(updated);
if (currentId === id) { setCurrentId(null); setMessages([]); }
}
const groups = groupConversations(conversations);
const labels: Record<string, string> = { today: 'Today', yesterday: 'Yesterday', week: 'This week', older: 'Older' };
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-3" style={{ borderBottom: '1px solid var(--border)' }}>
<button
onClick={newConversation}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-opacity hover:opacity-85"
style={{ background: 'var(--accent)', color: '#e8f5ed', border: 'none', cursor: 'pointer' }}
>
<span style={{ fontSize: '16px', lineHeight: 1 }}>+</span>
New conversation
</button>
</div>
{/* Conversation list */}
<div className="flex-1 overflow-y-auto py-1 px-2">
{Object.entries(labels).map(([key, label]) => {
const group = groups[key];
if (!group.length) return null;
return (
<div key={key}>
<div
className="px-2 py-2 text-xs uppercase tracking-wider"
style={{ color: 'var(--text3)', letterSpacing: '0.06em' }}
>
{label}
</div>
{group.map(c => (
<div
key={c.id}
onClick={() => loadConversation(c.id)}
className="flex items-center gap-2 px-2 py-2 rounded-lg cursor-pointer mb-0.5 group"
style={{
background: c.id === currentId ? 'var(--bg3)' : 'transparent',
}}
onMouseEnter={e => { if (c.id !== currentId) (e.currentTarget as HTMLElement).style.background = 'var(--bg3)'; }}
onMouseLeave={e => { if (c.id !== currentId) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
>
<div className="flex-1 min-w-0">
<div className="text-sm truncate" style={{ color: 'var(--text)' }} title={c.title}>
{c.title}
</div>
<div className="text-xs mt-0.5" style={{ color: 'var(--text3)' }}>
{formatDate(c.updated_at)}
</div>
</div>
<button
onClick={e => deleteConv(e, c.id)}
className="opacity-0 group-hover:opacity-100 px-1 py-0.5 rounded text-sm transition-opacity"
style={{ background: 'none', border: 'none', color: 'var(--text3)', cursor: 'pointer', minWidth: '24px', minHeight: '24px' }}
>
×
</button>
</div>
))}
</div>
);
})}
{conversations.length === 0 && (
<div className="px-3 py-4 text-sm" style={{ color: 'var(--text3)' }}>
No conversations yet
</div>
)}
</div>
</div>
);
}