Improve admin form workspace UX
This commit is contained in:
@@ -14,6 +14,14 @@ import {
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Copy, Check, Users, Calendar, Clock } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import Image from 'next/image';
|
||||
import { stripHtml } from "@/lib/utils";
|
||||
import { HtmlDisplay } from "@/components/html-display";
|
||||
@@ -77,14 +85,14 @@ export default function SubmissionDetailClient({
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="w-4 h-4 shrink-0" />
|
||||
<span className="font-semibold text-foreground">Initial Submission:</span>
|
||||
<span>{new Date(submission.submittedAt).toLocaleString("en-US", { timeZone: "Asia/Bangkok", hour12: false })}</span>
|
||||
<span>{new Date(submission.submittedAt).toLocaleString("en-GB", { timeZone: "Asia/Bangkok", hour12: false })}</span>
|
||||
</div>
|
||||
{editHistory.length > 0 && (
|
||||
<div className="flex flex-col mt-2 pt-2 border-t">
|
||||
<span className="font-semibold text-foreground mb-1">Edit History:</span>
|
||||
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-1">
|
||||
{editHistory.map((edit, idx) => (
|
||||
<li key={idx}>Edited at {new Date(edit.editedAt).toLocaleString("en-US", { timeZone: "Asia/Bangkok", hour12: false })}</li>
|
||||
<li key={idx}>Edited at {new Date(edit.editedAt).toLocaleString("en-GB", { timeZone: "Asia/Bangkok", hour12: false })}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -119,9 +127,36 @@ export default function SubmissionDetailClient({
|
||||
<HtmlDisplay html={q.label} />
|
||||
</div>
|
||||
{wasEdited && (
|
||||
<p className="text-xs text-muted-foreground italic flex items-center gap-1">
|
||||
edited
|
||||
</p>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">View history</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Answer history</DialogTitle>
|
||||
<DialogDescription>
|
||||
Previous values for {stripHtml(q.label)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-72">
|
||||
<div className="flex flex-col gap-3 pr-4">
|
||||
{qAnswerHistory.map((entry, idx) => (
|
||||
<div key={idx} className="flex flex-col gap-1 text-sm">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(entry.editedAt).toLocaleString("en-GB", {
|
||||
timeZone: "Asia/Bangkok",
|
||||
hour12: false,
|
||||
})}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap">
|
||||
{entry.answer || "No response"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
{q.imageUrl && (
|
||||
@@ -152,13 +187,14 @@ export default function SubmissionDetailClient({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="shrink-0 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
|
||||
onClick={() => handleCopy(answerValue, q.id)}
|
||||
aria-label={`Copy answer for ${stripHtml(q.label)}`}
|
||||
>
|
||||
{copiedId === q.id ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
<Check />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-muted-foreground" />
|
||||
<Copy />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -198,7 +234,7 @@ export default function SubmissionDetailClient({
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="w-3 h-3 shrink-0" />
|
||||
<span>{new Date(entry.editedAt).toLocaleString("en-US", { timeZone: "Asia/Bangkok", hour12: false })}</span>
|
||||
<span>{new Date(entry.editedAt).toLocaleString("en-GB", { timeZone: "Asia/Bangkok", hour12: false })}</span>
|
||||
</div>
|
||||
<span className="text-sm text-foreground/80 whitespace-pre-wrap leading-relaxed">
|
||||
{entry.answer || <span className="italic text-muted-foreground">No response</span>}
|
||||
|
||||
@@ -12,13 +12,26 @@ interface SubmissionDetailPageProps {
|
||||
id: string;
|
||||
submissionId: string;
|
||||
}>;
|
||||
searchParams: Promise<{ return?: string | string[] }>;
|
||||
}
|
||||
|
||||
export default async function SubmissionDetailPage({ params }: SubmissionDetailPageProps) {
|
||||
const { id: formId, submissionId } = await params;
|
||||
export default async function SubmissionDetailPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: SubmissionDetailPageProps) {
|
||||
const [{ id: formId, submissionId }, query] = await Promise.all([
|
||||
params,
|
||||
searchParams,
|
||||
]);
|
||||
const returnValue = Array.isArray(query.return) ? query.return[0] : query.return;
|
||||
const returnQuery = returnValue?.slice(0, 1000) || "";
|
||||
const backHref = `/admin/form/${formId}/result${returnQuery ? `?${returnQuery}` : ""}`;
|
||||
const detailHref = (id: string) =>
|
||||
`/admin/form/${formId}/result/${id}${returnQuery ? `?return=${encodeURIComponent(returnQuery)}` : ""}`;
|
||||
|
||||
const submission = await db.query.submissions.findFirst({
|
||||
where: (s, { eq }) => eq(s.id, submissionId),
|
||||
where: (s, { eq, and }) =>
|
||||
and(eq(s.id, submissionId), eq(s.formId, formId)),
|
||||
});
|
||||
|
||||
if (!submission) {
|
||||
@@ -50,28 +63,14 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-6xl mx-auto p-4 md:p-6 pb-10">
|
||||
<RealtimeRefresh topic="submissions" formId={formId} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/admin/form/${formId}/result`}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Button variant="ghost" asChild className="self-start">
|
||||
<Link href={backHref}>
|
||||
<ArrowLeft data-icon="inline-start" />
|
||||
Back to responses
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Response Detail</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{prevId && (
|
||||
<Link href={`/admin/form/${formId}/result/${prevId}`}>
|
||||
<Button variant="outline" size="sm">Previous</Button>
|
||||
</Link>
|
||||
)}
|
||||
{nextId && (
|
||||
<Link href={`/admin/form/${formId}/result/${nextId}`}>
|
||||
<Button variant="outline" size="sm">Next</Button>
|
||||
</Link>
|
||||
)}
|
||||
<DeleteSubmissionButton id={submission.id} redirectUrl={`/admin/form/${formId}/result`} />
|
||||
</div>
|
||||
</Button>
|
||||
<DeleteSubmissionButton id={submission.id} redirectUrl={backHref} />
|
||||
</div>
|
||||
|
||||
<SubmissionDetailClient
|
||||
@@ -82,18 +81,18 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
|
||||
<div className="flex justify-between items-center mt-8 pt-6 border-t">
|
||||
{prevId ? (
|
||||
<Link href={`/admin/form/${formId}/result/${prevId}`}>
|
||||
<Link href={detailHref(prevId)}>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Previous
|
||||
<ArrowLeft data-icon="inline-start" /> Previous
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextId && (
|
||||
<Link href={`/admin/form/${formId}/result/${nextId}`}>
|
||||
<Link href={detailHref(nextId)}>
|
||||
<Button variant="outline">
|
||||
Next <ArrowRight className="w-4 h-4 ml-2" />
|
||||
Next <ArrowRight data-icon="inline-end" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user