refactor: move AdminSidebar component to client file and update form layout and submission date formatting
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useTransition, useRef, useCallback, useEffect } from "react";
|
import React, { useState, useTransition, useRef, useCallback, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -18,10 +19,21 @@ import {
|
|||||||
createQuestion,
|
createQuestion,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
deleteQuestion,
|
deleteQuestion,
|
||||||
reorderQuestions,
|
|
||||||
bulkUpdateQuestions,
|
bulkUpdateQuestions,
|
||||||
} from "@/app/actions/questions";
|
} from "@/app/actions/questions";
|
||||||
import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X, Check } from "lucide-react";
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarFooter,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X, Check, LayoutDashboard, ClipboardList, ArrowLeft, Webhook } from "lucide-react";
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { RichTextEditor } from "@/components/rich-text-editor";
|
import { RichTextEditor } from "@/components/rich-text-editor";
|
||||||
import { FormattedText } from "@/components/formatted-text";
|
import { FormattedText } from "@/components/formatted-text";
|
||||||
@@ -596,3 +608,66 @@ function AnswerPreview({ question }: { question: Question }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AdminSidebar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar variant="sidebar" collapsible="icon">
|
||||||
|
<SidebarHeader className="border-b border-border py-4 px-4 flex items-center justify-between">
|
||||||
|
<span className="font-bold tracking-wider text-primary text-sm uppercase">
|
||||||
|
Form admin
|
||||||
|
</span>
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild isActive={pathname === "/form/admin"}>
|
||||||
|
<Link href="/form/admin">
|
||||||
|
<LayoutDashboard className="w-4 h-4" />
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/result")}>
|
||||||
|
<Link href="/form/admin/result">
|
||||||
|
<ClipboardList className="w-4 h-4" />
|
||||||
|
<span>Response</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/extra")}>
|
||||||
|
<Link href="/form/admin/extra">
|
||||||
|
<Webhook className="w-4 h-4" />
|
||||||
|
<span>Extra</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarFooter className="border-t border-border p-4">
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<Link href="/form">
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
<span>Go public</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
||||||
import { AdminSidebar } from "@/components/admin-sidebar";
|
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
import { AdminSidebar } from "./client";
|
||||||
|
|
||||||
export default async function AdminLayout({
|
export default async function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -22,11 +22,11 @@ export default async function AdminLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex min-h-screen w-full bg-background text-foreground dark">
|
<div className="flex w-full bg-background text-foreground dark">
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
<main className="flex-1 p-6 md:p-8 overflow-y-auto max-w-6xl mx-auto">
|
<div className="flex-1 overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ export default function SubmissionDetailClient({
|
|||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
<Calendar className="w-4 h-4 shrink-0" />
|
<Calendar className="w-4 h-4 shrink-0" />
|
||||||
<span className="font-semibold text-foreground">Initial Submission:</span>
|
<span className="font-semibold text-foreground">Initial Submission:</span>
|
||||||
<span>{new Date(submission.submittedAt).toLocaleString()}</span>
|
<span>{new Date(submission.submittedAt).toLocaleString(undefined, { hour12: false })}</span>
|
||||||
</div>
|
</div>
|
||||||
{editHistory.length > 0 && (
|
{editHistory.length > 0 && (
|
||||||
<div className="flex flex-col mt-2 pt-2 border-t">
|
<div className="flex flex-col mt-2 pt-2 border-t">
|
||||||
<span className="font-semibold text-foreground mb-1">Edit History:</span>
|
<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">
|
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-1">
|
||||||
{editHistory.map((edit, idx) => (
|
{editHistory.map((edit, idx) => (
|
||||||
<li key={idx}>Edited at {new Date(edit.editedAt).toLocaleString()}</li>
|
<li key={idx}>Edited at {new Date(edit.editedAt).toLocaleString(undefined, { hour12: false })}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-53
@@ -16,7 +16,7 @@ import { Dialog, DialogContent, DialogTrigger, DialogTitle } from "@/components/
|
|||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { submitForm, deleteOwnSubmission } from "@/app/actions/submissions";
|
import { submitForm, deleteOwnSubmission } from "@/app/actions/submissions";
|
||||||
import { Loader2, LogOut, ChevronRight, ChevronLeft } from "lucide-react";
|
import { Loader2, LogOut } from "lucide-react";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -87,7 +87,6 @@ interface FormFillerClientProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LS_KEY = (formId: string) => `form-draft-${formId}`;
|
const LS_KEY = (formId: string) => `form-draft-${formId}`;
|
||||||
const QUESTIONS_PER_STEP = 3;
|
|
||||||
|
|
||||||
const buildSchema = (questions: Question[]) => {
|
const buildSchema = (questions: Question[]) => {
|
||||||
const shape: Record<string, z.ZodTypeAny> = {};
|
const shape: Record<string, z.ZodTypeAny> = {};
|
||||||
@@ -177,27 +176,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [watch, form.id]);
|
}, [watch, form.id]);
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
|
||||||
const totalSteps = Math.ceil(questions.length / QUESTIONS_PER_STEP);
|
|
||||||
|
|
||||||
const currentQuestions = questions.slice(
|
|
||||||
currentStep * QUESTIONS_PER_STEP,
|
|
||||||
(currentStep + 1) * QUESTIONS_PER_STEP
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextStep = async () => {
|
|
||||||
const fieldsToValidate = currentQuestions.map(q => q.id);
|
|
||||||
const isStepValid = await trigger(fieldsToValidate);
|
|
||||||
if (isStepValid) {
|
|
||||||
setCurrentStep(prev => Math.min(prev + 1, totalSteps - 1));
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
setCurrentStep(prev => Math.max(prev - 1, 0));
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
if (!form.isOpen) return;
|
if (!form.isOpen) return;
|
||||||
@@ -276,7 +255,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
<Form {...methods}>
|
<Form {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{currentQuestions.map((q) => (
|
{questions.map((q) => (
|
||||||
<Card key={q.id} className="shadow-sm">
|
<Card key={q.id} className="shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
@@ -453,33 +432,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 pt-4">
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground font-medium w-full sm:w-auto justify-center sm:justify-start">
|
|
||||||
หน้า {currentStep + 1} จาก {totalSteps || 1}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between sm:justify-end items-center gap-3 w-full sm:w-auto">
|
|
||||||
{currentStep > 0 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={isSubmitting || !form.isOpen}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4 mr-1" /> ย้อนกลับ
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep < totalSteps - 1 ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={nextStep}
|
|
||||||
disabled={isSubmitting || !form.isOpen}
|
|
||||||
>
|
|
||||||
ถัดไป <ChevronRight className="w-4 h-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{isUpdating && (
|
{isUpdating && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
@@ -524,11 +477,10 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -19,8 +19,8 @@ export default async function FormLayout({
|
|||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="flex min-h-screen flex-col flex-1 w-full bg-background text-foreground dark">
|
||||||
<header className="border-b border-border px-6 py-3 flex items-center justify-between">
|
<header className="border-b border-border px-6 py-3 flex items-center justify-between shrink-0">
|
||||||
<span className="font-semibold text-sm">
|
<span className="font-semibold text-sm">
|
||||||
ฟอร์ม
|
ฟอร์ม
|
||||||
</span>
|
</span>
|
||||||
@@ -31,7 +31,9 @@ export default async function FormLayout({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1 p-6 md:p-8 overflow-y-auto max-w-6xl mx-auto w-full">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
{!session && <LoginDialog />}
|
{!session && <LoginDialog />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarFooter,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import { LayoutDashboard, ClipboardList, ArrowLeft, Webhook } from "lucide-react";
|
|
||||||
|
|
||||||
export function AdminSidebar() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sidebar variant="sidebar" collapsible="icon">
|
|
||||||
<SidebarHeader className="border-b border-border py-4 px-4 flex items-center justify-between">
|
|
||||||
<span className="font-bold tracking-wider text-primary text-sm uppercase">
|
|
||||||
Erika Admin
|
|
||||||
</span>
|
|
||||||
</SidebarHeader>
|
|
||||||
|
|
||||||
<SidebarContent>
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild isActive={pathname === "/form/admin"}>
|
|
||||||
<Link href="/form/admin">
|
|
||||||
<LayoutDashboard className="w-4 h-4" />
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/result")}>
|
|
||||||
<Link href="/form/admin/result">
|
|
||||||
<ClipboardList className="w-4 h-4" />
|
|
||||||
<span>Response</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/extra")}>
|
|
||||||
<Link href="/form/admin/extra">
|
|
||||||
<Webhook className="w-4 h-4" />
|
|
||||||
<span>Extra</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
|
|
||||||
<SidebarFooter className="border-t border-border p-4">
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<Link href="/form">
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
<span>Go public</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user