added shell screens

This commit is contained in:
shafi54 2026-05-23 11:25:27 +05:30
parent 7c9fb6b060
commit 17f1f8e4d2
21 changed files with 667 additions and 154 deletions

1
agents.md Normal file
View file

@ -0,0 +1 @@
don't try to compile or run or build any project until explicitly asked for

View file

@ -13,7 +13,8 @@
"**/index.html", "**/index.html",
"**/vite.config.ts", "**/vite.config.ts",
"!**/src/routeTree.gen.ts", "!**/src/routeTree.gen.ts",
"!**/src/styles.css" "!**/src/styles.css",
"!**/inspiration"
] ]
}, },
"formatter": { "formatter": {
@ -33,4 +34,3 @@
} }
} }
} }

View file

@ -0,0 +1,67 @@
import { useState, useEffect, type ReactNode } from "react";
import { useMatches } from "@tanstack/react-router";
import { Sidebar } from "#/components/Sidebar";
import { Header } from "#/components/Header";
interface RouteStaticData {
title?: string;
subtitle?: string;
}
interface AppLayoutProps {
children: ReactNode;
}
export function AppLayout({ children }: AppLayoutProps) {
const [minimized, setMinimized] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const matches = useMatches();
const currentMatch = matches[matches.length - 1];
const staticData = currentMatch?.staticData as RouteStaticData | undefined;
useEffect(() => {
function handleResize() {
if (window.innerWidth > 768) {
setMobileOpen(false);
}
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (mobileOpen && window.innerWidth <= 768) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [mobileOpen]);
return (
<div className="flex h-screen">
<Sidebar
minimized={minimized}
mobileOpen={mobileOpen}
onToggleMinimized={() => setMinimized((prev) => !prev)}
onCloseMobile={() => setMobileOpen(false)}
/>
<div className="flex flex-1 flex-col min-w-0">
<Header
pageTitle={staticData?.title ?? "Dashboard"}
pageSubtitle={
staticData?.subtitle ?? "Pharmacy overview & inventory at a glance"
}
onMobileMenuToggle={() => setMobileOpen((prev) => !prev)}
/>
<main className="flex-1 overflow-y-auto p-6 bg-slate-100">
{children}
</main>
</div>
</div>
);
}

View file

@ -0,0 +1,54 @@
import { Menu, Bell, Search } from "lucide-react";
interface HeaderProps {
pageTitle: string;
pageSubtitle: string;
onMobileMenuToggle: () => void;
}
export function Header({
pageTitle,
pageSubtitle,
onMobileMenuToggle,
}: HeaderProps) {
return (
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-6 shrink-0">
<div className="flex items-center gap-4">
<button
type="button"
onClick={onMobileMenuToggle}
className="md:hidden w-9 h-9 flex items-center justify-center rounded-md text-slate-600 hover:bg-slate-100"
aria-label="Open menu"
>
<Menu className="w-5 h-5" />
</button>
<div>
<h1 className="text-lg font-semibold text-slate-900">{pageTitle}</h1>
<p className="text-[13px] text-slate-600 mt-px">{pageSubtitle}</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 px-3 py-2 bg-slate-100 rounded-md border border-transparent transition-all duration-200 focus-within:border-blue-600 focus-within:bg-white focus-within:w-[280px] w-[220px]">
<Search className="w-3.5 h-3.5 text-slate-600 shrink-0" />
<input
type="text"
placeholder="Search medicines..."
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-600"
/>
</div>
<button
type="button"
className="relative w-9 h-9 flex items-center justify-center rounded-md text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition-colors"
aria-label="Notifications"
>
<Bell className="w-[18px] h-[18px]" />
<span className="absolute top-1 right-1 w-4 h-4 rounded-full bg-red-600 text-white text-[9px] font-bold flex items-center justify-center">
3
</span>
</button>
</div>
</header>
);
}

View file

@ -0,0 +1,159 @@
import { Link, useLocation } from "@tanstack/react-router";
import {
LayoutDashboard,
ReceiptText,
Package,
Truck,
Warehouse,
Users,
Pill,
UserRoundCog,
ChevronLeft,
} from "lucide-react";
import { cn } from "#/lib/cn";
const NAV_ITEMS = [
{ to: "/", label: "Home", icon: LayoutDashboard },
{ to: "/billing", label: "Billing", icon: ReceiptText },
{ to: "/stock", label: "Stock", icon: Package },
{ to: "/distributors", label: "Distributors", icon: Truck },
{ to: "/storage", label: "Storage", icon: Warehouse },
{ to: "/customers", label: "Customers", icon: Users },
{ to: "/products", label: "Products", icon: Pill },
{ to: "/staff", label: "Staff", icon: UserRoundCog },
] as const;
interface SidebarProps {
minimized: boolean;
mobileOpen: boolean;
onToggleMinimized: () => void;
onCloseMobile: () => void;
}
export function Sidebar({
minimized,
mobileOpen,
onToggleMinimized,
onCloseMobile,
}: SidebarProps) {
const location = useLocation();
const showLabels = !minimized || mobileOpen;
return (
<>
<button
type="button"
className={cn(
"fixed inset-0 z-40 bg-black/40 md:hidden transition-opacity border-0 p-0 w-full h-full cursor-default",
mobileOpen ? "opacity-100" : "opacity-0 pointer-events-none",
)}
onClick={onCloseMobile}
onKeyDown={(e) => {
if (e.key === "Escape" || e.key === "Enter") onCloseMobile();
}}
aria-label="Close sidebar"
/>
<aside
className={cn(
"z-50 flex flex-col bg-slate-900 overflow-hidden transition-[width,transform] duration-300",
"fixed inset-y-0 left-0 md:static",
"w-60 min-w-60",
mobileOpen ? "translate-x-0" : "-translate-x-full",
"md:translate-x-0",
minimized && "md:w-16 md:min-w-16",
)}
>
<div className="flex items-center justify-between h-16 px-4 shrink-0">
<div className="flex items-center gap-2.5 overflow-hidden">
<div className="w-7 h-7 rounded-md bg-blue-600 flex items-center justify-center shrink-0">
<svg
width="16"
height="16"
viewBox="0 0 28 28"
fill="none"
aria-hidden="true"
>
<path
d="M9 14l3.5 3.5L19 11"
stroke="#fff"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
{showLabels && (
<span className="text-white font-semibold text-lg whitespace-nowrap">
Pharmanager
</span>
)}
</div>
<button
type="button"
onClick={onToggleMinimized}
className="hidden md:flex w-8 h-8 items-center justify-center rounded-md text-slate-400 hover:bg-slate-800 hover:text-white transition-colors shrink-0"
aria-label="Toggle sidebar"
>
<ChevronLeft
className={cn(
"w-[18px] h-[18px] transition-transform",
minimized && "rotate-180",
)}
/>
</button>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-2 flex flex-col gap-0.5">
{NAV_ITEMS.map((item) => {
const isActive = location.pathname === item.to;
return (
<Link
key={item.to}
to={item.to}
onClick={onCloseMobile}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium whitespace-nowrap overflow-hidden transition-colors",
minimized &&
!(item.to === "/") &&
"md:justify-center md:px-0",
isActive
? "bg-blue-600 text-white"
: "text-slate-400 hover:bg-slate-800 hover:text-white",
)}
>
<item.icon className="w-5 h-5 shrink-0" />
{showLabels && <span>{item.label}</span>}
</Link>
);
})}
</nav>
<div
className={cn(
"shrink-0 border-t border-white/[0.06]",
minimized && !mobileOpen ? "p-3" : "p-4",
)}
>
<Link
to="/profile"
className="flex items-center gap-2.5 overflow-hidden text-slate-400 text-sm"
>
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-[13px] shrink-0">
MS
</div>
{showLabels && (
<div className="overflow-hidden">
<div className="text-white font-medium whitespace-nowrap text-[13px]">
Dr. Mohammed
</div>
<div className="whitespace-nowrap text-[11px]">Pharmacist</div>
</div>
)}
</Link>
</div>
</aside>
</>
);
}

View file

@ -0,0 +1,3 @@
export function cn(...classes: (string | false | null | undefined)[]): string {
return classes.filter(Boolean).join(" ");
}

View file

@ -1,27 +1,28 @@
import ReactDOM from 'react-dom/client' import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from '@tanstack/react-router' import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from './routeTree.gen' import { routeTree } from "./routeTree.gen";
import { TrpcProvider } from 'shared-react' import { TrpcProvider } from "shared-react";
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
defaultPreload: 'intent', defaultPreload: "intent",
scrollRestoration: true, scrollRestoration: true,
}) });
declare module '@tanstack/react-router' { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router router: typeof router;
} }
} }
const rootElement = document.getElementById('app')! const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Root element not found");
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement) const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<TrpcProvider baseUrl="http://localhost:3001"> <TrpcProvider baseUrl="http://localhost:3001">
<RouterProvider router={router} /> <RouterProvider router={router} />
</TrpcProvider>, </TrpcProvider>,
) );
} }

View file

@ -9,8 +9,56 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as StorageRouteImport } from './routes/storage'
import { Route as StockRouteImport } from './routes/stock'
import { Route as StaffRouteImport } from './routes/staff'
import { Route as ProfileRouteImport } from './routes/profile'
import { Route as ProductsRouteImport } from './routes/products'
import { Route as DistributorsRouteImport } from './routes/distributors'
import { Route as CustomersRouteImport } from './routes/customers'
import { Route as BillingRouteImport } from './routes/billing'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
const StorageRoute = StorageRouteImport.update({
id: '/storage',
path: '/storage',
getParentRoute: () => rootRouteImport,
} as any)
const StockRoute = StockRouteImport.update({
id: '/stock',
path: '/stock',
getParentRoute: () => rootRouteImport,
} as any)
const StaffRoute = StaffRouteImport.update({
id: '/staff',
path: '/staff',
getParentRoute: () => rootRouteImport,
} as any)
const ProfileRoute = ProfileRouteImport.update({
id: '/profile',
path: '/profile',
getParentRoute: () => rootRouteImport,
} as any)
const ProductsRoute = ProductsRouteImport.update({
id: '/products',
path: '/products',
getParentRoute: () => rootRouteImport,
} as any)
const DistributorsRoute = DistributorsRouteImport.update({
id: '/distributors',
path: '/distributors',
getParentRoute: () => rootRouteImport,
} as any)
const CustomersRoute = CustomersRouteImport.update({
id: '/customers',
path: '/customers',
getParentRoute: () => rootRouteImport,
} as any)
const BillingRoute = BillingRouteImport.update({
id: '/billing',
path: '/billing',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@ -19,28 +67,144 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/billing': typeof BillingRoute
'/customers': typeof CustomersRoute
'/distributors': typeof DistributorsRoute
'/products': typeof ProductsRoute
'/profile': typeof ProfileRoute
'/staff': typeof StaffRoute
'/stock': typeof StockRoute
'/storage': typeof StorageRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/billing': typeof BillingRoute
'/customers': typeof CustomersRoute
'/distributors': typeof DistributorsRoute
'/products': typeof ProductsRoute
'/profile': typeof ProfileRoute
'/staff': typeof StaffRoute
'/stock': typeof StockRoute
'/storage': typeof StorageRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/billing': typeof BillingRoute
'/customers': typeof CustomersRoute
'/distributors': typeof DistributorsRoute
'/products': typeof ProductsRoute
'/profile': typeof ProfileRoute
'/staff': typeof StaffRoute
'/stock': typeof StockRoute
'/storage': typeof StorageRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' fullPaths:
| '/'
| '/billing'
| '/customers'
| '/distributors'
| '/products'
| '/profile'
| '/staff'
| '/stock'
| '/storage'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' to:
id: '__root__' | '/' | '/'
| '/billing'
| '/customers'
| '/distributors'
| '/products'
| '/profile'
| '/staff'
| '/stock'
| '/storage'
id:
| '__root__'
| '/'
| '/billing'
| '/customers'
| '/distributors'
| '/products'
| '/profile'
| '/staff'
| '/stock'
| '/storage'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
BillingRoute: typeof BillingRoute
CustomersRoute: typeof CustomersRoute
DistributorsRoute: typeof DistributorsRoute
ProductsRoute: typeof ProductsRoute
ProfileRoute: typeof ProfileRoute
StaffRoute: typeof StaffRoute
StockRoute: typeof StockRoute
StorageRoute: typeof StorageRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/storage': {
id: '/storage'
path: '/storage'
fullPath: '/storage'
preLoaderRoute: typeof StorageRouteImport
parentRoute: typeof rootRouteImport
}
'/stock': {
id: '/stock'
path: '/stock'
fullPath: '/stock'
preLoaderRoute: typeof StockRouteImport
parentRoute: typeof rootRouteImport
}
'/staff': {
id: '/staff'
path: '/staff'
fullPath: '/staff'
preLoaderRoute: typeof StaffRouteImport
parentRoute: typeof rootRouteImport
}
'/profile': {
id: '/profile'
path: '/profile'
fullPath: '/profile'
preLoaderRoute: typeof ProfileRouteImport
parentRoute: typeof rootRouteImport
}
'/products': {
id: '/products'
path: '/products'
fullPath: '/products'
preLoaderRoute: typeof ProductsRouteImport
parentRoute: typeof rootRouteImport
}
'/distributors': {
id: '/distributors'
path: '/distributors'
fullPath: '/distributors'
preLoaderRoute: typeof DistributorsRouteImport
parentRoute: typeof rootRouteImport
}
'/customers': {
id: '/customers'
path: '/customers'
fullPath: '/customers'
preLoaderRoute: typeof CustomersRouteImport
parentRoute: typeof rootRouteImport
}
'/billing': {
id: '/billing'
path: '/billing'
fullPath: '/billing'
preLoaderRoute: typeof BillingRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@ -53,6 +217,14 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
BillingRoute: BillingRoute,
CustomersRoute: CustomersRoute,
DistributorsRoute: DistributorsRoute,
ProductsRoute: ProductsRoute,
ProfileRoute: ProfileRoute,
StaffRoute: StaffRoute,
StockRoute: StockRoute,
StorageRoute: StorageRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View file

@ -1,19 +0,0 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}

View file

@ -1,28 +1,31 @@
import { Outlet, createRootRoute } from '@tanstack/react-router' import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanStackDevtools } from '@tanstack/react-devtools' import { TanStackDevtools } from "@tanstack/react-devtools";
import { AppLayout } from "#/components/AppLayout";
import '../styles.css' import "../styles.css";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RootComponent, component: RootComponent,
}) });
function RootComponent() { function RootComponent() {
return ( return (
<> <>
<AppLayout>
<Outlet /> <Outlet />
</AppLayout>
<TanStackDevtools <TanStackDevtools
config={{ config={{
position: 'bottom-right', position: "bottom-right",
}} }}
plugins={[ plugins={[
{ {
name: 'TanStack Router', name: "TanStack Router",
render: <TanStackRouterDevtoolsPanel />, render: <TanStackRouterDevtoolsPanel />,
}, },
]} ]}
/> />
</> </>
) );
} }

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/billing")({
component: BillingPage,
staticData: {
title: "Billing",
subtitle: "Invoices, payments, revenue tracking",
},
});
function BillingPage() {
return <div>Billing</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/customers")({
component: CustomersPage,
staticData: {
title: "Customers",
subtitle: "Customer management with purchase history",
},
});
function CustomersPage() {
return <div>Customers</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/distributors")({
component: DistributorsPage,
staticData: {
title: "Distributors",
subtitle: "Agency management with contact details",
},
});
function DistributorsPage() {
return <div>Distributors</div>;
}

View file

@ -1,45 +1,13 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import type { Person } from '@repo/shared'
import { useCounterStore, useGetStorageSpaces } from 'shared-react'
export const Route = createFileRoute('/')({ component: Home }) export const Route = createFileRoute("/")({
component: HomePage,
staticData: {
title: "Dashboard",
subtitle: "Pharmacy overview & inventory at a glance",
},
});
function Home() { function HomePage() {
const shafi:Person = {age: 32, name: 'Shafi'} return <div>Home</div>;
const count = useCounterStore((s) => s.count)
const inc = useCounterStore((s) => s.inc)
const spaces = useGetStorageSpaces()
return (
<div className="p-8">
<h1 className="text-4xl font-bold">Welcome to TanStack Start</h1>
<p className="mt-4 text-lg">
{shafi.name} is {shafi.age} years old.
</p>
<button
type="button"
className="mt-6 rounded bg-black px-4 py-2 text-white"
onClick={inc}
>
Count: {count}
</button>
<div className="mt-8">
<h2 className="text-2xl font-semibold">Storage Spaces</h2>
{spaces.isLoading ? (
<p className="mt-2">Loading</p>
) : spaces.error ? (
<p className="mt-2 text-red-600">Failed to load storage spaces</p>
) : (
<ul className="mt-2 list-disc pl-5">
{spaces.data?.map((s) => (
<li key={s.id}>
{s.name}
{s.description ? `: ${s.description}` : ''}
</li>
))}
</ul>
)}
</div>
</div>
)
} }

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/products")({
component: ProductsPage,
staticData: {
title: "Products",
subtitle: "Medicine catalog with search, add, edit, delete",
},
});
function ProductsPage() {
return <div>Products</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/profile")({
component: ProfilePage,
staticData: {
title: "Profile",
subtitle: "Your profile information",
},
});
function ProfilePage() {
return <div>Profile</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/staff")({
component: StaffPage,
staticData: {
title: "Staff",
subtitle: "Staff management with roles & permissions",
},
});
function StaffPage() {
return <div>Staff</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/stock")({
component: StockPage,
staticData: {
title: "Stock",
subtitle: "Inventory levels, low-stock alerts, purchase orders",
},
});
function StockPage() {
return <div>Stock</div>;
}

View file

@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/storage")({
component: StoragePage,
staticData: {
title: "Storage",
subtitle: "Rack & shelf management with image records",
},
});
function StoragePage() {
return <div>Storage</div>;
}

View file

@ -1,19 +1,19 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import { devtools } from '@tanstack/devtools-vite' import { devtools } from "@tanstack/devtools-vite";
import { tanstackRouter } from '@tanstack/router-plugin/vite' import { tanstackRouter } from "@tanstack/router-plugin/vite";
import viteReact from '@vitejs/plugin-react' import viteReact from "@vitejs/plugin-react";
import tailwindcss from '@tailwindcss/vite' import tailwindcss from "@tailwindcss/vite";
const config = defineConfig({ const config = defineConfig({
resolve: { tsconfigPaths: true }, resolve: { tsconfigPaths: true },
plugins: [ plugins: [
devtools(), devtools(),
tailwindcss(), tailwindcss(),
tanstackRouter({ target: 'react', autoCodeSplitting: true }), tanstackRouter({ target: "react", autoCodeSplitting: true }),
viteReact(), viteReact(),
], ],
}) });
export default config export default config;