Basic Architecture of Sport Web
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# apps/admin
|
||||
#
|
||||
# There is deliberately NO DATABASE_URL here. The admin dashboard has no
|
||||
# database driver, no Prisma client and no direct access to Redis or R2.
|
||||
# Everything goes through the REST API so that authorization, validation and
|
||||
# audit logging can never be bypassed by a second write path.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3001
|
||||
|
||||
# Server Components / route handlers. Inside Docker this is the API container.
|
||||
API_INTERNAL_URL=http://localhost:4000
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextConfig } from '@sport/eslint-config/next';
|
||||
|
||||
export default nextConfig;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/types/root-params.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@sport/ui'],
|
||||
typedRoutes: true,
|
||||
output: 'standalone',
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ protocol: 'http', hostname: 'localhost', port: '9000' },
|
||||
{ protocol: 'https', hostname: '**.r2.dev' },
|
||||
{ protocol: 'https', hostname: 'cdn.sport-store.local' },
|
||||
],
|
||||
},
|
||||
|
||||
// The admin is an internal tool: keep it out of every index, permanently.
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{ key: 'X-Robots-Tag', value: 'noindex, nofollow' },
|
||||
{ key: 'Referrer-Policy', value: 'same-origin' },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@sport/admin",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Back-office Next.js dashboard. Talks to the REST API only — never to the database.",
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start --port 3001",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"clean": "rm -rf .next .turbo *.tsbuildinfo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sport/api-client": "workspace:*",
|
||||
"@sport/types": "workspace:*",
|
||||
"@sport/ui": "workspace:*",
|
||||
"@sport/validation": "workspace:*",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sport/config": "workspace:*",
|
||||
"@sport/eslint-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "catalog:",
|
||||
"@types/node": "^22.19.0",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"eslint": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = { title: 'Sign in' };
|
||||
|
||||
/**
|
||||
* Sits outside the dashboard route group so it renders without the sidebar and
|
||||
* without the auth requirement.
|
||||
*/
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="bg-ink-50 flex min-h-screen items-center justify-center px-6">
|
||||
<div className="border-ink-200 w-full max-w-sm border bg-white p-8">
|
||||
<h1 className="text-lg font-black uppercase tracking-tighter">
|
||||
Sport<span className="text-volt-600">.</span> Admin
|
||||
</h1>
|
||||
<p className="text-ink-500 mt-4 text-sm">
|
||||
Sign-in lands with the auth milestone (M2). Credentials will be exchanged for a
|
||||
short-lived access token plus a rotating, httpOnly refresh cookie scoped to the admin
|
||||
audience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Brands' };
|
||||
|
||||
export default function BrandsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Brands"
|
||||
description="Brand records, logos and SEO fields."
|
||||
permission="brand.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Categories' };
|
||||
|
||||
export default function CategoriesPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Categories"
|
||||
description="Drag-to-reorder category tree with materialised-path maintenance."
|
||||
permission="category.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Content' };
|
||||
|
||||
export default function CmsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Content"
|
||||
description="Homepage blocks, banners, blog posts and static pages."
|
||||
permission="cms.read"
|
||||
milestone="M7 — content"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Collections' };
|
||||
|
||||
export default function CollectionsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Collections"
|
||||
description="Manual and rule-based collections, with campaign scheduling."
|
||||
permission="collection.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Coupons' };
|
||||
|
||||
export default function CouponsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Coupons"
|
||||
description="Coupon codes, usage limits and redemption reporting."
|
||||
permission="coupon.manage"
|
||||
milestone="M7 — marketing"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Customers' };
|
||||
|
||||
export default function CustomersPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Customers"
|
||||
description="Customer records, order history and addresses."
|
||||
permission="customer.read"
|
||||
milestone="M5 — orders"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Inventory' };
|
||||
|
||||
export default function InventoryPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Inventory"
|
||||
description="Stock by variant and location, adjustments and the movement ledger."
|
||||
permission="inventory.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AdminSidebar } from '@/components/layout/admin-sidebar';
|
||||
|
||||
/**
|
||||
* Every route in this group requires an authenticated back-office actor.
|
||||
* Enforcement is layered: middleware checks for a session cookie, this layout
|
||||
* verifies the token server-side, and the API re-checks permissions on every
|
||||
* request. Only the last one is real security; the first two are UX.
|
||||
*/
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<AdminSidebar />
|
||||
<div className="min-w-0 flex-1">
|
||||
<header className="border-ink-200 flex h-14 items-center justify-end border-b bg-white px-6">
|
||||
<span className="text-ink-500 text-xs font-medium">Signed out</span>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Media' };
|
||||
|
||||
export default function MediaPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Media"
|
||||
description="Asset library. Uploads go browser → presigned URL → R2; the API only records metadata."
|
||||
permission="media.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Orders' };
|
||||
|
||||
export default function OrdersPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Orders"
|
||||
description="Order list and detail: fulfilment, status transitions, refunds and the audit trail."
|
||||
permission="order.read"
|
||||
milestone="M5 — orders"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Dashboard' };
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Dashboard"
|
||||
description="Revenue, orders, conversion and low-stock alerts."
|
||||
permission="order.read"
|
||||
milestone="M9 — admin analytics"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Products' };
|
||||
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Products"
|
||||
description="Product list with status, brand and variant count. The editor manages options, generates the variant matrix and edits per-variant SKU, price and stock."
|
||||
permission="product.read"
|
||||
milestone="M3 — admin catalog"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Promotions' };
|
||||
|
||||
export default function PromotionsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Promotions"
|
||||
description="Automatic cart-level discount rules."
|
||||
permission="promotion.manage"
|
||||
milestone="M7 — marketing"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Reviews' };
|
||||
|
||||
export default function ReviewsPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Reviews"
|
||||
description="Review moderation queue."
|
||||
permission="review.moderate"
|
||||
milestone="M7 — marketing"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Roles' };
|
||||
|
||||
export default function RolesPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Roles"
|
||||
description="Role editor: a role is a named set of permissions, editable at runtime with no deploy."
|
||||
permission="role.read"
|
||||
milestone="M2 — auth & RBAC"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { PageScaffold } from '@/components/layout/page-scaffold';
|
||||
|
||||
export const metadata: Metadata = { title: 'Users' };
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<PageScaffold
|
||||
title="Users"
|
||||
description="Back-office accounts and role assignment."
|
||||
permission="user.read"
|
||||
milestone="M2 — auth & RBAC"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: { default: 'Sport Store Admin', template: '%s | Sport Admin' },
|
||||
description: 'Back-office dashboard.',
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="min-h-screen antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { NAVIGATION } from '@/lib/navigation';
|
||||
|
||||
/**
|
||||
* Renders every section for now. Once the session carries permissions, each
|
||||
* item is filtered with `hasPermission(actor.permissions, item.permission)` —
|
||||
* the same catalog the API guards read, so menu and enforcement cannot drift.
|
||||
*/
|
||||
export function AdminSidebar() {
|
||||
return (
|
||||
<aside className="border-ink-200 hidden w-60 shrink-0 border-r bg-white lg:block">
|
||||
<div className="border-ink-200 flex h-14 items-center border-b px-5">
|
||||
<Link href="/" className="text-sm font-black uppercase tracking-tighter">
|
||||
Sport<span className="text-volt-600">.</span> Admin
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-6 p-5">
|
||||
{NAVIGATION.map((section) => (
|
||||
<div key={section.title}>
|
||||
<h2 className="text-ink-400 text-[0.625rem] font-semibold uppercase tracking-widest">
|
||||
{section.title}
|
||||
</h2>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="rounded-card text-ink-600 hover:bg-ink-100 hover:text-ink-950 block px-2 py-1.5 text-sm"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Badge } from '@sport/ui';
|
||||
|
||||
/** Placeholder for admin screens that are not built yet. */
|
||||
export function PageScaffold({
|
||||
title,
|
||||
description,
|
||||
permission,
|
||||
milestone,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
permission: string;
|
||||
milestone: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<p className="text-ink-500 mt-2 max-w-2xl text-sm">{description}</p>
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
<Badge variant="outline">requires: {permission}</Badge>
|
||||
<Badge variant="neutral">Planned: {milestone}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: auth
|
||||
|
||||
Admin sign-in, session handling and the permission-aware `<Can>` component.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: customers
|
||||
|
||||
Customer list and detail.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: inventory
|
||||
|
||||
Stock table, adjustments and the movement ledger.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: media
|
||||
|
||||
Asset library, uploader and picker dialog.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: orders
|
||||
|
||||
Order list, detail, fulfilment and refund flows.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: products
|
||||
|
||||
Product list, editor, option builder and the variant matrix grid.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,7 @@
|
||||
# feature: settings
|
||||
|
||||
Users, roles and store settings.
|
||||
|
||||
Same rules as the storefront's feature folders: no cross-feature imports, and
|
||||
all data access goes through `@sport/api-client`. The admin has no database
|
||||
client of its own.
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createApiClient } from '@sport/api-client';
|
||||
|
||||
import { clientEnv, getServerEnv } from './env';
|
||||
|
||||
/**
|
||||
* The admin's only channel to data.
|
||||
*
|
||||
* There is no Prisma client in this application and there never will be. Every
|
||||
* read and write crosses the REST boundary, which is what guarantees that RBAC,
|
||||
* validation and audit logging apply uniformly — a second write path is a
|
||||
* second place for authorization to be forgotten.
|
||||
*/
|
||||
export function getServerApi() {
|
||||
return createApiClient({ baseUrl: getServerEnv().API_INTERNAL_URL });
|
||||
}
|
||||
|
||||
export const browserApi = createApiClient({
|
||||
baseUrl: clientEnv.NEXT_PUBLIC_API_URL,
|
||||
getAccessToken: () => null, // wired to the auth store in the auth milestone
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const clientEnvSchema = z.object({
|
||||
NEXT_PUBLIC_API_URL: z.url(),
|
||||
NEXT_PUBLIC_APP_URL: z.url(),
|
||||
});
|
||||
|
||||
export const clientEnv = clientEnvSchema.parse({
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
});
|
||||
|
||||
export function getServerEnv() {
|
||||
return z
|
||||
.object({ API_INTERNAL_URL: z.url() })
|
||||
.parse({ API_INTERNAL_URL: process.env.API_INTERNAL_URL ?? clientEnv.NEXT_PUBLIC_API_URL });
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { PERMISSIONS, type Permission } from '@sport/types';
|
||||
|
||||
/**
|
||||
* The sidebar is derived from the same permission catalog the API guards use.
|
||||
*
|
||||
* A user who cannot read orders never sees an Orders link, so the UI has no
|
||||
* dead ends. This is presentation only — hiding a link is not authorization.
|
||||
* The API re-checks every request, because a hidden link is one devtools
|
||||
* inspection away from being visible.
|
||||
*/
|
||||
export interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
export interface NavSection {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
export const NAVIGATION: NavSection[] = [
|
||||
{
|
||||
title: 'Catalog',
|
||||
items: [
|
||||
{ href: '/products', label: 'Products', permission: PERMISSIONS.PRODUCT_READ },
|
||||
{ href: '/categories', label: 'Categories', permission: PERMISSIONS.CATEGORY_READ },
|
||||
{ href: '/collections', label: 'Collections', permission: PERMISSIONS.COLLECTION_READ },
|
||||
{ href: '/brands', label: 'Brands', permission: PERMISSIONS.BRAND_READ },
|
||||
{ href: '/inventory', label: 'Inventory', permission: PERMISSIONS.INVENTORY_READ },
|
||||
{ href: '/media', label: 'Media', permission: PERMISSIONS.MEDIA_READ },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sales',
|
||||
items: [
|
||||
{ href: '/orders', label: 'Orders', permission: PERMISSIONS.ORDER_READ },
|
||||
{ href: '/customers', label: 'Customers', permission: PERMISSIONS.CUSTOMER_READ },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Marketing',
|
||||
items: [
|
||||
{ href: '/promotions', label: 'Promotions', permission: PERMISSIONS.PROMOTION_MANAGE },
|
||||
{ href: '/coupons', label: 'Coupons', permission: PERMISSIONS.COUPON_MANAGE },
|
||||
{ href: '/reviews', label: 'Reviews', permission: PERMISSIONS.REVIEW_MODERATE },
|
||||
{ href: '/cms', label: 'Content', permission: PERMISSIONS.CMS_READ },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
items: [
|
||||
{ href: '/settings/users', label: 'Users', permission: PERMISSIONS.USER_READ },
|
||||
{ href: '/settings/roles', label: 'Roles', permission: PERMISSIONS.ROLE_READ },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@sport/config/tailwind/theme.css';
|
||||
|
||||
@source "../../../../packages/ui/src";
|
||||
|
||||
:root {
|
||||
--font-inter: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-display: var(--font-inter);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-ink-50);
|
||||
color: var(--color-ink-950);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@sport/config/typescript/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"*.ts",
|
||||
"*.mjs"
|
||||
],
|
||||
"exclude": ["node_modules", ".next"]
|
||||
}
|
||||
Reference in New Issue
Block a user