diff --git a/docs/admin-settings.md b/docs/admin-settings.md new file mode 100644 index 0000000..c00ac06 --- /dev/null +++ b/docs/admin-settings.md @@ -0,0 +1,271 @@ +# Admin Settings & Site Control Specification + +> Scope: what a **site admin** must be able to control across the fashion e-commerce app. +> This maps admin capabilities to the real surfaces in the codebase (`_store` storefront, +> `user/_authenticated` admin) and flags what exists today vs. what is planned. +> +> Legend: +> - βœ… **Exists** β€” surface/UI is present in the app (data may still be `@faker-js/faker` mock). +> - 🟑 **Partial** β€” UI shell exists but is placeholder/template scaffolding. +> - πŸ”΄ **Planned** β€” no surface yet; needs building (often tied to [roadmap](roadmap.md)). +> +> Related docs: [roles-and-access.md](roles-and-access.md), [roadmap.md](roadmap.md), [conventions.md](conventions.md). + +--- + +## Table of contents + +1. [Organizing model](#1-organizing-model) +2. [Sidebar settings (admin nav)](#2-sidebar-settings-admin-nav) + - [General group](#21-general-group) + - [Pages group](#22-pages-group) + - [Other group](#23-other-group) +3. [Sub-options (settings section)](#3-sub-options-settings-section) +4. [Storefront content control](#4-storefront-content-control) + - [Homepage blocks](#41-homepage-blocks) + - [Global navigation](#42-global-navigation) + - [Footer](#43-footer) + - [Other storefront pages](#44-other-storefront-pages) +5. [Others (site-wide / cross-cutting)](#5-others-site-wide--cross-cutting) +6. [Access control per setting](#6-access-control-per-setting) +7. [Open questions / clearance needed](#7-open-questions--clearance-needed) + +--- + +## 1. Organizing model + +Admin control is grouped into three tiers, matching how the app is structured: + +| Tier | Meaning | Source of truth in code | +| --- | --- | --- | +| **Sidebar** | Top-level admin nav destinations | `src/components/layout/data/sidebar-data.ts` | +| **Sub-options** | Nested items under a sidebar entry (e.g. Settings β†’ Profile) | same file, `items[].items` | +| **Others** | Cross-cutting site settings not tied to one nav item (branding, SEO, payments, roles) | mostly πŸ”΄ not built yet | + +Storefront **content** (homepage, footer, nav) is controlled from the admin but is grouped +separately in Β§4 because it drives customer-facing pages rather than admin dashboards. + +--- + +## 2. Sidebar settings (admin nav) + +The admin sidebar (`sidebar-data.ts`) is the backbone of admin control. Each entry below lists +what the admin should be able to manage from that destination. + +### 2.1 General group + +| Sidebar item | Route | Admin controls | Status | +| --- | --- | --- | --- | +| **Dashboard** | `/user` | View KPIs (sales, orders, revenue, visitors), date-range filter, recent activity, quick actions | βœ… (mock data) | +| **Orders** | β€” (parent) | See sub-options below | βœ… | +| β”œ Orders | `/user/orders` | View/search/filter orders, update fulfillment & payment status, refunds, print invoice, export | βœ… (mock) | +| β”œ Drafts | `/user/drafts` | Create/edit draft orders, convert to order | βœ… (mock) | +| β”” Abandoned Checkouts | `/user/acheckouts` | View abandoned carts, send recovery email/link | βœ… (mock) | +| **Apps** | `/user/apps` | Install/enable integrations (payment, shipping, analytics, marketing) | 🟑 template | +| **Chats** | `/user/chats` | Customer support conversations, canned replies, assign agent | 🟑 template | +| **Products** | β€” (parent) | See sub-options below | βœ… | +| β”œ Products | `/user/products` | CRUD products: title, description, images, price/compare-at, SKU, variants (size/color), tags, category, status (active/draft/archived), SEO fields | βœ… (mock) | +| β”œ Collections | `/user/products/$section` | Group products into collections, manual/automated rules, collection banner & description | βœ… (mock) | +| β”œ Inventory | `/user/products/$section` | Stock levels per variant, low-stock alerts, restock, track/untrack inventory | βœ… (mock) | +| β”” GiftCards | `/user/products/$section` | Issue/manage gift cards, denominations, expiry, balance | βœ… (mock) | +| **Users** | `/user/users` | Manage user accounts: view, role assignment, status (active/invited/suspended), invite, delete | βœ… (mock) | + +> Note: Collections/Inventory/GiftCards currently share the `$section` route β€” they need distinct +> routes or a section param as the product area is built out. + +### 2.2 Pages group + +These are **template/demo** entries (auth screens, error pages) inherited from `shadcn-admin`. +For a real admin they are reference links, not settings. Admin-relevant control here is limited to: + +| Sidebar item | Admin control | Status | +| --- | --- | --- | +| **Auth** (Sign In / Sign Up / Forgot / OTP) | These are storefront/auth UI, not admin settings. Admin may control copy/branding via Β§5 branding. Auth logic is **backend-owned** (see CLAUDE.md). | 🟑 demo | +| **Errors** (401/403/404/500/503) | Optionally: customize error page copy & maintenance-mode toggle (Β§5). | 🟑 demo | + +### 2.3 Other group + +| Sidebar item | Route | Admin controls | Status | +| --- | --- | --- | --- | +| **Settings** | β€” (parent) | See Β§3 sub-options | βœ… | +| **Help Center** | `/user/help-center` | Manage FAQ/help articles shown to customers | 🟑 template | + +--- + +## 3. Sub-options (Settings section) + +Current Settings sub-options (`sidebar-data.ts` β†’ Settings) are **operator/account-scoped**, not +store-scoped. This is the biggest gap: there is no "Store settings" area yet. + +### Existing (account-scoped) β€” βœ… / 🟑 + +| Sub-option | Route | Controls | +| --- | --- | --- | +| **Profile** | `/user/settings` | Admin's own name, bio, avatar, username | +| **Account** | `/user/settings/account` | Email, password change (via backend), language, timezone | +| **Appearance** | `/user/settings/appearance` | Admin UI theme/font (dashboard only) | +| **Notifications** | `/user/settings/notifications` | Which admin notifications to receive (email/in-app) | +| **Display** | `/user/settings/display` | Which dashboard sidebar items/panels are visible | + +### Missing (store-scoped) β€” πŸ”΄ recommended additions + +**Decision:** these will be added as **new sub-options under the existing Settings section** +(not a separate sidebar group), alongside Profile/Account/Appearance/Notifications/Display. + +| Proposed sub-option | Controls | +| --- | --- | +| **General store** | Store name, legal name, contact email/phone, address, business hours, default currency, weight/measurement units | +| **Branding** | Logo (light/dark), favicon, brand colors/theme, storefront font | +| **Homepage / Content** | Manage every homepage block (see Β§4.1) β€” hero slides, banners, featured collections, review carousel, CTA | +| **Navigation** | Storefront top-bar categories & menu structure (see Β§4.2) | +| **Footer** | Company links, policy links, social links, payment badges, copyright (see Β§4.3) | +| **Payments** | Enable/configure payment methods (bKash, Nagad, Rocket, Bank, COD), keys via backend | +| **Shipping & delivery** | Zones, rates, free-shipping thresholds, delivery estimates | +| **Taxes** | Tax rates/regions, tax-inclusive pricing toggle | +| **Checkout** | Required fields, guest checkout, order notes, terms acceptance | +| **Policies / legal** | Edit Privacy, Terms, Return, Refund, Delivery, Cancellation, Warranty pages | +| **SEO & metadata** | Titles/descriptions, Open Graph defaults, sitemap, robots | +| **Analytics & Pixels** | Meta/Facebook Pixel, product catalog feed, GA/other analytics (roadmap item 4) | +| **Roles & permissions** | Site admin / seller / customer roles, per-role access (roadmap item 1) | +| **Notifications (store)** | Customer-facing email templates: order confirm, shipping, abandoned cart | +| **Integrations / Apps** | Third-party connections (overlaps with Apps sidebar) | + +--- + +## 4. Storefront content control + +Everything below is what the admin edits to change **customer-facing** pages. Each block maps to +a real component so the admin UI can target concrete data. + +### 4.1 Homepage blocks + +Source: `src/components/store/features/home/Home.tsx` + `src/assets/*`. + +| # | Block | Component / data source | Admin should control | +| --- | --- | --- | --- | +| 1 | **Hero carousel** | `carousel.tsx` | Slides: image, headline, subtext, CTA text/link, order, active dates | +| 2 | **Side banners** | `SideBanner_Images` (`imageObj`) | Image, alt text, link, order | +| 3 | **Category quick-list** | `Ctg_List` (`cateogryList`) | Icon, label, link, order, visibility | +| 4 | **Popular Categories** | `categoryCarousel.tsx` | Which categories, images, order | +| 5 | **Trending Products** | `trendingProducts.tsx` | Product selection (manual/auto rule), section title | +| 6 | **Latest Offers** | `latestOffersCarousel.tsx` | Offer cards: image, discount, link, validity | +| 7 | **Collection carousels** (Tshirt, Panjabi, …) | `productCarousel.tsx` + `productsTopSellings` | Section title, source collection, "View All" link, order; ability to add/remove sections | +| 8 | **Mid-page banners** | `SideBanner_Images` (reused) | Image, alt, link | +| 9 | **Customer Reviews** | `customerCarousel.tsx` | Review cards: name, avatar, rating, text; show/hide | +| 10 | **Featured Showcase** | `featuredShowcase.tsx` | Featured items/brands, images, links | +| 11 | **"Need Expert Help" CTA** | inline in `Home.tsx` | Heading, body copy, Messenger/WhatsApp links, image, hours text | + +> **Decision:** homepage **section order & presence stay hard-coded** in `Home.tsx`. Admin control +> is limited to editing the **data** feeding each fixed section (slides, banner images/links, +> product selection, review cards, CTA copy). No reorder/add/remove of sections in the admin. + +### 4.2 Global navigation + +| Element | Source | Admin controls | +| --- | --- | --- | +| **Top-bar categories/menu** | `src/components/store/data/navbarData.tsx` (`StoreTopBarData`) | Menu items (Categoriesβ†’Women/Men/Kids/Trend/Shoes/Clothes, Shop, Offers, Stores): labels, children, links, order | +| **Navbar / search / cart** | `shared/navbar.tsx`, `searchWithIcon.tsx`, `shoppingcart.tsx` | Logo, announcement bar, promo message, search placeholder | +| **Mobile nav** | `shared/mobileNav.tsx` | Same items, mobile ordering | + +### 4.3 Footer + +Source: `src/components/store/shared/footer.tsx`. + +| Footer section | Admin controls | +| --- | --- | +| **Brand blurb** | Logo, description text | +| **Social links** | Facebook, Instagram, YouTube, Twitter/X, WhatsApp β€” URL + show/hide | +| **Company links** | About Us, Privacy Policy, Terms & Conditions, Latest News, Contact Us β€” label + link | +| **Policies links** | Delivery, Return, Refund, Cancellation, Warranty β€” label + link | +| **Useful links** | Store Locator, Call Helpline β€” label, action/link | +| **Payment badges** | Bank, bKash, COD, Nagad, Rocket β€” which to display | +| **Copyright / credit** | Copyright text, "Fueled by" credit | + +### 4.4 Other storefront pages + +| Page | Route | Admin controls | Status | +| --- | --- | --- | --- | +| **About** | `_store/about.tsx` | Page copy, images, sections | βœ… page exists | +| **Category listing** | `_store/category.$name.tsx` | Which products, sort/filter defaults, banner | βœ… | +| **Product detail** | `_store/$singleproduct.tsx`, `products.$productCtg.$productName.tsx` | Product data (via Products Β§2.1), related products, reviews | βœ… | +| **Wishlist** | `_store/whishlist.tsx` | (customer-owned; admin has no content to edit) | βœ… | +| **Checkout** | `_store/checkout.tsx` | Fields, payment/shipping options (via Β§3 Checkout/Payments) | βœ… | +| **Menus** | `_store/$menus.tsx` | Dynamic menu/landing pages content | βœ… | + +--- + +## 5. Others (site-wide / cross-cutting) + +Settings that don't belong to a single sidebar item. Most are πŸ”΄ planned. + +| Area | What admin controls | Status | +| --- | --- | --- | +| **Branding & theme** | Logo variants, favicon, brand colors, storefront font, `rounded-theme` radius | πŸ”΄ | +| **SEO** | Per-page title/description, canonical, sitemap, robots.txt | πŸ”΄ | +| **Open Graph / social** | OG image/title/description defaults per page (roadmap: FB pages) | πŸ”΄ | +| **Meta / Facebook Pixel** | Pixel ID, catalog feed, event mapping (roadmap item 4) | πŸ”΄ | +| **Web analytics** | GA / other provider IDs, consent handling | πŸ”΄ | +| **Payments** | Enable methods, gateway keys (via backend), currency | πŸ”΄ | +| **Shipping / delivery** | Zones, rates, thresholds, estimates | πŸ”΄ | +| **Taxes** | Rates, regions, inclusive/exclusive | πŸ”΄ | +| **Legal / policy pages** | Content for all footer policy links | πŸ”΄ | +| **Roles & access** | site admin / seller / customer, route guards (roadmap item 1; see [roles-and-access.md](roles-and-access.md)) | πŸ”΄ | +| **Maintenance mode** | Toggle storefront maintenance/503 with custom message | πŸ”΄ | +| **Localization** | Language(s), currency display, RTL | πŸ”΄ | +| **Notifications (transactional)** | Customer email/SMS templates & triggers | πŸ”΄ | +| **Data & privacy** | Cookie consent, data export/delete requests | πŸ”΄ | + +--- + +## 6. Access control per setting + +Per roadmap item 1, once the **site admin / seller / customer** model lands, settings should be +scoped. Suggested default matrix (to confirm): + +| Setting area | Site admin | Seller | Customer | +| --- | --- | --- | --- | +| Store branding, homepage, footer, nav | βœ… full | ❌ | ❌ | +| Payments, shipping, taxes, roles | βœ… full | ❌ | ❌ | +| Own products & inventory | βœ… all | βœ… own only | ❌ | +| Own orders/fulfillment | βœ… all | βœ… own only | view own | +| Analytics/Pixel | βœ… full | 🟑 own store scope? | ❌ | +| Account/profile | βœ… | βœ… | βœ… | + +> The **seller** scope (own-products-only) is the key data-boundary concern β€” see the +> `access-reviewer` guidance before wiring seller-scoped queries. + +### 6.1 Settings are role-differentiated (different sets, shared components) + +**Decision:** each user type sees a **different set** of Settings sub-options. The Settings surface +is not one flat list β€” the set shown is selected by role. What is shared across roles is the +**scaffolding**, not the items: `ContentSection`, the settings sidebar-nav, the react-hook-form + zod +form primitives, the persisted-store + route pattern. Build each role's items on those shared parts. + +| Settings set | Who sees it | Sub-options | +| --- | --- | --- | +| **Account** (shared) | admin, seller, customer | Profile, Account, Appearance, Notifications, Display β€” the existing sub-options; generic to any logged-in user | +| **Store** (admin) | site admin only | General store, Branding, Homepage, Navigation, Footer, Policies (+ future: Payments, Shipping, Taxes, SEO, Pixel, Roles) β€” the plans in `plans/` build these | +| **Seller** | seller only | Shop profile, payout/bank details, own-shipping defaults, seller notifications (πŸ”΄ future) | +| **Customer** | customer only | Addresses, saved payment methods, order/marketing preferences (πŸ”΄ future) β€” mostly overlaps Account | + +Implication for implementation: the settings nav lists (`sidebar-data.ts` Settings children and +`features/settings/index.tsx` `sidebarNavItems`) should become **role-derived** when the role model +lands (roadmap item 1). Until then, the admin "Store" set is added as an identifiable, contiguous +block, and no role guard is built. See `plans/001` "Cross-cutting decisions" for exactly how the +plans keep this forward-compatible. + +--- + +## 7. Decisions & remaining clearance + +**Confirmed:** +- **Store Settings home** β†’ new **sub-options under the existing Settings section** (not a new sidebar group). +- **Doc purpose** β†’ becomes a **build plan** (see companion plan below / `improve` output). +- **Homepage model** β†’ sections stay **hard-coded**; admin edits only the **data** per fixed section. + +**Still open (confirm before build):** +1. **Seller access** β€” For the seller role, which settings (if any) beyond own products/orders are + in scope (e.g. their own analytics, storefront sub-page)? +2. **Backend contract** β€” Which settings are backend-owned vs. stored client-side? Per CLAUDE.md, + no backend should be invented; settings persistence needs an API contract before build. +``` diff --git a/plans/001-store-content-foundation-and-general.md b/plans/001-store-content-foundation-and-general.md new file mode 100644 index 0000000..a7bc956 --- /dev/null +++ b/plans/001-store-content-foundation-and-general.md @@ -0,0 +1,503 @@ +# Plan 001: Store-content store + "General Store" settings sub-option (reference implementation) + +> **Executor instructions**: Follow this plan step by step. Run every verification +> command and confirm the expected result before moving to the next step. If anything in +> "STOP conditions" occurs, stop and report β€” do not improvise. When done, update the +> status row for this plan in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 9add86d..HEAD -- src/features/settings src/routes/user/_authenticated/settings src/stores src/components/layout/data` +> If any in-scope file below changed since this plan was written, compare the "Current state" +> excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: M +- **Risk**: LOW +- **Depends on**: none +- **Category**: direction / dx +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +The admin dashboard has a Settings section, but every sub-option today is operator-scoped +(the admin's own profile/account/theme). There is no way for an admin to control **store-level** +settings, and the storefront reads all its content from hard-coded static modules. This plan +builds the foundation for admin-editable storefront content: a single persisted Zustand store +(seeded from the current static data so nothing breaks), plus the first real sub-option +("General Store"). It establishes the exact route + feature-folder + settings-nav + form + test +pattern that plans 002–006 will copy for Branding, Homepage, Navigation, Footer, and Policies. + +## Current state + +The app is a client-side SPA (no backend). State uses Zustand (`src/stores/authStore.ts`) and +React context. Settings sub-pages are simple react-hook-form + zod forms that currently just +`toast` the submitted JSON. This plan makes forms write to a persisted store instead. + +**There are TWO navs to update for a sub-option β€” this is important.** + +1. **Main app left sidebar** β€” `src/components/layout/data/sidebar-data.ts`. "Settings" is a + collapsible parent whose `items` array lists Profile/Account/Appearance/Notifications/Display, + each `{ title, url, icon }`. This is the SAME mechanism by which "Abandoned Checkouts" is a + child of "Orders". A new sub-option must be added here to appear in the left sidebar. Excerpt + (`sidebar-data.ts`, the Settings entry under the "Other" group): + ```ts + { + title: 'Settings', + icon: IconSettings, + items: [ + { title: 'Profile', url: '/user/settings', icon: IconUserCog }, + { title: 'Account', url: '/user/settings/account', icon: IconTool }, + { title: 'Appearance', url: '/user/settings/appearance', icon: IconPalette }, + { title: 'Notifications', url: '/user/settings/notifications', icon: IconNotification }, + { title: 'Display', url: '/user/settings/display', icon: IconBrowserCheck }, + ], + } + ``` + > NOTE: these `url`s use the FULL `/user/settings/...` prefix (unlike the settings-page inner nav + > below, which is prefix-less). `icon` here is a component reference (`IconTool`), NOT JSX β€” the + > renderer does `` (see `src/components/layout/nav-group.tsx:111`). Import the icon + > from `@tabler/icons-react` at the top of `sidebar-data.ts` and pass the bare component. + +2. **The Settings page's own inner nav** β€” `src/features/settings/index.tsx`. + It renders a sidebar-nav + ``. The nav items array (lines 52–78) is what we extend: + +```tsx +const sidebarNavItems = [ + { title: 'Profile', icon: , href: '/settings' }, + { title: 'Account', icon: , href: '/settings/account' }, + { title: 'Appearance', icon: , href: '/settings/appearance' }, + { title: 'Notifications', icon: , href: '/settings/notifications' }, + { title: 'Display', icon: , href: '/settings/display' }, +] +``` + +> NOTE: the `href` values here are written WITHOUT the `/user` prefix (e.g. `/settings/account`), +> even though the real route is `/user/settings/account`. `SidebarNav` navigates with these exact +> strings. **Match this existing (prefix-less) convention** β€” do not "fix" it in this plan. + +**A sub-option feature** is a folder under `src/features/settings//` with an `index.tsx` +that wraps a form in `ContentSection`. Example β€” `src/features/settings/notifications/index.tsx`: + +```tsx +import ContentSection from '../components/content-section' +import { NotificationsForm } from './notifications-form' + +export default function SettingsNotifications() { + return ( + + + + ) +} +``` + +**The form pattern** β€” `src/features/settings/notifications/notifications-form.tsx` uses +`useForm` + `zodResolver`, shadcn `Form*` primitives, and on submit calls `toast(...)`. Use it as +the structural template (imports, `FormField`/`FormItem`/`FormLabel`/`FormControl`/`FormMessage`, +a trailing ` + + + ) +} +``` + +**Verify**: file compiles in the Step 4 build. (No standalone command yet.) + +### Step 3: Create the sub-option index + route + +Create `src/features/settings/general/index.tsx`: + +```tsx +import ContentSection from '../components/content-section' +import { GeneralForm } from './general-form' + +export default function SettingsGeneral() { + return ( + + + + ) +} +``` + +Create `src/routes/user/_authenticated/settings/general.lazy.tsx`: + +```tsx +import { createLazyFileRoute } from '@tanstack/react-router' +import SettingsGeneral from '@/features/settings/general' + +export const Route = createLazyFileRoute( + '/user/_authenticated/settings/general' +)({ component: SettingsGeneral }) +``` + +**Verify**: `npx vite build` β†’ exit 0, then +`grep -n "settings/general" src/routeTree.gen.ts` β†’ at least one match (route registered). + +### Step 4: Add the nav item to BOTH navs + +General Store is the FIRST item of the admin **Store** block (see "Access scope"). So it also carries +the block-label / role-filter attach-point comment; later plans (002–006) append their items right +after it, keeping the Store block contiguous. + +**4a β€” Left sidebar** (`src/components/layout/data/sidebar-data.ts`): import an icon (e.g. +`IconBuildingStore`) into the existing `@tabler/icons-react` import block, and add a child to the END +of the Settings collapsible's `items` array (after `Display`). Use the FULL url and a bare icon +component (not JSX), preceded by the block-label comment: + +```ts +// --- Store settings (site-admin only) --- TODO(roadmap-1): filter this block by role +{ title: 'General store', url: '/user/settings/general', icon: IconBuildingStore }, +``` + +**4b β€” Settings inner nav** (`src/features/settings/index.tsx`): add an icon import and one entry to +`sidebarNavItems`, after `Display`. Here the `icon` IS JSX and the `href` is prefix-less (match the +existing entries in this file exactly), preceded by the mirror comment: + +```tsx +// --- Store settings (site-admin only) --- TODO(roadmap-1): filter this block by role +{ title: 'General store', icon: , href: '/settings/general' }, +``` + +**Verify**: `pnpm build` β†’ exit 0. Then `pnpm lint` β†’ exit 0. Then `pnpm format` (rewrites files; +exit 0). Manually confirm "General store" appears BOTH in the left sidebar under Settings and in the +Settings page's own nav column. + +### Step 5: Write the store test (the pattern for later plans) + +Create `src/stores/storeContentStore.test.ts`. Vitest + jsdom is configured (`pnpm test`). +Test that the store defaults are present and that `setGeneral` updates state. This file is the +structural template plans 002–006 copy for their slices. + +```tsx +import { describe, expect, it } from 'vitest' +import { useStoreContentStore } from './storeContentStore' + +describe('storeContentStore β€” general slice', () => { + it('exposes sensible defaults', () => { + const { general } = useStoreContentStore.getState() + expect(general.storeName).toBeTruthy() + expect(general.weightUnit).toBe('kg') + }) + + it('updates the general slice via setGeneral', () => { + const next = { + storeName: 'Test Store', + contactEmail: 'a@b.co', + contactPhone: '123', + currency: 'USD', + weightUnit: 'lb' as const, + } + useStoreContentStore.getState().setGeneral(next) + expect(useStoreContentStore.getState().general.storeName).toBe('Test Store') + }) +}) +``` + +> If an existing test in the repo shows a different import/setup style for Vitest, follow that +> instead. Search: `grep -rn "from 'vitest'" src` to find any existing example first. + +**Verify**: `pnpm test` β†’ all pass, including the 2 new tests. + +### Step 6: Manual smoke check (optional but recommended) + +Start `pnpm dev`, open `/user/settings/general`, change the store name, click Save, reload the +page β€” the field should retain the new value (persisted to localStorage under key `store-content`). +Stop the dev server when done. + +## Test plan + +- New file `src/stores/storeContentStore.test.ts` covering: defaults exist (happy path) and + `setGeneral` mutates state (the write path every later plan relies on). +- Structural pattern: the store test above (self-contained). If the repo already has a Vitest + test, mirror its imports/setup. +- Verification: `pnpm test` β†’ all pass including 2 new tests. + +## Done criteria + +ALL must hold: + +- [ ] `pnpm build` exits 0 (tsc + vite). +- [ ] `pnpm lint` exits 0. +- [ ] `pnpm test` exits 0; the 2 new store tests pass. +- [ ] `grep -n "settings/general" src/routeTree.gen.ts` returns β‰₯1 match. +- [ ] `/user/settings/general` renders the General Store form; Save persists across reload. +- [ ] "General store" appears under Settings in the left sidebar (`grep -n "settings/general" src/components/layout/data/sidebar-data.ts` β†’ 1 match) AND in the Settings inner nav. +- [ ] No storefront file under `src/components/store/**` was modified (`git status`). +- [ ] `plans/README.md` status row for 001 updated to DONE. + +## STOP conditions + +Stop and report (do not improvise) if: + +- The excerpts in "Current state" don't match live code (drift since commit `9add86d`). +- `zustand/middleware` `persist` is not importable (unexpected β€” zustand v5 is installed); + report rather than swapping in a different persistence approach. +- `tsc` rejects the route path string in `general.lazy.tsx` even after `npx vite build` + regenerated `routeTree.gen.ts` β€” this signals a routing-config change; report it. +- Adding the sub-option appears to require touching an out-of-scope file. + +## Cross-cutting decisions (apply to plans 001–006) + +These resolve the two questions left open in `docs/admin-settings.md Β§7`. They are DEFAULTS chosen +to match the roadmap; the operator may override, but build to these unless told otherwise. + +### Access scope β€” settings are role-differentiated; these plans build the ADMIN set + +**Design fact:** each user type (site admin / seller / customer) sees a **different set** of Settings +sub-options. The Settings surface is not one flat list shared by everyone β€” the set shown is selected +by role. What IS shared across roles is the **scaffolding**, and you must reuse it, not fork it: + +- `src/features/settings/components/content-section.tsx` (`ContentSection`) +- `src/features/settings/components/sidebar-nav.tsx` (`SidebarNav`) +- the react-hook-form + zod form primitives (`@/components/ui/form`, etc.) +- the persisted-store + lazy-route pattern this plan establishes + +The three sets: + +| Set | Who | Sub-options | +| --- | --- | --- | +| **Account** (shared) | admin, seller, customer | Profile, Account, Appearance, Notifications, Display β€” already exist | +| **Store** (admin-only) | site admin | General, Branding, Homepage, Navigation, Footer, Policies β€” **plans 001–006 build these** | +| **Seller** | seller only | shop profile, payout, own-shipping, seller notifications β€” πŸ”΄ future, NOT in these plans | +| **Customer** | customer only | addresses, payment methods, order/marketing prefs β€” πŸ”΄ future, NOT in these plans | + +So plans 001–006 add the **Store (admin-only)** set. Sellers and customers get their own future sets +that will REUSE the same scaffolding β€” do not build them here, and do not assume the admin items are +visible to every user. + +**Do NOT build a role guard in these plans.** The real role model + route guards don't exist yet +(roadmap item 1; see `docs/roles-and-access.md`); a half-guard now would conflict with it. Instead, +keep this forward-compatible so the future role filter is a clean change: + +1. **Keep the Store items as one contiguous, labeled block.** In BOTH nav lists, add the admin Store + sub-options as a contiguous run (do not interleave them with the Account items), immediately after + the Account items. This way a future role filter can select the block by range/tag trivially. +2. **Leave a single attach-point breadcrumb.** In `src/features/settings/index.tsx`, directly above + the first Store item you add to `sidebarNavItems`, add the comment: + `// --- Store settings (site-admin only) --- TODO(roadmap-1): filter this block by role` + and add the mirror comment above the first Store child in the Settings collapsible in + `src/components/layout/data/sidebar-data.ts`. +3. Do not add any auth/role-check code anywhere in plans 001–006. + +If the operator says sellers or customers SHOULD get some of these specific store settings, STOP and +get the exact per-role list before building β€” do not guess a split. + +### Backend ownership β€” client-side now, `@/api/*` later + +There is no backend. Every settings slice is **client-side-persisted** via `useStoreContentStore` +(localStorage key `store-content`) for now. The migration seam to a real backend is deliberate and +uniform: + +- Each store slice (`general`, `branding`, `homepage`, `navigation`, `footer`, `policies`) maps 1:1 + to a future settings endpoint under the reserved `@/api/*` alias (e.g. `GET/PUT /api/settings/general`). +- When a backend contract exists: replace the selector reads with TanStack Query reads and the + `set*` writes with mutations behind `@/api/*`; the store becomes an optimistic cache or is removed. +- Do NOT add axios or `@/api/*` calls in plans 001–006 β€” no contract exists yet. Keep the slice + shapes clean and serializable so they map directly onto future JSON payloads. + +## Maintenance notes + +- **Store settings are admin-only by intent** (see Cross-cutting decisions); no guard is built yet β€” + cover this sub-option when roadmap-item-1 route guards land. +- **SSR migration (roadmap item 2, TanStack Start):** this store persists to `localStorage`, + which is unavailable on the server. When SSR lands, gate persistence to the client or move this + content to a real backend behind `@/api/*`. Flag any use of `useStoreContentStore` during SSR. +- The store `name: 'store-content'` is the localStorage key; changing it drops saved admin edits. +- Reviewers should confirm later plans **add sibling slices** to this store rather than creating + separate stores β€” one persisted store keyed `store-content` is the intended design. +- `@/api/*` remains reserved for the future backend; do not add data-fetching here. diff --git a/plans/002-branding-settings.md b/plans/002-branding-settings.md new file mode 100644 index 0000000..bbdb0a1 --- /dev/null +++ b/plans/002-branding-settings.md @@ -0,0 +1,187 @@ +# Plan 002: Branding settings sub-option + +> **Executor instructions**: Follow step by step; run every verification command; honor STOP +> conditions. Update this plan's row in `plans/README.md` when done. +> +> **Drift check (run first)**: `git diff --stat 9add86d..HEAD -- src/features/settings src/stores src/components/store/shared src/components/layout/data` +> Also confirm plan 001 has landed (see "Depends on"). On excerpt mismatch, STOP. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: LOW +- **Depends on**: plans/001-store-content-foundation-and-general.md +- **Category**: direction +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +Admins should control store branding β€” logo and brand color β€” without editing code. This adds a +Branding sub-option that persists brand values and wires the storefront navbar/footer logo to read +from the store. It is the smallest storefront-consuming slice, so it validates the +"admin edits β†’ storefront reflects" loop end to end. + +## Prerequisite from plan 001 (must already exist) + +- `src/stores/storeContentStore.ts` exporting `useStoreContentStore` (Zustand + `persist`, + localStorage key `store-content`) plus per-slice selector hooks. +- The sub-option pattern: `src/features/settings//{index.tsx,-form.tsx}` + route file + `src/routes/user/_authenticated/settings/.lazy.tsx` + a nav entry in + `src/features/settings/index.tsx`. +- The route-tree regeneration workflow: run `npx vite build` (regenerates + `src/routeTree.gen.ts`) BEFORE `pnpm build`. + +If any prerequisite is missing, STOP β€” plan 001 is not done. + +## Current state + +Storefront logo is hard-coded. Footer β€” `src/components/store/shared/footer.tsx:12-16`: + +```tsx +Logo +``` + +The navbar logo lives in `src/components/store/shared/navbar.tsx` (search for `` wrapped, or a plain text `Input` validated +as hex), `defaultValues` from `useBrandingSettings()`, submit calls `useSetBrandingSettings()` + +`toast`. + +Create `src/features/settings/branding/index.tsx` (copy `general/index.tsx`, retitle +"Branding" / "Logo, favicon and brand color."). + +Create `src/routes/user/_authenticated/settings/branding.lazy.tsx` (copy `general.lazy.tsx`, swap +path to `/user/_authenticated/settings/branding` and component to `SettingsBranding`). + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "settings/branding" src/routeTree.gen.ts` β†’ β‰₯1 match. + +### Step 3: Add the nav item to BOTH navs + +Plan 001 established that a Settings sub-option must be registered in two places (see plan 001 +"Current state" for the full explanation). Append this item to the END of both lists, after the +existing admin **Store** block items (keep that block contiguous; do NOT re-add the +`// --- Store settings ---` label comment β€” plan 001 already added it): + +- **Left sidebar** β€” `src/components/layout/data/sidebar-data.ts`, add a child to the Settings + collapsible's `items` (full url, bare icon component): + `{ title: 'Branding', url: '/user/settings/branding', icon: IconPhoto }` (import `IconPhoto` into + the existing `@tabler/icons-react` import block). +- **Settings inner nav** β€” `src/features/settings/index.tsx`, add (JSX icon, prefix-less href): + `{ title: 'Branding', icon: , href: '/settings/branding' }`. + +### Step 4: Wire storefront logos to the store + +In `src/components/store/shared/footer.tsx`, replace the hard-coded logo `src` with the store value: + +```tsx +const branding = useBrandingSettings() +// ... +Logo +``` + +Do the equivalent in `src/components/store/shared/navbar.tsx` using `branding.logoLight`. Keep all +existing className / layout untouched β€” change only the `src`. Import the selector from +`@/stores/storeContentStore`. + +**Verify**: `pnpm build` β†’ exit 0; storefront still renders the same logos (defaults equal the old +paths). + +### Step 5: Format, lint, test + +Extend `src/stores/storeContentStore.test.ts` with a branding block (defaults present; `setBranding` +updates). Then: + +**Verify**: `pnpm format` β†’ exit 0; `pnpm lint` β†’ exit 0; `pnpm test` β†’ all pass; `pnpm build` β†’ exit 0. + +## Test plan + +- Add to `storeContentStore.test.ts`: `branding` defaults exist; `setBranding` updates `logoLight`. +- Verification: `pnpm test` β†’ all pass. + +## Done criteria + +- [ ] `pnpm build`, `pnpm lint`, `pnpm test` all exit 0. +- [ ] `grep -n "settings/branding" src/routeTree.gen.ts` β†’ β‰₯1 match. +- [ ] "Branding" appears under Settings in the left sidebar (`grep -n "settings/branding" src/components/layout/data/sidebar-data.ts` β†’ 1 match) AND in the Settings inner nav. +- [ ] `/user/settings/branding` renders; changing logo path + Save changes the storefront logo after reload. +- [ ] Footer/navbar logos read from the store (no hard-coded logo `src` remains in those two files: + `grep -n "nexf-white-logo" src/components/store/shared/footer.tsx` β†’ no match). +- [ ] `plans/README.md` row for 002 β†’ DONE. + +## STOP conditions + +- Plan 001 artifacts missing (store or pattern absent). +- The navbar logo isn't a simple `` you can repoint β€” report what you found instead. +- Excerpts don't match live code (drift). + +## Maintenance notes + +- **Admin-only by intent** and **client-side-persisted now** β€” see plan 001 "Cross-cutting + decisions". Do not add role guards or `@/api/*` calls here; cover this sub-option when + roadmap-item-1 route guards land. +- **Deferred:** applying `brandColor` to the Tailwind v4 theme (`src/index.css`). Storing it now is + cheap; theming is a separate change with visual-regression risk. +- Logo paths point at `public/` assets; uploading new images is out of scope (no backend). Admins + can only select among existing public paths until an upload/asset service exists. diff --git a/plans/003-homepage-content-settings.md b/plans/003-homepage-content-settings.md new file mode 100644 index 0000000..3bd5eec --- /dev/null +++ b/plans/003-homepage-content-settings.md @@ -0,0 +1,234 @@ +# Plan 003: Homepage content settings sub-option + +> **Executor instructions**: Follow step by step; run every verification command; honor STOP +> conditions. Update this plan's row in `plans/README.md` when done. +> +> **Drift check (run first)**: +> `git diff --stat 9add86d..HEAD -- src/features/settings src/stores src/assets src/components/store/features/home src/components/layout/data` +> Confirm plan 001 has landed. On excerpt mismatch, STOP. + +## Status + +- **Priority**: P1 +- **Effort**: L +- **Risk**: MED +- **Depends on**: plans/001-store-content-foundation-and-general.md +- **Category**: direction +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +The homepage is the store's shop window and today it is 100% hard-coded β€” some data lives in +`src/assets/*` modules, and some is inlined directly inside components. This plan gives the admin +control over the **data** feeding the fixed homepage sections (hero slides, side banners, trust-badge +strip, review cards, and the "Need Expert Help" CTA copy) while keeping section order/presence +hard-coded in `Home.tsx` (per product decision). It is the highest-value content plan and the +largest, because it must first EXTRACT inlined data into the store. + +## Prerequisite from plan 001 (must already exist) + +`src/stores/storeContentStore.ts` with the `persist`ed store + selector-hook pattern, the sub-option +folder/route/nav pattern, and the `npx vite build` β†’ `pnpm build` route-tree workflow. If missing, STOP. + +## Current state β€” exact data sources per section + +`src/components/store/features/home/Home.tsx` composes fixed sections. The editable data: + +1. **Hero carousel** β€” `Carousel_Images` in `src/assets/imageObj.ts:1-22` (array of `{name, href}`; + `href` is a `public/` image path). Consumed by + `src/components/store/features/home/carousel.tsx:3,33-34`. +2. **Side banners** β€” `SideBanner_Images` in `src/assets/imageObj.ts:24-33` (`{name, href}`). + Consumed in `Home.tsx:24-33` and `Home.tsx:99-108`, and concatenated in `carousel.tsx:33`. +3. **Trust-badge strip** β€” `Ctg_List` in `src/assets/cateogryList.tsx:1-22` (`{name, icon}`). + Consumed in `Home.tsx:39-48`. +4. **Review cards** β€” **hard-coded INSIDE the component**: the `latestOffers` array in + `src/components/store/features/home/customerCarousel.tsx:12-53` (`{name, itemreviewed, userpic, + description, stars}`). This must be extracted to the store. +5. **CTA copy** β€” **inlined JSX** in `Home.tsx:128-158`: heading "Need Expert Help", the paragraph, + and Messenger/WhatsApp button labels. Extract the text fields (heading, body, messengerUrl, + whatsappUrl) to the store. + +> Everything above uses `{ href }` / `{ icon }` / `{ userpic }` string paths under `public/`. +> Keep that shape. Uploading new images is out of scope (no backend); admins edit paths/text. + +Scope guard: do NOT make the "Tshirt Collection" / "Panjabi Collection" product carousels editable +here β€” those read `productsTopSellings` and belong with product/collection management, not homepage +content. Leave `Home.tsx:70-96` untouched. + +## Commands you will need + +| Purpose | Command | Expected | +|---------|---------|----------| +| Regenerate route tree | `npx vite build` | exit 0; updates `src/routeTree.gen.ts` | +| Build + typecheck | `pnpm build` | exit 0 | +| Lint | `pnpm lint` | exit 0 | +| Format | `pnpm format` | exit 0 | +| Tests | `pnpm test` | all pass | + +## Scope + +**In scope:** +- `src/stores/storeContentStore.ts` (edit β€” add `homepage` slice + selectors) +- `src/features/settings/homepage/index.tsx` (create) +- `src/features/settings/homepage/homepage-form.tsx` (create) +- `src/routes/user/_authenticated/settings/homepage.lazy.tsx` (create) +- `src/features/settings/index.tsx` (edit β€” add inner-nav item) +- `src/components/layout/data/sidebar-data.ts` (edit β€” add child under Settings collapsible) +- `src/components/store/features/home/carousel.tsx` (edit β€” read hero + side banners from store) +- `src/components/store/features/home/customerCarousel.tsx` (edit β€” read reviews from store; remove inlined array) +- `src/components/store/features/home/Home.tsx` (edit β€” side banners, trust strip, CTA copy from store) +- `src/stores/storeContentStore.test.ts` (edit β€” add homepage assertions) + +**Out of scope:** +- `Home.tsx:70-96` product carousels and `productsTopSellings` in `imageObj.ts`. +- Reordering/adding/removing homepage sections (order stays hard-coded). +- `categoryCarousel.tsx`, `trendingProducts.tsx`, `latestOffersCarousel.tsx`, + `featuredShowcase.tsx`, `productTabs.tsx` β€” not in this plan. +- Deleting the original `src/assets/imageObj.ts` / `cateogryList.tsx` exports (other code imports + them; see STOP conditions). Seed store defaults FROM them; leave the modules in place. + +## Git workflow + +- Branch: `advisor/003-homepage-content`. Conventional commit, e.g. + `feat(settings): add homepage content sub-option`. No `Co-Authored-By` trailer. + +## Steps + +### Step 1: Add the `homepage` slice to the store + +In `src/stores/storeContentStore.ts`, add types mirroring the current data shapes exactly: + +```tsx +export interface HeroSlide { name: string; href: string } +export interface SideBanner { name: string; href: string } +export interface TrustBadge { name: string; icon: string } +export interface ReviewCard { + name: string; itemreviewed: string; userpic: string; description: string; stars: string +} +export interface HomepageCta { + heading: string; body: string; messengerUrl: string; whatsappUrl: string +} +export interface HomepageSettings { + heroSlides: HeroSlide[] + sideBanners: SideBanner[] + trustBadges: TrustBadge[] + reviews: ReviewCard[] + cta: HomepageCta +} +``` + +Seed `defaultHomepage` by IMPORTING the current arrays so defaults stay in sync: +`import { Carousel_Images, SideBanner_Images } from '@/assets/imageObj'` and +`import { Ctg_List } from '@/assets/cateogryList'`; for `reviews`, copy the `latestOffers` array +from `customerCarousel.tsx:12-53` into a new `defaultReviews` const here (it is not exported +anywhere yet). For `cta`, copy the current strings from `Home.tsx:131-147`. + +Add `homepage`/`setHomepage` to state + `useHomepageSettings`/`useSetHomepageSettings` selectors. + +**Verify**: part of Step 5 build. + +### Step 2: Rewire the storefront consumers to read from the store + +- `carousel.tsx`: replace `import { Carousel_Images, SideBanner_Images } from '@/assets/imageObj'` + usage with `const { heroSlides, sideBanners } = useHomepageSettings()`. Keep the mobile-concat + logic identical: `const mblCarosuel_Images = heroSlides.concat(sideBanners)`. +- `customerCarousel.tsx`: delete the inlined `latestOffers` array (lines 12-53) and read + `const reviews = useHomepageSettings()` β†’ map over `reviews` instead. Keep all JSX/markup identical. +- `Home.tsx`: read `const { sideBanners, trustBadges, cta } = useHomepageSettings()` and use them in + the two `SideBanner_Images.map` blocks (lines 24-33, 99-108), the `Ctg_List.map` block + (lines 39-48), and the CTA text (lines 131-147). Do NOT touch the product-carousel sections. + +Import selectors from `@/stores/storeContentStore`. Do not change any className / layout. + +**Verify**: `pnpm build` β†’ exit 0; storefront homepage renders identically to before (defaults equal +old data). + +### Step 3: Homepage settings form + +Create `src/features/settings/homepage/homepage-form.tsx`, modeled on plan 001's `general-form.tsx`. +This form is larger β€” it edits arrays. Keep it pragmatic: + +- Use `react-hook-form`'s `useFieldArray` for `heroSlides`, `sideBanners`, `trustBadges`, `reviews` + (add/remove/edit rows of simple text inputs). Import `useFieldArray` from `react-hook-form`. +- The `cta` object is four plain `Input`s. +- `defaultValues` from `useHomepageSettings()`; submit calls `useSetHomepageSettings()` + `toast`. +- Group the sections with the existing `

` heading style + seen in `notifications-form.tsx:102` so it reads as sub-sections. + +If `useFieldArray` proves too large to complete reliably, it is acceptable to ship editing for +`heroSlides`, `sideBanners`, `trustBadges`, and `cta`, and render `reviews` read-only with a +`// TODO` β€” but note this in the PR and README status. Do NOT silently drop a section without a note. + +**Verify**: part of Step 5 build. + +### Step 4: Index + route + nav item + +- Create `src/features/settings/homepage/index.tsx` (copy plan 001 `general/index.tsx`; title + "Homepage", desc "Hero slides, banners, trust badges, reviews and CTA."). +- Create `src/routes/user/_authenticated/settings/homepage.lazy.tsx` (copy `general.lazy.tsx`; path + `/user/_authenticated/settings/homepage`, component `SettingsHomepage`). +- Register the sub-option in BOTH navs (see plan 001 "Current state" for why). Append to the END of + both lists, after the existing admin **Store** block items (keep it contiguous; do NOT re-add the + `// --- Store settings ---` label comment β€” plan 001 added it): + - Left sidebar `src/components/layout/data/sidebar-data.ts` β€” add to the Settings collapsible's + `items`: `{ title: 'Homepage', url: '/user/settings/homepage', icon: IconLayoutDashboard }` + (import `IconLayoutDashboard` in the `@tabler/icons-react` block β€” note it may already be + imported for the Dashboard entry; reuse it, don't duplicate the import). + - Settings inner nav `src/features/settings/index.tsx` β€” add + `{ title: 'Homepage', icon: , href: '/settings/homepage' }`. + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "settings/homepage" src/routeTree.gen.ts` β†’ β‰₯1 match; +`grep -n "settings/homepage" src/components/layout/data/sidebar-data.ts` β†’ 1 match. + +### Step 5: Format, lint, test, build + +Extend `storeContentStore.test.ts` with a homepage block (defaults non-empty; `setHomepage` updates +`heroSlides`). Then: + +**Verify**: `pnpm format` β†’ exit 0; `pnpm lint` β†’ exit 0; `pnpm test` β†’ all pass; `pnpm build` β†’ exit 0. + +### Step 6: Manual smoke check + +`pnpm dev` β†’ `/user/settings/homepage`: add a hero slide (path e.g. `nexf/banner/banner-3.jpg`), +Save, open `/` (storefront home) β†’ the new slide appears. Reload β†’ persists. Stop dev server. + +## Test plan + +- `storeContentStore.test.ts`: homepage defaults have β‰₯1 hero slide and β‰₯1 review; `setHomepage` + updates the slice. +- Structural pattern: plan 001's store test. +- Verification: `pnpm test` β†’ all pass. + +## Done criteria + +- [ ] `pnpm build`, `pnpm lint`, `pnpm test` all exit 0. +- [ ] `grep -n "settings/homepage" src/routeTree.gen.ts` β†’ β‰₯1 match. +- [ ] `grep -n "const latestOffers" src/components/store/features/home/customerCarousel.tsx` β†’ no match + (inlined array removed / relocated to store). +- [ ] "Homepage" appears under Settings in the left sidebar AND in the Settings inner nav. +- [ ] Homepage renders identically with default data; editing hero slides in settings changes `/`. +- [ ] `Home.tsx:70-96` product carousels unchanged (`git diff` shows no edits to those lines). +- [ ] `plans/README.md` row for 003 β†’ DONE (note any read-only `reviews` fallback if used). + +## STOP conditions + +- Plan 001 artifacts missing. +- `Carousel_Images`, `SideBanner_Images`, or `Ctg_List` are imported by files OTHER than the home + components and the store (check: `grep -rn "Carousel_Images\|SideBanner_Images\|Ctg_List" src`). + If a non-home consumer exists, do NOT remove/relocate the source arrays β€” seed defaults from them + and leave them; report the extra consumer. +- Excerpts don't match live code (drift): e.g. `latestOffers` no longer at + `customerCarousel.tsx:12-53`. +- The `useFieldArray` editor cannot be completed for reviews AND you cannot fall back cleanly β€” + report rather than shipping a broken form. + +## Maintenance notes + +- **Admin-only by intent** and **client-side-persisted now** β€” see plan 001 "Cross-cutting + decisions". No role guards or `@/api/*` calls here; cover when roadmap-item-1 guards land. +- Section order stays in `Home.tsx` by design; a future "reorderable sections" feature would + supersede this and require a different model β€” do not add it here. +- Defaults are seeded from `src/assets/*`; if those source arrays change later, store defaults only + update for users who clear localStorage `store-content`. Consider a "reset to defaults" button as + a follow-up. +- SSR migration caveat applies (localStorage) β€” see plan 001 maintenance notes. diff --git a/plans/004-navigation-settings.md b/plans/004-navigation-settings.md new file mode 100644 index 0000000..f1c3965 --- /dev/null +++ b/plans/004-navigation-settings.md @@ -0,0 +1,169 @@ +# Plan 004: Navigation (top-nav menu) settings sub-option + +> **Executor instructions**: Follow step by step; run every verification command; honor STOP +> conditions. Update this plan's row in `plans/README.md` when done. +> +> **Drift check (run first)**: +> `git diff --stat 9add86d..HEAD -- src/features/settings src/stores src/components/store/data src/components/layout/data` +> Confirm plan 001 has landed. On excerpt mismatch, STOP. + +## Status + +- **Priority**: P2 +- **Effort**: M +- **Risk**: LOW +- **Depends on**: plans/001-store-content-foundation-and-general.md +- **Category**: direction +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +The storefront top-bar menu (Categories β†’ Women/Men/Kids/…, Shop, Offers, Stores) is hard-coded. +Admins should be able to edit menu labels and their children without touching code. This plan makes +that menu data admin-editable via a Navigation sub-option. + +## Prerequisite from plan 001 (must already exist) + +The `persist`ed `storeContentStore` + selector pattern, the sub-option folder/route/nav pattern, and +the `npx vite build` β†’ `pnpm build` route-tree workflow. If missing, STOP. + +## Current state β€” exact data source + +`src/components/store/data/navbarData.tsx` exports `StoreTopBarData`: + +```tsx +export const StoreTopBarData: { title: string; children?: { title: string }[] }[] = [ + { title: 'Categories', children: [ + { title: 'Women' }, { title: 'Men' }, { title: 'Kids' }, + { title: 'Trend' }, { title: 'Shoes' }, { title: 'Clothes' } ] }, + { title: 'Shop' }, + { title: 'Offers' }, + { title: 'Stores' }, +] +``` + +Find its consumers before editing: `grep -rn "StoreTopBarData" src`. It is rendered by the +storefront nav components (likely `src/components/store/shared/secondNav.tsx` / +`categoriesNav.tsx` / `mobileNav.tsx`). This plan seeds a store slice from `StoreTopBarData` and +repoints consumers to read from the store. + +## Commands you will need + +| Purpose | Command | Expected | +|---------|---------|----------| +| Find consumers | `grep -rn "StoreTopBarData" src` | list of files | +| Regenerate route tree | `npx vite build` | exit 0 | +| Build + typecheck | `pnpm build` | exit 0 | +| Lint / Format / Tests | `pnpm lint` / `pnpm format` / `pnpm test` | exit 0 / exit 0 / all pass | + +## Scope + +**In scope:** +- `src/stores/storeContentStore.ts` (edit β€” add `navigation` slice + selectors) +- `src/features/settings/navigation/index.tsx` (create) +- `src/features/settings/navigation/navigation-form.tsx` (create) +- `src/routes/user/_authenticated/settings/navigation.lazy.tsx` (create) +- `src/features/settings/index.tsx` (edit β€” add inner-nav item) +- `src/components/layout/data/sidebar-data.ts` (edit β€” add child under Settings collapsible) +- Every storefront file that imports `StoreTopBarData` (edit β€” read from store instead) +- `src/stores/storeContentStore.test.ts` (edit β€” add navigation assertions) + +**Out of scope:** +- The navbar search / cart / logo (those are branding/other concerns). +- Linking menu items to real category routes (menu items are label-only today β€” keep that; do not + invent `href`s). Adding hrefs is a documented follow-up. +- Deleting `navbarData.tsx` (seed defaults from it; leave the module). + +## Git workflow + +- Branch: `advisor/004-navigation-settings`. Conventional commit, e.g. + `feat(settings): add navigation menu sub-option`. No `Co-Authored-By` trailer. + +## Steps + +### Step 1: Add the `navigation` slice + +In `src/stores/storeContentStore.ts`, mirror the existing type exactly: + +```tsx +export interface NavMenuItem { title: string; children?: { title: string }[] } +export interface NavigationSettings { topBar: NavMenuItem[] } +``` + +Seed `defaultNavigation` by importing the source: +`import { StoreTopBarData } from '@/components/store/data/navbarData'` β†’ `{ topBar: StoreTopBarData }`. +Add `navigation`/`setNavigation` + `useNavigationSettings`/`useSetNavigationSettings` selectors. + +> Cross-boundary import note: importing a `components/store/**` data module into a store is +> acceptable for seeding defaults (it is data, not a storefront component). If lint's import rules +> object, inline a copy of the array as `defaultTopBar` in the store instead and note it. + +### Step 2: Repoint storefront consumers + +For each file from `grep -rn "StoreTopBarData" src`, replace the static import with +`const { topBar } = useNavigationSettings()` (import from `@/stores/storeContentStore`) and map over +`topBar`. Keep all markup/classNames identical. + +**Verify**: `pnpm build` β†’ exit 0; storefront menu renders identically to before. + +### Step 3: Navigation form + +Create `src/features/settings/navigation/navigation-form.tsx` modeled on plan 001's +`general-form.tsx`, using `useFieldArray` (from `react-hook-form`) for the top-level `topBar` items. +Each item: a `title` `Input` plus a nested editable list of child titles. A nested field array is +involved; if full nested add/remove proves too large to complete reliably, ship editing of top-level +titles + child titles as text (comma-joined) and note the simplification in the PR/README. Do NOT +silently drop child editing. + +`defaultValues` from `useNavigationSettings()`; submit calls `useSetNavigationSettings()` + `toast`. + +### Step 4: Index + route + nav item + +- `src/features/settings/navigation/index.tsx` (copy plan 001 `general/index.tsx`; title + "Navigation", desc "Storefront top-bar menu items."). +- `src/routes/user/_authenticated/settings/navigation.lazy.tsx` (copy `general.lazy.tsx`; path + `/user/_authenticated/settings/navigation`, component `SettingsNavigation`). +- Register in BOTH navs (see plan 001 "Current state"). Append to the END of both lists, after the + existing admin **Store** block items (keep it contiguous; do NOT re-add the label comment): + - Left sidebar `src/components/layout/data/sidebar-data.ts` β€” add to Settings `items`: + `{ title: 'Navigation', url: '/user/settings/navigation', icon: IconMenu2 }` (import `IconMenu2`). + - Settings inner nav `src/features/settings/index.tsx` β€” + `{ title: 'Navigation', icon: , href: '/settings/navigation' }`. + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "settings/navigation" src/routeTree.gen.ts` β†’ β‰₯1 match; +`grep -n "settings/navigation" src/components/layout/data/sidebar-data.ts` β†’ 1 match. + +### Step 5: Format, lint, test, build + +Extend `storeContentStore.test.ts` (navigation defaults non-empty; `setNavigation` updates `topBar`). + +**Verify**: `pnpm format` / `pnpm lint` β†’ exit 0; `pnpm test` β†’ all pass; `pnpm build` β†’ exit 0. + +## Test plan + +- `storeContentStore.test.ts`: navigation defaults include β‰₯4 top items; `setNavigation` updates. +- Verification: `pnpm test` β†’ all pass. + +## Done criteria + +- [ ] `pnpm build`, `pnpm lint`, `pnpm test` all exit 0. +- [ ] `grep -n "settings/navigation" src/routeTree.gen.ts` β†’ β‰₯1 match. +- [ ] No storefront file still imports `StoreTopBarData` directly except the store's default seed + (`grep -rn "StoreTopBarData" src` β†’ only `navbarData.tsx` and `storeContentStore.ts`). +- [ ] "Navigation" appears under Settings in the left sidebar AND in the Settings inner nav. +- [ ] Editing a menu label in settings changes the storefront top bar after reload. +- [ ] `plans/README.md` row for 004 β†’ DONE. + +## STOP conditions + +- Plan 001 artifacts missing. +- `StoreTopBarData` has a different shape than the excerpt (drift). +- A consumer renders the menu in a way that isn't a simple `.map` you can repoint β€” report it. + +## Maintenance notes + +- **Admin-only by intent** and **client-side-persisted now** β€” see plan 001 "Cross-cutting + decisions". No role guards or `@/api/*` calls here; cover when roadmap-item-1 guards land. +- Menu items are label-only (no routing). Wiring items to category routes is a deliberate follow-up + and interacts with the category route `src/routes/_store/category.$name.tsx`. +- SSR/localStorage caveat applies β€” see plan 001 maintenance notes. diff --git a/plans/005-footer-settings.md b/plans/005-footer-settings.md new file mode 100644 index 0000000..86c2ae9 --- /dev/null +++ b/plans/005-footer-settings.md @@ -0,0 +1,174 @@ +# Plan 005: Footer settings sub-option + +> **Executor instructions**: Follow step by step; run every verification command; honor STOP +> conditions. Update this plan's row in `plans/README.md` when done. +> +> **Drift check (run first)**: +> `git diff --stat 9add86d..HEAD -- src/features/settings src/stores src/components/store/shared/footer.tsx src/components/layout/data` +> Confirm plan 001 has landed. On excerpt mismatch, STOP. + +## Status + +- **Priority**: P2 +- **Effort**: M +- **Risk**: LOW +- **Depends on**: plans/001-store-content-foundation-and-general.md +- **Category**: direction +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +The storefront footer hard-codes company links, policy links, social URLs, payment badges, and the +copyright line β€” all with placeholder `href='/'`. Admins should manage these without editing code. +This plan extracts the footer content into the store and adds a Footer sub-option to edit it. + +## Prerequisite from plan 001 (must already exist) + +The `persist`ed `storeContentStore` + selector pattern, the sub-option folder/route/nav pattern, and +the `npx vite build` β†’ `pnpm build` route-tree workflow. If missing, STOP. + +## Current state β€” `src/components/store/shared/footer.tsx` + +All content is inlined JSX (no data module): +- **Brand blurb** paragraph β€” lines 19-24. +- **Social links** (Facebook, Instagram, YouTube, Twitter/X, WhatsApp) β€” `
  • ` blocks lines 26-86, + each an `` + ``. +- **Company links** (About Us, Privacy Policy, Terms & Conditions, Latest News, Contact Us) β€” + lines 90-127, each ``. +- **Policies links** (Delivery, Return, Refund, Cancellation, Warranty) β€” lines 130-167. +- **Useful links** (Store Locator, Call Helpline buttons) β€” lines 169-193. +- **Copyright + credit + payment badges** (Bank, bKash, COD, Nagad, Rocket) β€” lines 197-214. + +There is no `footer` data module yet; this plan creates the store slice and rewires the component. + +## Commands you will need + +| Purpose | Command | Expected | +|---------|---------|----------| +| Regenerate route tree | `npx vite build` | exit 0 | +| Build + typecheck | `pnpm build` | exit 0 | +| Lint / Format / Tests | `pnpm lint` / `pnpm format` / `pnpm test` | exit 0 / exit 0 / all pass | + +## Scope + +**In scope:** +- `src/stores/storeContentStore.ts` (edit β€” add `footer` slice + selectors) +- `src/features/settings/footer/index.tsx` (create) +- `src/features/settings/footer/footer-form.tsx` (create) +- `src/routes/user/_authenticated/settings/footer.lazy.tsx` (create) +- `src/features/settings/index.tsx` (edit β€” add inner-nav item) +- `src/components/layout/data/sidebar-data.ts` (edit β€” add child under Settings collapsible) +- `src/components/store/shared/footer.tsx` (edit β€” read content from store) +- `src/stores/storeContentStore.test.ts` (edit β€” add footer assertions) + +**Out of scope:** +- The footer logo `src` (owned by plan 002 Branding; if 002 already changed it, leave that as-is). +- Payment icon image files themselves (edit which badges show, by path β€” not upload). +- Restyling the footer layout. + +## Git workflow + +- Branch: `advisor/005-footer-settings`. Conventional commit, e.g. + `feat(settings): add footer content sub-option`. No `Co-Authored-By` trailer. + +## Steps + +### Step 1: Add the `footer` slice + +In `src/stores/storeContentStore.ts`: + +```tsx +export interface FooterLink { label: string; href: string } +export interface FooterSocial { name: string; href: string; icon: string } // icon: path under public/ +export interface FooterPaymentBadge { name: string; icon: string; enabled: boolean } +export interface FooterSettings { + blurb: string + socials: FooterSocial[] + companyLinks: FooterLink[] + policyLinks: FooterLink[] + paymentBadges: FooterPaymentBadge[] + copyright: string + credit: string +} +``` + +Seed `defaultFooter` by copying the CURRENT values out of `footer.tsx` (labels, the `/nexf/...` icon +paths, `href: '/'`, copyright "Β© 2026 NEXF Lifestyle Limited. All Rights Reserved.", credit +"Fueled by CyberSpace Digital"). Read them from the file β€” do not guess. Add `footer`/`setFooter` + +`useFooterSettings`/`useSetFooterSettings` selectors. + +### Step 2: Rewire `footer.tsx` to read from the store + +Replace each inlined list with a `.map` over the store slice: +`const footer = useFooterSettings()` (import from `@/stores/storeContentStore`), then map +`footer.socials`, `footer.companyLinks`, `footer.policyLinks`, `footer.paymentBadges` (render only +`enabled` ones), and interpolate `footer.blurb` / `footer.copyright` / `footer.credit`. Keep every +className and wrapper element identical β€” change only the data source. Leave the "Useful links" +buttons (Store Locator / Call Helpline) as-is unless trivially data-driven; they are out of scope. + +**Verify**: `pnpm build` β†’ exit 0; footer renders identically to before (defaults equal old content). + +### Step 3: Footer form + +Create `src/features/settings/footer/footer-form.tsx` modeled on plan 001's `general-form.tsx`, using +`useFieldArray` for `socials`, `companyLinks`, `policyLinks`, `paymentBadges` (label/href/path text +inputs; `enabled` as a `Switch` for payment badges β€” see `notifications-form.tsx` for `Switch` +usage), plus `Input`s for `blurb`, `copyright`, `credit`. `defaultValues` from `useFooterSettings()`; +submit calls `useSetFooterSettings()` + `toast`. Group with `

    ` +section headings. + +### Step 4: Index + route + nav item + +- `src/features/settings/footer/index.tsx` (copy plan 001 `general/index.tsx`; title "Footer", desc + "Footer links, socials, payment badges and copyright."). +- `src/routes/user/_authenticated/settings/footer.lazy.tsx` (copy `general.lazy.tsx`; path + `/user/_authenticated/settings/footer`, component `SettingsFooter`). +- Register in BOTH navs (see plan 001 "Current state"). Append to the END of both lists, after the + existing admin **Store** block items (keep it contiguous; do NOT re-add the label comment). Pick an + icon that exists in `@tabler/icons-react` (e.g. `IconLayoutBottombar`; if unavailable, `IconColumns`): + - Left sidebar `src/components/layout/data/sidebar-data.ts` β€” add to Settings `items`: + `{ title: 'Footer', url: '/user/settings/footer', icon: IconLayoutBottombar }`. + - Settings inner nav `src/features/settings/index.tsx` β€” + `{ title: 'Footer', icon: , href: '/settings/footer' }`. + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "settings/footer" src/routeTree.gen.ts` β†’ β‰₯1 match; +`grep -n "settings/footer" src/components/layout/data/sidebar-data.ts` β†’ 1 match. + +### Step 5: Format, lint, test, build + +Extend `storeContentStore.test.ts` (footer defaults non-empty; `setFooter` updates `copyright`). + +**Verify**: `pnpm format` / `pnpm lint` β†’ exit 0; `pnpm test` β†’ all pass; `pnpm build` β†’ exit 0. + +## Test plan + +- `storeContentStore.test.ts`: footer defaults include β‰₯1 social and β‰₯1 payment badge; `setFooter` + updates the slice. +- Verification: `pnpm test` β†’ all pass. + +## Done criteria + +- [ ] `pnpm build`, `pnpm lint`, `pnpm test` all exit 0. +- [ ] `grep -n "settings/footer" src/routeTree.gen.ts` β†’ β‰₯1 match. +- [ ] Footer content comes from the store (`grep -n "Fueled by CyberSpace" src/components/store/shared/footer.tsx` + β†’ no match; the string now lives in the store default). +- [ ] "Footer" appears under Settings in the left sidebar AND in the Settings inner nav. +- [ ] Editing copyright in settings changes the storefront footer after reload. +- [ ] `plans/README.md` row for 005 β†’ DONE. + +## STOP conditions + +- Plan 001 artifacts missing. +- `footer.tsx` structure differs materially from the excerpt (drift). +- Rewiring the footer would require restyling to keep it visually identical β€” report instead of + restyling (out of scope). + +## Maintenance notes + +- **Admin-only by intent** and **client-side-persisted now** β€” see plan 001 "Cross-cutting + decisions". No role guards or `@/api/*` calls here; cover when roadmap-item-1 guards land. +- **Interacts with plan 006 (Policies):** once 006 adds the `/policy/$slug` route, the footer + `policyLinks` `href`s should point at those slugs (e.g. `/policy/return-policy`) instead of `'/'`. + If 006 is already done, set the default policy-link hrefs accordingly. +- Payment badge / social icons are `public/` paths; uploading new icons needs a backend (out of scope). +- SSR/localStorage caveat applies β€” see plan 001 maintenance notes. diff --git a/plans/006-policies-settings.md b/plans/006-policies-settings.md new file mode 100644 index 0000000..f3de80f --- /dev/null +++ b/plans/006-policies-settings.md @@ -0,0 +1,191 @@ +# Plan 006: Policies settings sub-option + storefront render route + +> **Executor instructions**: Follow step by step; run every verification command; honor STOP +> conditions. Update this plan's row in `plans/README.md` when done. +> +> **Drift check (run first)**: +> `git diff --stat 9add86d..HEAD -- src/features/settings src/stores src/routes/_store src/components/layout/data` +> Confirm plan 001 has landed. On excerpt mismatch, STOP. + +## Status + +- **Priority**: P3 +- **Effort**: M +- **Risk**: LOW +- **Depends on**: plans/001-store-content-foundation-and-general.md +- **Category**: direction +- **Planned at**: commit `9add86d`, 2026-07-20 + +## Why this matters + +The footer links to legal/policy pages (Privacy, Terms, Return, Refund, Delivery, Cancellation, +Warranty) but none of that content exists and the links go to `'/'`. Admins need to edit policy text, +and the storefront needs a page to display it. This plan stores policy documents (title + body) and +adds a single dynamic storefront route `/policy/$slug` that renders the selected policy. + +## Prerequisite from plan 001 (must already exist) + +The `persist`ed `storeContentStore` + selector pattern, the sub-option folder/route/nav pattern, and +the `npx vite build` β†’ `pnpm build` route-tree workflow. If missing, STOP. + +## Current state β€” storefront routes + +Storefront routes live under `src/routes/_store/` and use `createFileRoute` (non-lazy) with dynamic +params, e.g. `src/routes/_store/category.$name.tsx` and `src/routes/_store/$singleproduct.tsx`. The +storefront layout route is `src/routes/_store/route.tsx`. Model the new policy route on +`category.$name.tsx`'s structure (a `createFileRoute('/_store/policy/$slug')` with a component that +reads the `$slug` param). Read `category.$name.tsx` first to copy its exact param-reading idiom +(`Route.useParams()`). + +There is no policies data anywhere today β€” this plan creates it in the store. + +## Commands you will need + +| Purpose | Command | Expected | +|---------|---------|----------| +| Read routing idiom | open `src/routes/_store/category.$name.tsx` | note `useParams` usage | +| Regenerate route tree | `npx vite build` | exit 0 | +| Build + typecheck | `pnpm build` | exit 0 | +| Lint / Format / Tests | `pnpm lint` / `pnpm format` / `pnpm test` | exit 0 / exit 0 / all pass | + +## Scope + +**In scope:** +- `src/stores/storeContentStore.ts` (edit β€” add `policies` slice + selectors) +- `src/features/settings/policies/index.tsx` (create) +- `src/features/settings/policies/policies-form.tsx` (create) +- `src/routes/user/_authenticated/settings/policies.lazy.tsx` (create) +- `src/features/settings/index.tsx` (edit β€” add inner-nav item) +- `src/components/layout/data/sidebar-data.ts` (edit β€” add child under Settings collapsible) +- `src/routes/_store/policy.$slug.tsx` (create β€” storefront render page) +- `src/stores/storeContentStore.test.ts` (edit β€” add policies assertions) + +**Out of scope:** +- Rich-text / markdown rendering. Store and render plain text (preserve line breaks with + `whitespace-pre-line`). A markdown editor is a follow-up. +- Editing footer `policyLinks` hrefs β€” that belongs to plan 005 (note the interaction there). +- SEO metadata for policy pages (roadmap item, separate). + +## Git workflow + +- Branch: `advisor/006-policies-settings`. Conventional commit, e.g. + `feat(settings): add policies sub-option and storefront policy page`. No `Co-Authored-By` trailer. + +## Steps + +### Step 1: Add the `policies` slice + +In `src/stores/storeContentStore.ts`: + +```tsx +export interface PolicyDoc { slug: string; title: string; body: string } +export interface PoliciesSettings { docs: PolicyDoc[] } +``` + +Seed `defaultPolicies.docs` with the seven policies referenced by the footer, using kebab-case slugs +matching the footer labels: `privacy-policy`, `terms-and-conditions`, `delivery-policy`, +`return-policy`, `refund-policy`, `cancellation-policy`, `warranty-policy`. Give each a `title` and a +short placeholder `body` (e.g. `'Edit this policy in Settings β†’ Policies.'`). Add +`policies`/`setPolicies` + `usePoliciesSettings`/`useSetPoliciesSettings` selectors. + +### Step 2: Storefront policy render route + +Create `src/routes/_store/policy.$slug.tsx` following `category.$name.tsx`'s structure: + +```tsx +import { createFileRoute, notFound } from '@tanstack/react-router' +import { useStoreContentStore } from '@/stores/storeContentStore' + +export const Route = createFileRoute('/_store/policy/$slug')({ + component: PolicyPage, +}) + +function PolicyPage() { + const { slug } = Route.useParams() + const doc = useStoreContentStore((s) => s.policies.docs.find((d) => d.slug === slug)) + if (!doc) return
    Policy not found.
    + return ( +
    +

    {doc.title}

    +

    {doc.body}

    +
    + ) +} +``` + +> Match the exact `createFileRoute` param idiom you observed in `category.$name.tsx` β€” if it reads +> params differently (e.g. a loader), follow that. Drop the `notFound` import if unused (strict +> `noUnusedLocals`). + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "policy/\$slug\|policy/\\$slug" src/routeTree.gen.ts` +β†’ β‰₯1 match (route registered). + +### Step 3: Policies form + index + settings route + +- `src/features/settings/policies/policies-form.tsx` modeled on plan 001's `general-form.tsx`, using + `useFieldArray` over `docs`; each doc = `title` `Input` + `body` `Textarea` + (`@/components/ui/textarea`; if that primitive is absent, add it via the shadcn workflow or use a + multi-line `Input` and note it). `slug` shown read-only. `defaultValues` from `usePoliciesSettings()`; + submit calls `useSetPoliciesSettings()` + `toast`. +- `src/features/settings/policies/index.tsx` (copy plan 001 `general/index.tsx`; title "Policies", + desc "Edit legal and policy pages."). +- `src/routes/user/_authenticated/settings/policies.lazy.tsx` (copy `general.lazy.tsx`; path + `/user/_authenticated/settings/policies`, component `SettingsPolicies`). + +**Verify**: `npx vite build` β†’ exit 0; `grep -n "settings/policies" src/routeTree.gen.ts` β†’ β‰₯1 match. + +### Step 4: Nav item (BOTH navs) + +See plan 001 "Current state" for why two navs are updated. Append to the END of both lists, after the +existing admin **Store** block items (keep it contiguous; do NOT re-add the label comment). Import +`IconFileText` from `@tabler/icons-react` where needed. + +- Left sidebar `src/components/layout/data/sidebar-data.ts` β€” add to Settings `items`: + `{ title: 'Policies', url: '/user/settings/policies', icon: IconFileText }`. +- Settings inner nav `src/features/settings/index.tsx` β€” + `{ title: 'Policies', icon: , href: '/settings/policies' }`. + +**Verify**: `grep -n "settings/policies" src/components/layout/data/sidebar-data.ts` β†’ 1 match. + +### Step 5: Format, lint, test, build + +Extend `storeContentStore.test.ts` (policies defaults include all 7 slugs; `setPolicies` updates a body). + +**Verify**: `pnpm format` / `pnpm lint` β†’ exit 0; `pnpm test` β†’ all pass; `pnpm build` β†’ exit 0. + +### Step 6: Manual smoke check + +`pnpm dev` β†’ `/user/settings/policies`: edit "Return Policy" body, Save. Open +`/policy/return-policy` β†’ the edited text shows. Reload β†’ persists. Stop dev server. + +## Test plan + +- `storeContentStore.test.ts`: policies defaults contain the 7 expected slugs; `setPolicies` updates + a doc body. +- Verification: `pnpm test` β†’ all pass. + +## Done criteria + +- [ ] `pnpm build`, `pnpm lint`, `pnpm test` all exit 0. +- [ ] `grep -n "settings/policies" src/routeTree.gen.ts` β†’ β‰₯1 match AND policy `$slug` route registered. +- [ ] "Policies" appears under Settings in the left sidebar AND in the Settings inner nav. +- [ ] `/policy/return-policy` renders the stored Return Policy; editing it in settings updates the page after reload. +- [ ] `plans/README.md` row for 006 β†’ DONE. + +## STOP conditions + +- Plan 001 artifacts missing. +- `category.$name.tsx` uses a routing idiom incompatible with the excerpt (e.g. different param API) + β€” follow the real idiom; if unclear, report. +- No `Textarea` primitive and adding one is non-trivial β€” fall back to multi-line `Input` and note it. + +## Maintenance notes + +- **Admin-only by intent** and **client-side-persisted now** β€” see plan 001 "Cross-cutting + decisions". No role guards or `@/api/*` calls here; cover when roadmap-item-1 guards land. (The + public `/policy/$slug` render route is customer-facing and intentionally unguarded.) +- **Interacts with plan 005 (Footer):** footer `policyLinks` hrefs should point at `/policy/` + for these to be reachable from the storefront. Coordinate the slugs (they match the footer labels). +- Plain-text bodies only; a markdown/rich-text editor and per-policy SEO metadata are deliberate + follow-ups (roadmap: SEO/metadata). +- SSR/localStorage caveat applies β€” see plan 001 maintenance notes. diff --git a/plans/README.md b/plans/README.md new file mode 100644 index 0000000..472930c --- /dev/null +++ b/plans/README.md @@ -0,0 +1,75 @@ +# Implementation Plans β€” Admin control over store settings & storefront content + +Generated by the improve skill on 2026-07-20 (planned against commit `9add86d`). +Spec: [docs/admin-settings.md](../docs/admin-settings.md). + +Goal: give the **site admin** editable control over store-level settings and the +customer-facing storefront **data**, delivered as **new sub-options under the existing +Settings section** (`/user/settings/*`). No backend exists β€” content is persisted +client-side via a Zustand store seeded from the current static data modules, so admin +edits reflect on the storefront in the same browser. The `@/api/*` alias is reserved +for a future real backend. + +**Decided constraints (do not deviate):** +- New settings live as sub-options of the existing Settings section, NOT a new sidebar group. +- Homepage **section order/presence stays hard-coded** in `Home.tsx`; admin edits only the + DATA feeding each fixed section. +- Keep the `data/` / static-module mock pattern. Do not invent a backend or add axios calls. +- **Access:** Settings are **role-differentiated** β€” admin, seller, and customer each get a + DIFFERENT set of sub-options, sharing the same scaffolding (`ContentSection`, `SidebarNav`, the + form primitives, the store/route pattern). These plans build the **admin "Store" set** (General, + Branding, Homepage, Navigation, Footer, Policies); the shared Account set already exists; seller + and customer sets are future work reusing the same parts. The Store items are added as one + contiguous, labeled block so a future role filter selects them cleanly. No role guard is built yet + (roadmap item 1); plans leave a `TODO(roadmap-1)` attach point. Full rationale in plan 001 + "Cross-cutting decisions". +- **Backend ownership:** everything is **client-side-persisted now** via a Zustand `persist` store; + each slice maps 1:1 to a future `@/api/settings/*` endpoint. No axios/`@/api/*` calls yet. + +**Each sub-option is registered in TWO navs** (both edited by every plan): +1. The **main left sidebar** β€” a child under the "Settings" collapsible in + `src/components/layout/data/sidebar-data.ts` (full `/user/settings/...` url, bare icon component). + This is the exact mechanism used for "Abandoned Checkouts" under "Orders". +2. The **Settings page's own inner nav** β€” `sidebarNavItems` in `src/features/settings/index.tsx` + (prefix-less `/settings/...` href, JSX icon). + +## Execution order & status + +| Plan | Title | Priority | Effort | Depends on | Status | +|------|-------|----------|--------|------------|--------| +| 001 | Store-content store + General Store sub-option (reference impl) | P1 | M | β€” | DONE | +| 002 | Branding settings sub-option | P2 | S | 001 | TODO | +| 003 | Homepage content settings sub-option | P1 | L | 001 | TODO | +| 004 | Navigation (top-nav menu) settings sub-option | P2 | M | 001 | TODO | +| 005 | Footer settings sub-option | P2 | M | 001 | TODO | +| 006 | Policies settings sub-option + storefront render route | P3 | M | 001 | TODO | + +Status values: TODO | IN PROGRESS | DONE | BLOCKED (reason) | REJECTED (rationale) + +## Dependency notes + +- **001 must land first.** It creates `src/stores/storeContentStore.ts` (the persisted + Zustand store every later plan extends with a new slice) and establishes the exact + route + feature-folder + settings-nav wiring pattern that 002–006 copy. Every other plan + depends on it. +- 002–006 are independent of each other and may be done in any order after 001. +- 005 (Footer) and 006 (Policies) interact: if both are done, Footer policy links should point + at the `/policy/$slug` route that 006 adds. 005 notes this; do 006 first if you want live links. + +## What each plan delivers + +- **001** β€” the shared persisted store, plus the first end-to-end sub-option ("General Store": + store name, contact, currency, units). Proves the whole vertical slice + adds the test pattern. +- **002** β€” Branding: logo paths, favicon, brand color; consumed by navbar/footer logo. +- **003** β€” Homepage: hero slides, side banners, trust-badge list, review cards, CTA copy. + Includes extracting arrays currently hard-coded inside home components into the store. +- **004** β€” Navigation: the storefront top-bar menu (`StoreTopBarData`). +- **005** β€” Footer: company/policy link lists, social URLs, payment badges, copyright. +- **006** β€” Policies: editable policy documents + a `/policy/$slug` storefront page to show them. + +## Findings considered and rejected + +- **Making homepage sections reorderable/addable** β€” rejected by product decision; order stays + hard-coded in `Home.tsx`. Admin edits data only. +- **A separate top-level "Store" sidebar group** β€” rejected by product decision; use Settings sub-options. +- **Real backend / axios data layer** β€” out of scope; no backend contract exists. `@/api/*` reserved. diff --git a/src/components/layout/data/sidebar-data.ts b/src/components/layout/data/sidebar-data.ts index 0b18d6a..bfc2c24 100644 --- a/src/components/layout/data/sidebar-data.ts +++ b/src/components/layout/data/sidebar-data.ts @@ -2,6 +2,7 @@ import { IconBarrierBlock, IconBrowserCheck, IconBug, + IconBuildingStore, IconChecklist, IconError404, IconHelp, @@ -207,6 +208,12 @@ export const sidebarData: SidebarData = { url: '/user/settings/display', icon: IconBrowserCheck, }, + // --- Store settings (site-admin only) --- TODO(roadmap-1): filter this block by role + { + title: 'General store', + url: '/user/settings/general', + icon: IconBuildingStore, + }, ], }, { diff --git a/src/features/settings/general/general-form.tsx b/src/features/settings/general/general-form.tsx new file mode 100644 index 0000000..516b3ea --- /dev/null +++ b/src/features/settings/general/general-form.tsx @@ -0,0 +1,140 @@ +import { z } from 'zod' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { + useGeneralSettings, + useSetGeneralSettings, +} from '@/stores/storeContentStore' +import { toast } from '@/hooks/use-toast' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +const generalFormSchema = z.object({ + storeName: z.string().min(2, { message: 'Store name is required.' }), + contactEmail: z.string().email({ message: 'Enter a valid email.' }), + contactPhone: z.string().min(3, { message: 'Enter a contact phone.' }), + currency: z.string().min(1, { message: 'Enter a currency code.' }), + weightUnit: z.enum(['kg', 'lb']), +}) + +type GeneralFormValues = z.infer + +export function GeneralForm() { + const general = useGeneralSettings() + const setGeneral = useSetGeneralSettings() + const form = useForm({ + resolver: zodResolver(generalFormSchema), + defaultValues: general, + }) + + function onSubmit(data: GeneralFormValues) { + setGeneral(data) + toast({ title: 'Store settings saved' }) + } + + return ( +
    + + ( + + Store name + + + + + Shown to customers across the storefront. + + + + )} + /> + ( + + Contact email + + + + + + )} + /> + ( + + Contact phone + + + + + + )} + /> + ( + + Default currency + + + + + ISO currency code used for prices (e.g. BDT, USD). + + + + )} + /> + ( + + Weight unit + + + Used for product weight and shipping estimates. + + + + )} + /> + + + + ) +} diff --git a/src/features/settings/general/index.tsx b/src/features/settings/general/index.tsx new file mode 100644 index 0000000..330d244 --- /dev/null +++ b/src/features/settings/general/index.tsx @@ -0,0 +1,13 @@ +import ContentSection from '../components/content-section' +import { GeneralForm } from './general-form' + +export default function SettingsGeneral() { + return ( + + + + ) +} diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index 10a5bd0..ac8e071 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -1,6 +1,7 @@ import { Outlet } from '@tanstack/react-router' import { IconBrowserCheck, + IconBuildingStore, IconNotification, IconPalette, IconTool, @@ -75,4 +76,10 @@ const sidebarNavItems = [ icon: , href: '/settings/display', }, + // --- Store settings (site-admin only) --- TODO(roadmap-1): filter this block by role + { + title: 'General store', + icon: , + href: '/settings/general', + }, ] diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 540190f..bd78e81 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -69,6 +69,9 @@ const UserAuthenticatedAcheckoutsIndexLazyRouteImport = createFileRoute( const UserAuthenticatedSettingsNotificationsLazyRouteImport = createFileRoute( '/user/_authenticated/settings/notifications', )() +const UserAuthenticatedSettingsGeneralLazyRouteImport = createFileRoute( + '/user/_authenticated/settings/general', +)() const UserAuthenticatedSettingsDisplayLazyRouteImport = createFileRoute( '/user/_authenticated/settings/display', )() @@ -311,6 +314,16 @@ const UserAuthenticatedSettingsNotificationsLazyRoute = (d) => d.Route, ), ) +const UserAuthenticatedSettingsGeneralLazyRoute = + UserAuthenticatedSettingsGeneralLazyRouteImport.update({ + id: '/general', + path: '/general', + getParentRoute: () => UserAuthenticatedSettingsRouteLazyRoute, + } as any).lazy(() => + import('./routes/user/_authenticated/settings/general.lazy').then( + (d) => d.Route, + ), + ) const UserAuthenticatedSettingsDisplayLazyRoute = UserAuthenticatedSettingsDisplayLazyRouteImport.update({ id: '/display', @@ -383,6 +396,7 @@ export interface FileRoutesByFullPath { '/user/settings/account': typeof UserAuthenticatedSettingsAccountLazyRoute '/user/settings/appearance': typeof UserAuthenticatedSettingsAppearanceLazyRoute '/user/settings/display': typeof UserAuthenticatedSettingsDisplayLazyRoute + '/user/settings/general': typeof UserAuthenticatedSettingsGeneralLazyRoute '/user/settings/notifications': typeof UserAuthenticatedSettingsNotificationsLazyRoute '/user/acheckouts/': typeof UserAuthenticatedAcheckoutsIndexLazyRoute '/user/apps/': typeof UserAuthenticatedAppsIndexLazyRoute @@ -418,6 +432,7 @@ export interface FileRoutesByTo { '/user/settings/account': typeof UserAuthenticatedSettingsAccountLazyRoute '/user/settings/appearance': typeof UserAuthenticatedSettingsAppearanceLazyRoute '/user/settings/display': typeof UserAuthenticatedSettingsDisplayLazyRoute + '/user/settings/general': typeof UserAuthenticatedSettingsGeneralLazyRoute '/user/settings/notifications': typeof UserAuthenticatedSettingsNotificationsLazyRoute '/user/acheckouts': typeof UserAuthenticatedAcheckoutsIndexLazyRoute '/user/apps': typeof UserAuthenticatedAppsIndexLazyRoute @@ -458,6 +473,7 @@ export interface FileRoutesById { '/user/_authenticated/settings/account': typeof UserAuthenticatedSettingsAccountLazyRoute '/user/_authenticated/settings/appearance': typeof UserAuthenticatedSettingsAppearanceLazyRoute '/user/_authenticated/settings/display': typeof UserAuthenticatedSettingsDisplayLazyRoute + '/user/_authenticated/settings/general': typeof UserAuthenticatedSettingsGeneralLazyRoute '/user/_authenticated/settings/notifications': typeof UserAuthenticatedSettingsNotificationsLazyRoute '/user/_authenticated/acheckouts/': typeof UserAuthenticatedAcheckoutsIndexLazyRoute '/user/_authenticated/apps/': typeof UserAuthenticatedAppsIndexLazyRoute @@ -497,6 +513,7 @@ export interface FileRouteTypes { | '/user/settings/account' | '/user/settings/appearance' | '/user/settings/display' + | '/user/settings/general' | '/user/settings/notifications' | '/user/acheckouts/' | '/user/apps/' @@ -532,6 +549,7 @@ export interface FileRouteTypes { | '/user/settings/account' | '/user/settings/appearance' | '/user/settings/display' + | '/user/settings/general' | '/user/settings/notifications' | '/user/acheckouts' | '/user/apps' @@ -571,6 +589,7 @@ export interface FileRouteTypes { | '/user/_authenticated/settings/account' | '/user/_authenticated/settings/appearance' | '/user/_authenticated/settings/display' + | '/user/_authenticated/settings/general' | '/user/_authenticated/settings/notifications' | '/user/_authenticated/acheckouts/' | '/user/_authenticated/apps/' @@ -825,6 +844,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UserAuthenticatedSettingsNotificationsLazyRouteImport parentRoute: typeof UserAuthenticatedSettingsRouteLazyRoute } + '/user/_authenticated/settings/general': { + id: '/user/_authenticated/settings/general' + path: '/general' + fullPath: '/user/settings/general' + preLoaderRoute: typeof UserAuthenticatedSettingsGeneralLazyRouteImport + parentRoute: typeof UserAuthenticatedSettingsRouteLazyRoute + } '/user/_authenticated/settings/display': { id: '/user/_authenticated/settings/display' path: '/display' @@ -894,6 +920,7 @@ interface UserAuthenticatedSettingsRouteLazyRouteChildren { UserAuthenticatedSettingsAccountLazyRoute: typeof UserAuthenticatedSettingsAccountLazyRoute UserAuthenticatedSettingsAppearanceLazyRoute: typeof UserAuthenticatedSettingsAppearanceLazyRoute UserAuthenticatedSettingsDisplayLazyRoute: typeof UserAuthenticatedSettingsDisplayLazyRoute + UserAuthenticatedSettingsGeneralLazyRoute: typeof UserAuthenticatedSettingsGeneralLazyRoute UserAuthenticatedSettingsNotificationsLazyRoute: typeof UserAuthenticatedSettingsNotificationsLazyRoute UserAuthenticatedSettingsIndexLazyRoute: typeof UserAuthenticatedSettingsIndexLazyRoute } @@ -906,6 +933,8 @@ const UserAuthenticatedSettingsRouteLazyRouteChildren: UserAuthenticatedSettings UserAuthenticatedSettingsAppearanceLazyRoute, UserAuthenticatedSettingsDisplayLazyRoute: UserAuthenticatedSettingsDisplayLazyRoute, + UserAuthenticatedSettingsGeneralLazyRoute: + UserAuthenticatedSettingsGeneralLazyRoute, UserAuthenticatedSettingsNotificationsLazyRoute: UserAuthenticatedSettingsNotificationsLazyRoute, UserAuthenticatedSettingsIndexLazyRoute: diff --git a/src/routes/user/_authenticated/settings/general.lazy.tsx b/src/routes/user/_authenticated/settings/general.lazy.tsx new file mode 100644 index 0000000..f39ddf5 --- /dev/null +++ b/src/routes/user/_authenticated/settings/general.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import SettingsGeneral from '@/features/settings/general' + +export const Route = createLazyFileRoute( + '/user/_authenticated/settings/general' +)({ component: SettingsGeneral }) diff --git a/src/stores/storeContentStore.test.ts b/src/stores/storeContentStore.test.ts new file mode 100644 index 0000000..c3fc466 --- /dev/null +++ b/src/stores/storeContentStore.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { useStoreContentStore } from './storeContentStore' + +describe('storeContentStore β€” general slice', () => { + it('exposes sensible defaults', () => { + const { general } = useStoreContentStore.getState() + expect(general.storeName).toBeTruthy() + expect(general.weightUnit).toBe('kg') + }) + + it('updates the general slice via setGeneral', () => { + const next = { + storeName: 'Test Store', + contactEmail: 'a@b.co', + contactPhone: '123', + currency: 'USD', + weightUnit: 'lb' as const, + } + useStoreContentStore.getState().setGeneral(next) + expect(useStoreContentStore.getState().general.storeName).toBe('Test Store') + }) +}) diff --git a/src/stores/storeContentStore.ts b/src/stores/storeContentStore.ts new file mode 100644 index 0000000..5c30674 --- /dev/null +++ b/src/stores/storeContentStore.ts @@ -0,0 +1,39 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export interface GeneralStoreSettings { + storeName: string + contactEmail: string + contactPhone: string + currency: string + weightUnit: 'kg' | 'lb' +} + +interface StoreContentState { + general: GeneralStoreSettings + setGeneral: (value: GeneralStoreSettings) => void +} + +const defaultGeneral: GeneralStoreSettings = { + storeName: 'NEXF Lifestyle', + contactEmail: 'support@nexf.example', + contactPhone: '+880 1000 000000', + currency: 'BDT', + weightUnit: 'kg', +} + +export const useStoreContentStore = create()( + persist( + (set) => ({ + general: defaultGeneral, + setGeneral: (value) => set({ general: value }), + }), + { name: 'store-content' } + ) +) + +// Selector hooks (mirror authStore's `useAuth` export style) +export const useGeneralSettings = () => + useStoreContentStore((state) => state.general) +export const useSetGeneralSettings = () => + useStoreContentStore((state) => state.setGeneral)