refactor : remove one time used componenets file

This commit is contained in:
2026-08-15 23:07:13 +07:00 Unverified
parent 285187ca41
commit 42b441a4d9
17 changed files with 314 additions and 363 deletions
@@ -19,13 +19,11 @@ import {
ContextMenuGroup
} from "@/components/ui/context-menu";
import {
AlertCircle,
Calendar,
Check,
Clock,
Copy,
ExternalLink,
Loader2,
Maximize2,
Users,
} from "lucide-react";
@@ -47,12 +45,12 @@ import { normalizeStoredAnswerSnapshot } from "@/lib/form/submission-intake";
import { updateSubmissionAdminNote } from "@/app/admin/form/actions";
import {
useDebouncedAutoSave,
type AutoSaveStatus,
} from "@/hooks/use-debounced-autosave";
import {
DiscordProfileDropdown,
type DiscordMemberProfile,
} from "@/components/dropdown-menu-avatar";
import { SaveStatusIndicator } from "@/components/save-status-indicator";
interface Question {
id: string;
@@ -181,6 +179,7 @@ export default function SubmissionDetailClient({
status={noteAutoSave.status}
error={noteAutoSave.error}
onRetry={noteAutoSave.retry}
fallbackError="Could not save the note."
/>
</Field>
</FieldGroup>
@@ -351,54 +350,6 @@ export default function SubmissionDetailClient({
);
}
function SaveStatusIndicator({
status,
error,
onRetry,
}: {
status: AutoSaveStatus;
error: string | null;
onRetry: () => Promise<void>;
}) {
if (status === "clean") return null;
return (
<span
className="flex items-center gap-1.5 text-xs text-muted-foreground"
aria-live="polite"
>
{status === "dirty" && "Unsaved"}
{status === "saving" && (
<>
<Loader2 className="animate-spin" />
Saving
</>
)}
{status === "saved" && (
<>
<Check />
Saved
</>
)}
{status === "error" && (
<>
<AlertCircle />
<span>{error || "Could not save the note."}</span>
<Button
type="button"
variant="link"
size="sm"
className="h-auto px-1"
onClick={() => void onRetry().catch(() => undefined)}
>
Retry
</Button>
</>
)}
</span>
);
}
function AnswerImagePreview({ src, label }: { src: string; label: string }) {
const [unavailable, setUnavailable] = useState(false);
+234
View File
@@ -0,0 +1,234 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import type { ResultFilters } from "@/lib/form/admin-form-results";
import { Download, X, CalendarDays } from "lucide-react";
import { format, parseISO } from "date-fns";
function DateFilterPicker({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
const selected = value ? parseISO(value) : undefined;
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
data-empty={!selected}
className="w-full justify-start text-left font-normal data-[empty=true]:text-muted-foreground lg:w-44"
>
<CalendarDays data-icon="inline-start" />
{selected ? format(selected, "PP") : label}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selected}
onSelect={(date) => onChange(date ? format(date, "yyyy-MM-dd") : "")}
/>
{selected && (
<div className="border-t p-2">
<Button
type="button"
variant="ghost"
size="sm"
className="w-full"
onClick={() => onChange("")}
>
Clear date
</Button>
</div>
)}
</PopoverContent>
</Popover>
);
}
export function ResultsFilters({
formId,
filters,
onFiltersChange,
}: {
formId: string;
filters: ResultFilters;
onFiltersChange?: (filters: ResultFilters) => void;
}) {
const router = useRouter();
const [values, setValues] = useState(filters);
const base = `/admin/form/${formId}/result`;
const updateValues = (
update: (current: ResultFilters) => ResultFilters
) => {
setValues((current) => {
const next = update(current);
onFiltersChange?.(next);
return next;
});
};
const filterQuery = useMemo(() => {
const params = new URLSearchParams();
if (values.q) params.set("q", values.q);
if (values.sort !== "submittedAt") params.set("sort", values.sort);
if (values.direction !== "desc") params.set("direction", values.direction);
if (values.edited !== "all") params.set("edited", values.edited);
if (values.dateFrom) params.set("dateFrom", values.dateFrom);
if (values.dateTo) params.set("dateTo", values.dateTo);
return params.toString();
}, [values]);
const appliedQuery = useMemo(() => {
const params = new URLSearchParams();
if (filters.q) params.set("q", filters.q);
if (filters.sort !== "submittedAt") params.set("sort", filters.sort);
if (filters.direction !== "desc") params.set("direction", filters.direction);
if (filters.edited !== "all") params.set("edited", filters.edited);
if (filters.dateFrom) params.set("dateFrom", filters.dateFrom);
if (filters.dateTo) params.set("dateTo", filters.dateTo);
return params.toString();
}, [filters]);
useEffect(() => {
if (onFiltersChange) return;
if (filterQuery === appliedQuery) return;
const timeout = window.setTimeout(() => {
router.replace(`${base}${filterQuery ? `?${filterQuery}` : ""}`, {
scroll: false,
});
}, 350);
return () => window.clearTimeout(timeout);
}, [appliedQuery, base, filterQuery, onFiltersChange, router]);
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2 lg:flex-row">
<Input
value={values.q}
onChange={(event) =>
updateValues((current) => ({ ...current, q: event.target.value }))
}
placeholder="Search name or Discord ID"
aria-label="Search responses"
/>
<Select
value={values.edited}
onValueChange={(edited: ResultFilters["edited"]) =>
updateValues((current) => ({ ...current, edited }))
}
>
<SelectTrigger className="w-full lg:w-40" aria-label="Edit status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All responses</SelectItem>
<SelectItem value="edited">Edited</SelectItem>
<SelectItem value="unedited">Not edited</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
value={`${values.sort}-${values.direction}`}
onValueChange={(value) => {
const [sort, direction] = value.split("-") as [
ResultFilters["sort"],
ResultFilters["direction"],
];
updateValues((current) => ({ ...current, sort, direction }));
}}
>
<SelectTrigger className="w-full lg:w-44" aria-label="Sort responses">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="submittedAt-desc">Newest first</SelectItem>
<SelectItem value="submittedAt-asc">Oldest first</SelectItem>
<SelectItem value="respondent-asc">Respondent A–Z</SelectItem>
<SelectItem value="respondent-desc">Respondent Z–A</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<DateFilterPicker
label="From date"
value={values.dateFrom}
onChange={(dateFrom) =>
updateValues((current) => ({ ...current, dateFrom }))
}
/>
<DateFilterPicker
label="Through date"
value={values.dateTo}
onChange={(dateTo) =>
updateValues((current) => ({ ...current, dateTo }))
}
/>
<Button variant="outline" asChild>
<a href={`${base}/export${filterQuery ? `?${filterQuery}` : ""}`}>
<Download data-icon="inline-start" />
CSV
</a>
</Button>
{(values.q ||
values.edited !== "all" ||
values.sort !== "submittedAt" ||
values.direction !== "desc" ||
values.dateFrom ||
values.dateTo) && (
<Button
variant="ghost"
size="icon"
className="self-start lg:self-auto"
aria-label="Clear filters"
asChild={!onFiltersChange}
onClick={onFiltersChange ? () => updateValues(() => ({
q: "",
page: 1,
sort: "submittedAt",
direction: "desc",
edited: "all",
dateFrom: "",
dateTo: "",
})) : undefined}
>
{onFiltersChange ? (
<X />
) : (
<a href={base}>
<X />
</a>
)}
</Button>
)}
</div>
</div>
);
}
+1 -1
View File
@@ -12,7 +12,7 @@ import {
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DeleteAllSubmissionsButton, DeleteSubmissionButton } from "@/components/delete-buttons";
import { RealtimeRefresh } from "@/components/realtime-refresh";
import { ResultsFilters } from "@/components/results-filters";
import { ResultsFilters } from "./client";
import { getDiscordMemberProfile, type DiscordMemberProfile } from "@/lib/discord/discord";
import {
getFilteredSubmissions,