Skip to content

Commit f0aced3

Browse files
committed
unified list
1 parent e7e6197 commit f0aced3

File tree

4 files changed

+270
-397
lines changed

4 files changed

+270
-397
lines changed

src/app/drafts/page.tsx

Lines changed: 9 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,18 @@
11
'use client';
22

33
import { useState, useEffect } from 'react';
4-
import { Button } from '@/components/ui/button';
54
import { Card } from '@/components/ui/card';
65
import { getDraftEntries, deleteEntry } from '@/lib/storage';
7-
import { MorningMeetingEntry, PRIORITIES } from '@/types/morning-meeting';
8-
import { FileText, Trash2, Eye, Edit, FileEdit } from 'lucide-react';
9-
import Link from 'next/link';
10-
import { ViewEntryDialog } from '@/components/ViewEntryDialog';
11-
import { FilterBar } from '@/components/FilterBar';
6+
import { FileEdit } from 'lucide-react';
7+
import { EntriesTable } from '@/components/EntriesTable';
128
import { usePopup } from '@/lib/popup-context';
13-
import { useEntriesFilter, getPriorityBadgeClass, getRegionBadgeClass } from '@/lib/useEntriesFilter';
149

1510
// TODO: Replace with actual user authentication
1611
const CURRENT_USER = 'Current User';
1712

1813
export default function DraftsPage() {
1914
const { confirm: showConfirm, success: showSuccess } = usePopup();
2015
const [entries, setEntries] = useState<any[]>([]);
21-
const [selectedEntry, setSelectedEntry] = useState<MorningMeetingEntry | null>(null);
22-
const [showViewDialog, setShowViewDialog] = useState(false);
23-
24-
// Use shared filter hook
25-
const {
26-
searchTerm,
27-
filterRegion,
28-
filterCategory,
29-
filterPriority,
30-
sortField,
31-
sortDirection,
32-
setSearchTerm,
33-
setFilterRegion,
34-
setFilterCategory,
35-
setFilterPriority,
36-
handleSort,
37-
handleResetFilters,
38-
sortedEntries,
39-
} = useEntriesFilter(entries);
4016

4117
useEffect(() => {
4218
loadEntries();
@@ -84,135 +60,13 @@ export default function DraftsPage() {
8460
</div>
8561
</Card>
8662

87-
{/* Filters */}
88-
<FilterBar
89-
searchTerm={searchTerm}
90-
onSearchChange={setSearchTerm}
91-
filterRegion={filterRegion}
92-
onRegionChange={setFilterRegion}
93-
filterCategory={filterCategory}
94-
onCategoryChange={setFilterCategory}
95-
filterPriority={filterPriority}
96-
onPriorityChange={setFilterPriority}
97-
onReset={handleResetFilters}
98-
resultCount={sortedEntries.length}
99-
resultLabel={sortedEntries.length === 1 ? 'draft' : 'drafts'}
100-
/>
101-
102-
{/* Table */}
103-
<Card className="border-slate-200 p-0">
104-
<div className="overflow-x-auto">
105-
<table className="w-full">
106-
<thead className="border-b border-slate-200 bg-slate-50">
107-
<tr>
108-
<th
109-
className="rounded-tl-xl cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
110-
onClick={() => handleSort('date')}
111-
>
112-
Date {sortField === 'date' && (sortDirection === 'asc' ? '↑' : '↓')}
113-
</th>
114-
<th
115-
className="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
116-
onClick={() => handleSort('headline')}
117-
>
118-
Headline {sortField === 'headline' && (sortDirection === 'asc' ? '↑' : '↓')}
119-
</th>
120-
<th
121-
className="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
122-
onClick={() => handleSort('region')}
123-
>
124-
Region {sortField === 'region' && (sortDirection === 'asc' ? '↑' : '↓')}
125-
</th>
126-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700">
127-
Priority
128-
</th>
129-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700">
130-
Category
131-
</th>
132-
<th className="rounded-tr-xl px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-700">
133-
Actions
134-
</th>
135-
</tr>
136-
</thead>
137-
<tbody>
138-
{sortedEntries.length === 0 ? (
139-
<tr>
140-
<td colSpan={6} className="px-4 py-12 text-center text-slate-500">
141-
No drafts found. <Link href="/form" className="text-un-blue hover:underline">Create your first draft</Link>
142-
</td>
143-
</tr>
144-
) : (
145-
sortedEntries.map((entry) => (
146-
<tr key={entry.id} className="border-b border-slate-100 hover:bg-slate-50">
147-
<td className="whitespace-nowrap px-4 py-3 text-sm text-slate-600">
148-
{new Date(entry.date).toLocaleDateString('en-US', {
149-
month: 'short',
150-
day: 'numeric',
151-
year: 'numeric',
152-
})}
153-
</td>
154-
<td className="max-w-md px-4 py-3 text-sm">
155-
<div className="line-clamp-2">{entry.headline}</div>
156-
</td>
157-
<td className="whitespace-nowrap px-4 py-3">
158-
<span className={`inline-block rounded px-2 py-1 text-xs font-medium ${getRegionBadgeClass(entry.region)}`}>
159-
{entry.region}
160-
</span>
161-
</td>
162-
<td className="whitespace-nowrap px-4 py-3">
163-
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${getPriorityBadgeClass(entry.priority)}`}>
164-
<span className={`h-1.5 w-1.5 rounded-full ${entry.priority === 'sg-attention' ? 'bg-red-600' : 'bg-blue-600'}`} />
165-
{PRIORITIES.find(p => p.value === entry.priority)?.label}
166-
</span>
167-
</td>
168-
<td className="whitespace-nowrap px-4 py-3 text-sm text-slate-600">
169-
{entry.category}
170-
</td>
171-
<td className="whitespace-nowrap px-4 py-3 text-right">
172-
<div className="flex justify-end gap-1">
173-
<Button
174-
variant="ghost"
175-
size="sm"
176-
className="h-8 w-8 p-0"
177-
onClick={() => {
178-
setSelectedEntry(entry);
179-
setShowViewDialog(true);
180-
}}
181-
>
182-
<Eye className="h-4 w-4" />
183-
</Button>
184-
<Link href={`/form?edit=${entry.id}`}>
185-
<Button
186-
variant="ghost"
187-
size="sm"
188-
className="h-8 w-8 p-0 text-slate-600 hover:bg-slate-100"
189-
>
190-
<Edit className="h-4 w-4" />
191-
</Button>
192-
</Link>
193-
<Button
194-
variant="ghost"
195-
size="sm"
196-
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
197-
onClick={() => handleDelete(entry.id)}
198-
>
199-
<Trash2 className="h-4 w-4" />
200-
</Button>
201-
</div>
202-
</td>
203-
</tr>
204-
))
205-
)}
206-
</tbody>
207-
</table>
208-
</div>
209-
</Card>
210-
211-
{/* View Entry Dialog */}
212-
<ViewEntryDialog
213-
open={showViewDialog}
214-
onOpenChange={setShowViewDialog}
215-
entry={selectedEntry}
63+
{/* Entries Table */}
64+
<EntriesTable
65+
entries={entries}
66+
onDelete={handleDelete}
67+
showApprovedColumn={false}
68+
emptyMessage="No drafts found."
69+
resultLabel="drafts"
21670
/>
21771
</div>
21872
</main>

src/components/EntriesTable.tsx

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Button } from '@/components/ui/button';
5+
import { Card } from '@/components/ui/card';
6+
import { MorningMeetingEntry, PRIORITIES } from '@/types/morning-meeting';
7+
import { Trash2, Edit, CheckCircle2, Circle } from 'lucide-react';
8+
import Link from 'next/link';
9+
import { ViewEntryDialog } from './ViewEntryDialog';
10+
import { FilterBar } from './FilterBar';
11+
import { useEntriesFilter, getPriorityBadgeClass, getRegionBadgeClass } from '@/lib/useEntriesFilter';
12+
13+
interface EntriesTableProps {
14+
entries: any[];
15+
onDelete: (id: string) => void;
16+
onToggleApproval?: (entry: any) => void;
17+
showApprovedColumn?: boolean;
18+
emptyMessage?: string;
19+
resultLabel?: string;
20+
}
21+
22+
export function EntriesTable({
23+
entries,
24+
onDelete,
25+
onToggleApproval,
26+
showApprovedColumn = false,
27+
emptyMessage = 'No entries found.',
28+
resultLabel = 'entries',
29+
}: EntriesTableProps) {
30+
const [selectedEntry, setSelectedEntry] = useState<MorningMeetingEntry | null>(null);
31+
const [showViewDialog, setShowViewDialog] = useState(false);
32+
33+
const {
34+
searchTerm,
35+
filterRegion,
36+
filterCategory,
37+
filterPriority,
38+
sortField,
39+
sortDirection,
40+
setSearchTerm,
41+
setFilterRegion,
42+
setFilterCategory,
43+
setFilterPriority,
44+
handleSort,
45+
handleResetFilters,
46+
sortedEntries,
47+
} = useEntriesFilter(entries);
48+
49+
const handleRowClick = (entry: any) => {
50+
setSelectedEntry(entry);
51+
setShowViewDialog(true);
52+
};
53+
54+
const handleActionClick = (e: React.MouseEvent, callback: () => void) => {
55+
e.stopPropagation();
56+
callback();
57+
};
58+
59+
return (
60+
<>
61+
{/* Filters */}
62+
<FilterBar
63+
searchTerm={searchTerm}
64+
onSearchChange={setSearchTerm}
65+
filterRegion={filterRegion}
66+
onRegionChange={setFilterRegion}
67+
filterCategory={filterCategory}
68+
onCategoryChange={setFilterCategory}
69+
filterPriority={filterPriority}
70+
onPriorityChange={setFilterPriority}
71+
onReset={handleResetFilters}
72+
resultCount={sortedEntries.length}
73+
resultLabel={sortedEntries.length === 1 ? resultLabel.replace(/s$/, '') : resultLabel}
74+
/>
75+
76+
{/* Table */}
77+
<Card className="border-slate-200 p-0">
78+
<div className="overflow-x-auto">
79+
<table className="w-full">
80+
<thead className="border-b border-slate-200 bg-slate-50">
81+
<tr>
82+
<th
83+
className="rounded-tl-xl cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
84+
onClick={() => handleSort('date')}
85+
>
86+
Date {sortField === 'date' && (sortDirection === 'asc' ? '↑' : '↓')}
87+
</th>
88+
<th
89+
className="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
90+
onClick={() => handleSort('headline')}
91+
>
92+
Headline {sortField === 'headline' && (sortDirection === 'asc' ? '↑' : '↓')}
93+
</th>
94+
<th
95+
className="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700 hover:bg-slate-100"
96+
onClick={() => handleSort('region')}
97+
>
98+
Region {sortField === 'region' && (sortDirection === 'asc' ? '↑' : '↓')}
99+
</th>
100+
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700">
101+
Priority
102+
</th>
103+
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700">
104+
Category
105+
</th>
106+
{showApprovedColumn && (
107+
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-700">
108+
Approved
109+
</th>
110+
)}
111+
<th className="rounded-tr-xl px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-700">
112+
Actions
113+
</th>
114+
</tr>
115+
</thead>
116+
<tbody>
117+
{sortedEntries.length === 0 ? (
118+
<tr>
119+
<td colSpan={showApprovedColumn ? 7 : 6} className="px-4 py-12 text-center text-slate-500">
120+
{emptyMessage} <Link href="/form" className="text-un-blue hover:underline">Create your first entry</Link>
121+
</td>
122+
</tr>
123+
) : (
124+
sortedEntries.map((entry) => (
125+
<tr
126+
key={entry.id}
127+
className="border-b border-slate-100 hover:bg-slate-50 cursor-pointer"
128+
onClick={() => handleRowClick(entry)}
129+
>
130+
<td className="whitespace-nowrap px-4 py-3 text-sm text-slate-600">
131+
{new Date(entry.date).toLocaleDateString('en-US', {
132+
month: 'short',
133+
day: 'numeric',
134+
year: 'numeric',
135+
})}
136+
</td>
137+
<td className="max-w-md px-4 py-3 text-sm">
138+
<div className="line-clamp-2">{entry.headline}</div>
139+
</td>
140+
<td className="whitespace-nowrap px-4 py-3">
141+
<span className={`inline-block rounded px-2 py-1 text-xs font-medium ${getRegionBadgeClass(entry.region)}`}>
142+
{entry.region}
143+
</span>
144+
</td>
145+
<td className="whitespace-nowrap px-4 py-3">
146+
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${getPriorityBadgeClass(entry.priority)}`}>
147+
<span className={`h-1.5 w-1.5 rounded-full ${entry.priority === 'sg-attention' ? 'bg-red-600' : 'bg-blue-600'}`} />
148+
{PRIORITIES.find(p => p.value === entry.priority)?.label}
149+
</span>
150+
</td>
151+
<td className="whitespace-nowrap px-4 py-3 text-sm text-slate-600">
152+
{entry.category}
153+
</td>
154+
{showApprovedColumn && onToggleApproval && (
155+
<td className="whitespace-nowrap px-4 py-3">
156+
<button
157+
onClick={(e) => handleActionClick(e, () => onToggleApproval(entry))}
158+
className="flex items-center gap-2 text-sm font-medium transition-colors hover:text-un-blue"
159+
>
160+
{entry.approved ? (
161+
<CheckCircle2 className="h-5 w-5 text-green-600" />
162+
) : (
163+
<Circle className="h-5 w-5 text-slate-400" />
164+
)}
165+
<span className="text-xs text-slate-600">
166+
{entry.approved ? 'Yes' : 'No'}
167+
</span>
168+
</button>
169+
</td>
170+
)}
171+
<td className="whitespace-nowrap px-4 py-3 text-right">
172+
<div className="flex justify-end gap-1">
173+
<Link href={`/form?edit=${entry.id}`} onClick={(e) => e.stopPropagation()}>
174+
<Button
175+
variant="ghost"
176+
size="sm"
177+
className="h-8 w-8 p-0 text-slate-600 hover:bg-slate-100"
178+
>
179+
<Edit className="h-4 w-4" />
180+
</Button>
181+
</Link>
182+
<Button
183+
variant="ghost"
184+
size="sm"
185+
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
186+
onClick={(e) => handleActionClick(e, () => onDelete(entry.id))}
187+
>
188+
<Trash2 className="h-4 w-4" />
189+
</Button>
190+
</div>
191+
</td>
192+
</tr>
193+
))
194+
)}
195+
</tbody>
196+
</table>
197+
</div>
198+
</Card>
199+
200+
{/* View Entry Dialog */}
201+
<ViewEntryDialog
202+
open={showViewDialog}
203+
onOpenChange={setShowViewDialog}
204+
entry={selectedEntry}
205+
/>
206+
</>
207+
);
208+
}

0 commit comments

Comments
 (0)