From 6b935c9c2259a4cd421fb117580d47fc1df60ee3 Mon Sep 17 00:00:00 2001 From: bagas Date: Wed, 25 Feb 2026 10:26:15 +0700 Subject: [PATCH] update: new style for search toolbox --- app/app.css | 11 +- app/routes/form-responses.tsx | 5 - app/routes/form.tsx | 2 +- app/routes/forms.tsx | 63 ++-------- components/forms/forms-toolbar.tsx | 190 +++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 66 deletions(-) create mode 100644 components/forms/forms-toolbar.tsx diff --git a/app/app.css b/app/app.css index 97b4e54..6759d1b 100644 --- a/app/app.css +++ b/app/app.css @@ -19,7 +19,7 @@ --accent: oklch(0.96 0.005 285); --accent-foreground: oklch(0.30 0.05 300); --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.99 0 0); --border: oklch(0.91 0.005 285); --input: oklch(0.91 0.005 285); --ring: oklch(0.30 0.05 300); @@ -124,8 +124,6 @@ } } -/* ── Animation keyframes ── */ - @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } @@ -172,8 +170,6 @@ 50% { opacity: 0.7; } } -/* ── Utility classes ── */ - .animate-fade-in { animation: fade-in 0.5s ease-out both; } @@ -212,7 +208,6 @@ animation: pulse-soft 2s ease-in-out infinite; } -/* Stagger delays for children */ .stagger-1 { animation-delay: 0.05s; } .stagger-2 { animation-delay: 0.1s; } .stagger-3 { animation-delay: 0.15s; } @@ -222,7 +217,6 @@ .stagger-7 { animation-delay: 0.35s; } .stagger-8 { animation-delay: 0.4s; } -/* Button press effect */ .btn-press { transition: transform 0.15s ease, box-shadow 0.15s ease; } @@ -235,7 +229,6 @@ box-shadow: none; } -/* Card hover lift */ .card-hover { transition: transform 0.25s ease, box-shadow 0.25s ease; } @@ -244,7 +237,6 @@ box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.12); } -/* Smooth loading skeleton */ .skeleton { background: linear-gradient(90deg, var(--muted) 25%, var(--accent) 50%, var(--muted) 75%); background-size: 200% 100%; @@ -252,7 +244,6 @@ border-radius: 0.5rem; } -/* Reduced motion preference */ @media (prefers-reduced-motion: reduce) { .animate-fade-in, .animate-fade-in-up, diff --git a/app/routes/form-responses.tsx b/app/routes/form-responses.tsx index a5e6a6d..710d6c6 100644 --- a/app/routes/form-responses.tsx +++ b/app/routes/form-responses.tsx @@ -129,7 +129,6 @@ export default function FormResponsesPage() { Back to form - {/* Header */}
@@ -152,7 +151,6 @@ export default function FormResponsesPage() {
- {/* Stats */}
} @@ -182,7 +180,6 @@ export default function FormResponsesPage() {
- {/* Tabs */}
- {/* Answers */}
{form.questions.map((question, index) => { const answer = current.answers.find( diff --git a/app/routes/form.tsx b/app/routes/form.tsx index 26cd504..94baa62 100644 --- a/app/routes/form.tsx +++ b/app/routes/form.tsx @@ -206,7 +206,7 @@ export default function FormPreviewPage() { {showDeleteConfirm && (
-

Delete Form

+

Delete Form

Are you sure you want to delete this form? This action cannot be undone. diff --git a/app/routes/forms.tsx b/app/routes/forms.tsx index 0ce46dd..1df7681 100644 --- a/app/routes/forms.tsx +++ b/app/routes/forms.tsx @@ -1,11 +1,11 @@ import { useState, useEffect, useRef, useCallback } from "react" import { useNavigate, Link } from "react-router" -import { Plus, Search, Filter, ArrowUpDown, Loader2 } from "lucide-react" +import { Plus, Loader2 } from "lucide-react" import { Navbar } from "@/components/shared/navbar" import { Footer } from "@/components/shared/footer" -import { FormInput } from "@/components/shared/form-input" import { FormButton } from "@/components/shared/form-button" import { FormCard } from "@/components/forms/form-card" +import { FormsToolbar } from "@/components/forms/forms-toolbar" import { useAuth } from "@/app/context/auth-context" import { getForms } from "@/lib/api" import type { FormSummary } from "@/lib/types" @@ -122,54 +122,17 @@ export default function FormsPage() {

-
-
- - setSearch(e.target.value)} - className="pl-9" - /> -
- -
-
- - -
- -
- - -
- -
- -
-
+
+
diff --git a/components/forms/forms-toolbar.tsx b/components/forms/forms-toolbar.tsx new file mode 100644 index 0000000..8538e0f --- /dev/null +++ b/components/forms/forms-toolbar.tsx @@ -0,0 +1,190 @@ +import { useState, useEffect, useRef } from "react" +import { Search, Filter, ArrowUpDown, Clock, X } from "lucide-react" +import { cn } from "@/lib/utils" + +type StatusFilter = "all" | "has_responses" | "no_responses" +type SortBy = "created_at" | "updated_at" +type SortDir = "newest" | "oldest" + +const STATUS_OPTIONS: { label: string; value: StatusFilter }[] = [ + { label: "All Status", value: "all" }, + { label: "Has Responses", value: "has_responses" }, + { label: "No Responses", value: "no_responses" }, +] + +const SORT_OPTIONS: { label: string; value: SortBy }[] = [ + { label: "Created", value: "created_at" }, + { label: "Updated", value: "updated_at" }, +] + +const ORDER_OPTIONS: { label: string; value: SortDir }[] = [ + { label: "Newest first", value: "newest" }, + { label: "Oldest first", value: "oldest" }, +] + +interface FormsToolbarProps { + search: string + status: StatusFilter + sortBy: SortBy + sortDir: SortDir + onSearchChange: (value: string) => void + onStatusChange: (status: StatusFilter) => void + onSortChange: (sort: SortBy) => void + onOrderChange: (order: SortDir) => void +} + +export function FormsToolbar({ + search, + status, + sortBy, + sortDir, + onSearchChange, + onStatusChange, + onSortChange, + onOrderChange, +}: FormsToolbarProps) { + const [openDropdown, setOpenDropdown] = useState(null) + const toolbarRef = useRef(null) + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (toolbarRef.current && !toolbarRef.current.contains(e.target as Node)) { + setOpenDropdown(null) + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + function toggleDropdown(name: string) { + setOpenDropdown((prev) => (prev === name ? null : name)) + } + + const statusLabel = STATUS_OPTIONS.find((o) => o.value === status)?.label ?? "All Status" + const sortLabel = SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? "Created" + const orderLabel = ORDER_OPTIONS.find((o) => o.value === sortDir)?.label ?? "Newest first" + + return ( +
+
+
+ + onSearchChange(e.target.value)} + className="w-full min-w-0 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" + /> + {search && ( + + )} +
+ +
+
+ +
+ } + label={statusLabel} + open={openDropdown === "status"} + onToggle={() => toggleDropdown("status")} + options={STATUS_OPTIONS} + value={status} + onSelect={(v) => { + onStatusChange(v) + setOpenDropdown(null) + }} + /> + + } + label={`Sort: ${sortLabel}`} + open={openDropdown === "sort"} + onToggle={() => toggleDropdown("sort")} + options={SORT_OPTIONS} + value={sortBy} + onSelect={(v) => { + onSortChange(v) + setOpenDropdown(null) + }} + /> + + } + label={orderLabel} + open={openDropdown === "order"} + onToggle={() => toggleDropdown("order")} + options={ORDER_OPTIONS} + value={sortDir} + onSelect={(v) => { + onOrderChange(v) + setOpenDropdown(null) + }} + /> +
+
+
+ ) +} + +function ToolbarDropdown({ + icon, + label, + open, + onToggle, + options, + value, + onSelect, +}: { + icon: React.ReactNode + label: string + open: boolean + onToggle: () => void + options: { label: string; value: T }[] + value: T + onSelect: (value: T) => void +}) { + return ( +
+ + + {open && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ) +}