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({