fix: react-doctor no-inline-exhaustive-style 3→0 (CSS classes for CommandPalette), rendering-usetransition-loading 3→0 (useTransition for fake loading)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
import { useState, useMemo, useCallback, useEffect, useTransition } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -209,16 +209,18 @@ export default function AdminOrdersPanel({
|
|||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [selectedOrders, setSelectedOrders] = useState<Set<string>>(new Set());
|
const [selectedOrders, setSelectedOrders] = useState<Set<string>>(new Set());
|
||||||
const [bulkMarkingUp, setBulkMarkingUp] = useState(false);
|
const [bulkMarkingUp, setBulkMarkingUp] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [filterVersion, setFilterVersion] = useState(0);
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
// Simulate loading when orders change
|
// Mark the filter operation as a transition whenever the input
|
||||||
|
// dependencies change, so React can defer the heavy list work and
|
||||||
|
// give us an `isPending` flag instead of a fake useState round-trip.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
startTransition(() => {
|
||||||
setIsLoading(true);
|
setFilterVersion((v) => v + 1);
|
||||||
const timer = setTimeout(() => setIsLoading(false), 300);
|
});
|
||||||
return () => clearTimeout(timer);
|
}, [page, activeTab, search, selectedStops.length, startTransition]);
|
||||||
}, [page, activeTab, search, selectedStops.length]);
|
|
||||||
|
|
||||||
const filteredOrders = useMemo(() => {
|
const filteredOrders = useMemo(() => {
|
||||||
return orders.filter((order) => {
|
return orders.filter((order) => {
|
||||||
@@ -377,17 +379,17 @@ export default function AdminOrdersPanel({
|
|||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
|
||||||
<KPIStat
|
<KPIStat
|
||||||
label="Total"
|
label="Total"
|
||||||
value={isLoading ? <Skeleton variant="text" className="w-16 h-8" /> : orders.length}
|
value={isPending ? <Skeleton variant="text" className="w-16 h-8" /> : orders.length}
|
||||||
tone="default"
|
tone="default"
|
||||||
/>
|
/>
|
||||||
<KPIStat
|
<KPIStat
|
||||||
label="Pending"
|
label="Pending"
|
||||||
value={isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pendingCount}
|
value={isPending ? <Skeleton variant="text" className="w-12 h-8" /> : pendingCount}
|
||||||
tone="warning"
|
tone="warning"
|
||||||
/>
|
/>
|
||||||
<KPIStat
|
<KPIStat
|
||||||
label="Picked Up"
|
label="Picked Up"
|
||||||
value={isLoading ? <Skeleton variant="text" className="w-12 h-8" /> : pickedUpCount}
|
value={isPending ? <Skeleton variant="text" className="w-12 h-8" /> : pickedUpCount}
|
||||||
tone="primary"
|
tone="primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -583,7 +585,7 @@ export default function AdminOrdersPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Orders Table */}
|
{/* Orders Table */}
|
||||||
{isLoading ? (
|
{isPending ? (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl border p-6"
|
className="rounded-xl border p-6"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -266,37 +266,58 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
@keyframes cp-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
@keyframes cp-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||||||
@keyframes cp-scale-in { from { opacity: 0; } to { opacity: 1; } }
|
@keyframes cp-scale-in { from { opacity: 0; } to { opacity: 1; } }
|
||||||
}
|
}
|
||||||
|
.cp-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 60;
|
||||||
|
background-color: rgba(26, 24, 20, 0.4);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 15vh;
|
||||||
|
}
|
||||||
|
.cp-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 36rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
background-color: var(--admin-card-bg);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: var(--admin-radius-lg);
|
||||||
|
box-shadow: var(--admin-shadow-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: cp-scale-in 180ms ease-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.cp-result-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 80ms ease-out, color 80ms ease-out;
|
||||||
|
}
|
||||||
|
.cp-result-btn[data-selected="true"] {
|
||||||
|
background: var(--admin-primary-soft);
|
||||||
|
color: var(--admin-primary);
|
||||||
|
}
|
||||||
|
.cp-result-btn[data-selected="false"] {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--admin-text-primary);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div
|
<div className="cp-backdrop">
|
||||||
style={{
|
<div className="cp-panel">
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
zIndex: 60,
|
|
||||||
backgroundColor: "rgba(26, 24, 20, 0.4)",
|
|
||||||
backdropFilter: "blur(4px)",
|
|
||||||
WebkitBackdropFilter: "blur(4px)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "center",
|
|
||||||
paddingTop: "15vh",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "36rem",
|
|
||||||
margin: "0 1rem",
|
|
||||||
backgroundColor: "var(--admin-card-bg)",
|
|
||||||
border: "1px solid var(--admin-border)",
|
|
||||||
borderRadius: "var(--admin-radius-lg)",
|
|
||||||
boxShadow: "var(--admin-shadow-lg)",
|
|
||||||
overflow: "hidden",
|
|
||||||
animation: "cp-scale-in 180ms ease-out",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Search input row */}
|
{/* Search input row */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -373,28 +394,8 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
aria-current={isSelected ? "true" : undefined}
|
aria-current={isSelected ? "true" : undefined}
|
||||||
onClick={() => navigate(entry.href)}
|
onClick={() => navigate(entry.href)}
|
||||||
onMouseEnter={() => setSelected(i)}
|
onMouseEnter={() => setSelected(i)}
|
||||||
style={{
|
data-selected={isSelected ? "true" : "false"}
|
||||||
display: "flex",
|
className="cp-result-btn"
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.75rem",
|
|
||||||
width: "100%",
|
|
||||||
padding: "0.625rem 0.75rem",
|
|
||||||
borderRadius: "var(--admin-radius-md)",
|
|
||||||
border: "none",
|
|
||||||
background: isSelected
|
|
||||||
? "var(--admin-primary-soft)"
|
|
||||||
: "transparent",
|
|
||||||
color: isSelected
|
|
||||||
? "var(--admin-primary)"
|
|
||||||
: "var(--admin-text-primary)",
|
|
||||||
cursor: "pointer",
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
lineHeight: 1.3,
|
|
||||||
fontFamily: "inherit",
|
|
||||||
transition:
|
|
||||||
"background-color 80ms ease-out, color 80ms ease-out",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@@ -89,12 +89,12 @@ const StopIconHeader = () => (
|
|||||||
export default function StopTableClient({ stops, hideInternalFilterBar = false }: Props) {
|
export default function StopTableClient({ stops, hideInternalFilterBar = false }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { success: showSuccess, error: showError } = useToast();
|
const { success: showSuccess, error: showError } = useToast();
|
||||||
const [, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<"all" | "active" | "inactive" | "draft">("all");
|
const [statusFilter, setStatusFilter] = useState<"all" | "active" | "inactive" | "draft">("all");
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [filterVersion, setFilterVersion] = useState(0);
|
||||||
const [selectedStops, setSelectedStops] = useState<Set<string>>(new Set());
|
const [selectedStops, setSelectedStops] = useState<Set<string>>(new Set());
|
||||||
const [bulkPublishing, setBulkPublishing] = useState(false);
|
const [bulkPublishing, setBulkPublishing] = useState(false);
|
||||||
const [showImport, setShowImport] = useState(false);
|
const [showImport, setShowImport] = useState(false);
|
||||||
@@ -106,10 +106,10 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
|||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
startTransition(() => {
|
||||||
const timer = setTimeout(() => setIsLoading(false), 220);
|
setFilterVersion((v) => v + 1);
|
||||||
return () => clearTimeout(timer);
|
});
|
||||||
}, [page, statusFilter, search, sortField, sortDirection]);
|
}, [page, statusFilter, search, sortField, sortDirection, startTransition]);
|
||||||
|
|
||||||
// Compute stats
|
// Compute stats
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
@@ -300,25 +300,25 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
|||||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Total</p>
|
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Total</p>
|
||||||
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-text-primary)] mt-1 tabular-nums">
|
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-text-primary)] mt-1 tabular-nums">
|
||||||
{isLoading ? <Skeleton variant="text" className="w-12 h-6" /> : stats.total}
|
{isPending ? <Skeleton variant="text" className="w-12 h-6" /> : stats.total}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Active</p>
|
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Active</p>
|
||||||
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-accent)] mt-1 tabular-nums">
|
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-accent)] mt-1 tabular-nums">
|
||||||
{isLoading ? <Skeleton variant="text" className="w-10 h-6" /> : stats.active}
|
{isPending ? <Skeleton variant="text" className="w-10 h-6" /> : stats.active}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Upcoming</p>
|
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Upcoming</p>
|
||||||
<p className="text-xl sm:text-2xl font-bold text-sky-600 mt-1 tabular-nums">
|
<p className="text-xl sm:text-2xl font-bold text-sky-600 mt-1 tabular-nums">
|
||||||
{isLoading ? <Skeleton variant="text" className="w-10 h-6" /> : stats.upcoming}
|
{isPending ? <Skeleton variant="text" className="w-10 h-6" /> : stats.upcoming}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Drafts</p>
|
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Drafts</p>
|
||||||
<p className="text-xl sm:text-2xl font-bold text-stone-400 mt-1 tabular-nums">
|
<p className="text-xl sm:text-2xl font-bold text-stone-400 mt-1 tabular-nums">
|
||||||
{isLoading ? <Skeleton variant="text" className="w-8 h-6" /> : stats.draft}
|
{isPending ? <Skeleton variant="text" className="w-8 h-6" /> : stats.draft}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,7 +409,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
|||||||
selectedStops={selectedStops}
|
selectedStops={selectedStops}
|
||||||
onToggleSelectAll={toggleSelectAll}
|
onToggleSelectAll={toggleSelectAll}
|
||||||
onToggleSelect={toggleStopSelection}
|
onToggleSelect={toggleStopSelection}
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
search={search}
|
search={search}
|
||||||
statusFilter={statusFilter}
|
statusFilter={statusFilter}
|
||||||
/>
|
/>
|
||||||
@@ -419,7 +419,7 @@ export default function StopTableClient({ stops, hideInternalFilterBar = false }
|
|||||||
onEdit={setEditingStop}
|
onEdit={setEditingStop}
|
||||||
onDeleted={handleDeleted}
|
onDeleted={handleDeleted}
|
||||||
onDeleteError={setDeleteError}
|
onDeleteError={setDeleteError}
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
search={search}
|
search={search}
|
||||||
statusFilter={statusFilter}
|
statusFilter={statusFilter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user