mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
Add admin pagination and update configs
This commit is contained in:
1 parent
b60b72a289
commit
aff0bc0888
21 files changed
+1793
-481
No files matched your search
@@ -3,7 +3,9 @@ import { supabase } from '@/config';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { PaginationControls } from '@/components/ui/pagination';
|
||||
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
||||
@@ -84,9 +86,12 @@ const statusConfig: Record<FeedbackStatus, { label: string; variant: 'default' |
|
||||
export function FeedbackPage() {
|
||||
const [feedback, setFeedback] = useState<Feedback[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
|
||||
|
||||
// Detail Dialog State
|
||||
const [selectedFeedback, setSelectedFeedback] = useState<Feedback | null>(null);
|
||||
@@ -136,9 +141,14 @@ export function FeedbackPage() {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeedback();
|
||||
}, [fetchFeedback]);
|
||||
useEffect(() => {
|
||||
fetchFeedback();
|
||||
}, [fetchFeedback]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [search, typeFilter, statusFilter]);
|
||||
|
||||
|
||||
const handleOpenDetail = (item: Feedback) => {
|
||||
setSelectedFeedback(item);
|
||||
@@ -173,19 +183,33 @@ export function FeedbackPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredFeedback = feedback.filter(item => {
|
||||
if (typeFilter !== 'all' && item.type !== typeFilter) return false;
|
||||
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
|
||||
if (!search) return true;
|
||||
|
||||
const query = search.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(query) ||
|
||||
item.description.toLowerCase().includes(query) ||
|
||||
item.profile?.name?.toLowerCase().includes(query) ||
|
||||
item.profile?.email?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
const filteredFeedback = feedback.filter(item => {
|
||||
if (typeFilter !== 'all' && item.type !== typeFilter) return false;
|
||||
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
|
||||
if (!search) return true;
|
||||
|
||||
const query = search.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(query) ||
|
||||
item.description.toLowerCase().includes(query) ||
|
||||
item.profile?.name?.toLowerCase().includes(query) ||
|
||||
item.profile?.email?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const totalCount = filteredFeedback.length;
|
||||
const paginatedFeedback = filteredFeedback.slice(
|
||||
(page - 1) * pageSize,
|
||||
page * pageSize
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
||||
if (page > totalPages) {
|
||||
setPage(totalPages);
|
||||
}
|
||||
}, [page, pageSize, totalCount]);
|
||||
|
||||
|
||||
const getTypeBadge = (type: FeedbackType) => {
|
||||
const config = feedbackTypeConfig[type];
|
||||
@@ -267,67 +291,80 @@ export function FeedbackPage() {
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8">
|
||||
<div className="flex justify-center"><Skeleton className="h-6 w-32" /></div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredFeedback.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
{search || typeFilter !== 'all' || statusFilter !== 'all'
|
||||
? 'No feedback matches your filters.'
|
||||
: 'No feedback submissions yet.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredFeedback.map((item) => (
|
||||
<TableRow key={item.id} className="cursor-pointer hover:bg-muted/50" onClick={() => handleOpenDetail(item)}>
|
||||
<TableCell>{getTypeBadge(item.type)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-[300px]">
|
||||
<div className="font-medium truncate">{item.title}</div>
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{item.description.slice(0, 80)}
|
||||
{item.description.length > 80 && '...'}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium">{item.profile?.name || 'Unknown'}</div>
|
||||
<div className="text-muted-foreground">{item.profile?.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(item.status)}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{format(new Date(item.created_at), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); handleOpenDetail(item); }}>
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8">
|
||||
<div className="flex justify-center"><Skeleton className="h-6 w-32" /></div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredFeedback.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
{search || typeFilter !== 'all' || statusFilter !== 'all'
|
||||
? 'No feedback matches your filters.'
|
||||
: 'No feedback submissions yet.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedFeedback.map((item) => (
|
||||
<TableRow key={item.id} className="cursor-pointer hover:bg-muted/50" onClick={() => handleOpenDetail(item)}>
|
||||
<TableCell>{getTypeBadge(item.type)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-[300px]">
|
||||
<div className="font-medium truncate">{item.title}</div>
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{item.description.slice(0, 80)}
|
||||
{item.description.length > 80 && '...'}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium">{item.profile?.name || 'Unknown'}</div>
|
||||
<div className="text-muted-foreground">{item.profile?.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(item.status)}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{format(new Date(item.created_at), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); handleOpenDetail(item); }}>
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{filteredFeedback.length > 0 && (
|
||||
<PaginationControls
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
totalCount={totalCount}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={(size) => {
|
||||
setPage(1);
|
||||
setPageSize(size);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Detail Dialog */}
|
||||
<Dialog open={!!selectedFeedback} onOpenChange={(open) => !open && setSelectedFeedback(null)}>
|
||||
|
||||
Reference in new issue
Block a user