refactor : remove one time used componenets file
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user