From 3c418952d596b372ff7a3fb40d43f4e9c6f0f51d Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 13 Nov 2025 13:59:14 +0700 Subject: [PATCH] update item nav --- package-lock.json | 32 +++++++++- package.json | 3 +- src/app/page.tsx | 50 +--------------- src/components/home/data-session.tsx | 38 ++++++++++++ src/components/home/item.tsx | 14 ++--- src/components/home/nav-item.tsx | 76 ++++++++++++++++++++++++ src/components/home/sku-list-sidebar.tsx | 69 +++------------------ src/components/ui/pagination.tsx | 28 ++++----- src/stores/item-store.ts | 29 +++++++++ src/type.d.ts | 9 +++ 10 files changed, 213 insertions(+), 135 deletions(-) create mode 100644 src/components/home/data-session.tsx create mode 100644 src/components/home/nav-item.tsx create mode 100644 src/stores/item-store.ts create mode 100644 src/type.d.ts diff --git a/package-lock.json b/package-lock.json index 1b16be5..2791b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "typeorm": "^0.3.27", - "zod": "^4.1.12" + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -8391,6 +8392,35 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 1d2f154..3bbe8c7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "typeorm": "^0.3.27", - "zod": "^4.1.12" + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/app/page.tsx b/src/app/page.tsx index e6933ae..c81f725 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,18 +1,6 @@ +import DataSession from "@/components/home/data-session"; import HomePagination from "@/components/home/home-pagination"; -import Item from "@/components/home/item"; -import SKUListSidebar from "@/components/home/sku-list-sidebar"; -import { Button } from "@/components/ui/button"; -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from "@/components/ui/empty"; -import { Sku } from "@/entities/Sku"; import axios from "@/lib/axios"; -import { Box } from "lucide-react"; import { use } from "react"; const getData = async (query: { page: number; search: string }) => { @@ -39,41 +27,7 @@ export default function Home({ return (
-
- {data?.data?.length > 0 && - (data.data || []).map( - ( - item: Pick< - Sku, - | "id" - | "normalized_title" - | "normalized_short_description" - | "sku" - | "normalized_html" - | "status" - >, - index: number - ) => { - return ; - } - )} - - {data?.data?.length <= 0 && ( - - - - - - No Products - - - )} -
-
-
- -
-
+
diff --git a/src/components/home/data-session.tsx b/src/components/home/data-session.tsx new file mode 100644 index 0000000..101386c --- /dev/null +++ b/src/components/home/data-session.tsx @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Box } from "lucide-react"; +import { Empty, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty"; +import Item from "./item"; +import SKUListSidebar from "./sku-list-sidebar"; + +export interface IDataSessionProps { + data: any; +} + +export default function DataSession({ data }: IDataSessionProps) { + return ( + <> +
+ {data?.data?.length > 0 && + (data.data || []).map((item: ItemType, index: number) => { + return ; + })} + + {data?.data?.length <= 0 && ( + + + + + + No Products + + + )} +
+
+
+ +
+
+ + ); +} diff --git a/src/components/home/item.tsx b/src/components/home/item.tsx index fa4b1ab..36d81aa 100644 --- a/src/components/home/item.tsx +++ b/src/components/home/item.tsx @@ -23,17 +23,10 @@ import { Input } from "../ui/input"; import { Spinner } from "../ui/spinner"; import { Textarea } from "../ui/textarea"; import UncontrolledJoditEditor from "./uncontrolled-jodit-editor"; +import { useItemStore } from "@/stores/item-store"; export interface IItemProps { - data: Pick< - Sku, - | "id" - | "normalized_title" - | "normalized_short_description" - | "normalized_html" - | "sku" - | "status" - >; + data: ItemType; } const ItemSchema = z.object({ @@ -50,6 +43,8 @@ type ItemFormValues = z.infer; export default function Item({ data }: IItemProps) { const [loading, setLoading] = useState(false); + const { markUpdated } = useItemStore(); + const form = useForm({ resolver: zodResolver(ItemSchema), defaultValues: { @@ -65,6 +60,7 @@ export default function Item({ data }: IItemProps) { const res = await axios.get(`skus/${data.id}`); if (res?.data) { form.reset(res.data); + markUpdated(res.data); } } catch (error) { console.log(error); diff --git a/src/components/home/nav-item.tsx b/src/components/home/nav-item.tsx new file mode 100644 index 0000000..137b5db --- /dev/null +++ b/src/components/home/nav-item.tsx @@ -0,0 +1,76 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useItemStore } from "@/stores/item-store"; +import { ChevronRight } from "lucide-react"; +import { useMemo } from "react"; + +export interface INavItemProps { + item: ItemType; + active?: boolean; + onClick?: (data: ItemType) => void; +} + +export default function NavItem({ item, active, onClick }: INavItemProps) { + const { updatedItems } = useItemStore(); + + const dyData = useMemo(() => { + const i = updatedItems.find((j) => j.id === item.id); + + if (!i) return item; + + return i; + }, [updatedItems, item]); + + return ( +
{ + e.preventDefault(); // chặn default jump + // setSelectedSKU(item.sku); + onClick?.(dyData); + const el = document.getElementById(dyData.sku); + if (el) { + el.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + }} + className={`group flex items-start gap-3 px-3 py-3 rounded-lg transition-colors cursor-pointer ${ + // selectedSKU === item.sku + active + ? "bg-primary/10 border border-primary/20" + : "hover:bg-muted border border-transparent" + }`} + > + {/* SKU Badge */} +
+ + {dyData.sku} + +
+ + {/* Content */} +
+
+

+ {dyData.normalized_title} +

+ + {dyData.status} + +
+

+ {dyData.normalized_short_description} +

+
+ + {/* Chevron */} + +
+ ); +} diff --git a/src/components/home/sku-list-sidebar.tsx b/src/components/home/sku-list-sidebar.tsx index e0f3532..6f6afad 100644 --- a/src/components/home/sku-list-sidebar.tsx +++ b/src/components/home/sku-list-sidebar.tsx @@ -1,24 +1,13 @@ "use client"; import { Input } from "@/components/ui/input"; -import { Sku } from "@/entities/Sku"; import { ChevronRight, Search, X } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; import { Spinner } from "../ui/spinner"; +import NavItem from "./nav-item"; -export default function SKUListSidebar({ - data, -}: { - data: Pick< - Sku, - | "id" - | "normalized_title" - | "normalized_short_description" - | "sku" - | "status" - >[]; -}) { +export default function SKUListSidebar({ data }: { data: ItemType[] }) { const searchParams = useSearchParams(); const [searchQuery, setSearchQuery] = useState(""); const [selectedSKU, setSelectedSKU] = useState(null); @@ -78,56 +67,12 @@ export default function SKUListSidebar({