├── stack.tsx ├── app ├── favicon.ico ├── globals.css ├── handler │ └── [...stack] │ │ └── page.tsx ├── sign-in │ └── page.tsx ├── layout.tsx ├── settings │ └── page.tsx ├── page.tsx ├── add-product │ └── page.tsx ├── inventory │ └── page.tsx ├── loading.tsx └── dashboard │ └── page.tsx ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20250921232434_init │ │ └── migration.sql ├── schema.prisma └── seed.ts ├── stack ├── client.tsx └── server.tsx ├── lib ├── auth.ts ├── prisma.ts └── actions │ └── products.ts ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── package.json ├── components ├── products-chart.tsx ├── sidebar.tsx └── pagination.tsx └── README.md /stack.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machadop1407/NextJS-inventory-management-app/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /stack/client.tsx: -------------------------------------------------------------------------------- 1 | import { StackClientApp } from "@stackframe/stack"; 2 | 3 | export const stackClientApp = new StackClientApp({ 4 | tokenStore: "nextjs-cookie", 5 | }); 6 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | body { 4 | background: var(--background); 5 | color: var(--foreground); 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /stack/server.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { StackServerApp } from "@stackframe/stack"; 4 | 5 | export const stackServerApp = new StackServerApp({ 6 | tokenStore: "nextjs-cookie", 7 | }); 8 | -------------------------------------------------------------------------------- /app/handler/[...stack]/page.tsx: -------------------------------------------------------------------------------- 1 | import { StackHandler } from "@stackframe/stack"; 2 | import { stackServerApp } from "../../../stack/server"; 3 | 4 | export default function Handler(props: unknown) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { stackServerApp } from "@/stack/server"; 2 | import { redirect } from "next/navigation"; 3 | 4 | export async function getCurrentUser() { 5 | const user = await stackServerApp.getUser(); 6 | if (!user) { 7 | redirect("/sign-in"); 8 | } 9 | 10 | return user; 11 | } 12 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const globalForPrisma = globalThis as unknown as { 4 | prisma: PrismaClient | undefined; 5 | }; 6 | 7 | export const prisma = globalForPrisma.prisma ?? new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 10 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: [ 16 | "node_modules/**", 17 | ".next/**", 18 | "out/**", 19 | "build/**", 20 | "next-env.d.ts", 21 | ], 22 | }, 23 | ]; 24 | 25 | export default eslintConfig; 26 | -------------------------------------------------------------------------------- /app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@stackframe/stack"; 2 | import Link from "next/link"; 3 | import { stackServerApp } from "@/stack/server"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export default async function SignInPage() { 7 | const user = await stackServerApp.getUser(); 8 | if (user) { 9 | redirect("/dashboard"); 10 | } 11 | return ( 12 |
13 |
14 | 15 | Go Back Home 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | /app/generated/prisma 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /prisma/migrations/20250921232434_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "public"."Product" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "name" TEXT NOT NULL, 6 | "sku" TEXT, 7 | "price" DECIMAL(12,2) NOT NULL, 8 | "quantity" INTEGER NOT NULL DEFAULT 0, 9 | "lowStockAt" INTEGER, 10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" TIMESTAMP(3) NOT NULL, 12 | 13 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "Product_sku_key" ON "public"."Product"("sku"); 18 | 19 | -- CreateIndex 20 | CREATE INDEX "Product_userId_name_idx" ON "public"."Product"("userId", "name"); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "Product_createdAt_idx" ON "public"."Product"("createdAt"); 24 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Product { 17 | id String @id @default(cuid()) 18 | userId String // Stack Auth User ID 19 | name String 20 | sku String? @unique 21 | price Decimal @db.Decimal(12,2) 22 | quantity Int @default(0) 23 | lowStockAt Int? 24 | 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | 28 | @@index([userId, name]) 29 | @@index([createdAt]) 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-fullstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build --turbopack", 8 | "start": "next start", 9 | "lint": "eslint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^6.16.2", 13 | "@stackframe/stack": "^2.8.39", 14 | "lucide-react": "^0.544.0", 15 | "next": "15.5.2", 16 | "react": "19.1.0", 17 | "react-dom": "19.1.0", 18 | "recharts": "^3.2.1", 19 | "zod": "^4.1.11" 20 | }, 21 | "devDependencies": { 22 | "@eslint/eslintrc": "^3", 23 | "@tailwindcss/postcss": "^4", 24 | "@types/node": "^20", 25 | "@types/react": "^19", 26 | "@types/react-dom": "^19", 27 | "eslint": "^9", 28 | "eslint-config-next": "15.5.2", 29 | "prisma": "^6.16.2", 30 | "tailwindcss": "^4", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | const prisma = new PrismaClient(); 3 | 4 | async function main() { 5 | const demoUserId = "133767f0-768d-4338-a612-50c8dc722b84"; 6 | 7 | // Create sample products 8 | await prisma.product.createMany({ 9 | data: Array.from({ length: 25 }).map((_, i) => ({ 10 | userId: demoUserId, 11 | name: `Product ${i + 1}`, 12 | price: (Math.random() * 90 + 10).toFixed(2), 13 | quantity: Math.floor(Math.random() * 20), 14 | lowStockAt: 5, 15 | createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * (i * 5)), 16 | })), 17 | }); 18 | 19 | console.log("Seed data created successfully!"); 20 | console.log(`Created 25 products for user ID: ${demoUserId}`); 21 | } 22 | 23 | main() 24 | .catch((e) => { 25 | console.error(e); 26 | process.exit(1); 27 | }) 28 | .finally(async () => { 29 | await prisma.$disconnect(); 30 | }); 31 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { StackProvider, StackTheme } from "@stackframe/stack"; 3 | import { stackServerApp } from "../stack/server"; 4 | import { Geist, Geist_Mono } from "next/font/google"; 5 | import "./globals.css"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "@/components/sidebar"; 2 | import { getCurrentUser } from "@/lib/auth"; 3 | import { AccountSettings } from "@stackframe/stack"; 4 | 5 | export default async function SettingsPage() { 6 | const user = await getCurrentUser(); 7 | 8 | return ( 9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 |

Settings

17 |

18 | Manage your account settings and preferences. 19 |

20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/actions/products.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { getCurrentUser } from "../auth"; 5 | import { prisma } from "../prisma"; 6 | import { z } from "zod"; 7 | 8 | const ProductSchema = z.object({ 9 | name: z.string().min(1, "Name is required"), 10 | price: z.coerce.number().nonnegative("Price must be non-negative"), 11 | quantity: z.coerce.number().int().min(0, "Quantity must be non-negative"), 12 | sku: z.string().optional(), 13 | lowStockAt: z.coerce.number().int().min(0).optional(), 14 | }); 15 | 16 | export async function deleteProduct(formData: FormData) { 17 | const user = await getCurrentUser(); 18 | const id = String(formData.get("id") || ""); 19 | 20 | await prisma.product.deleteMany({ 21 | where: { id: id, userId: user.id }, 22 | }); 23 | } 24 | 25 | export async function createProduct(formData: FormData) { 26 | const user = await getCurrentUser(); 27 | 28 | const parsed = ProductSchema.safeParse({ 29 | name: formData.get("name"), 30 | price: formData.get("price"), 31 | quantity: formData.get("quantity"), 32 | sku: formData.get("sku") || undefined, 33 | lowStockAt: formData.get("lowStockAt") || undefined, 34 | }); 35 | 36 | if (!parsed.success) { 37 | throw new Error("Validation failed"); 38 | } 39 | 40 | try { 41 | await prisma.product.create({ 42 | data: { ...parsed.data, userId: user.id }, 43 | }); 44 | redirect("/inventory"); 45 | } catch (error) { 46 | throw new Error("Failed to create product."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { stackServerApp } from "@/stack/server"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export default async function Home() { 6 | const user = await stackServerApp.getUser(); 7 | if (user) { 8 | redirect("/dashboard"); 9 | } 10 | return ( 11 |
12 |
13 |
14 |

15 | Inventory Management 16 |

17 |

18 | Streamline your inventory tracking with our powerful, easy-to-use 19 | management system. Track products, monitor stock levels, and gain 20 | valuable insights. 21 |

22 |
23 | 27 | Sign In 28 | 29 | 33 | Learn More 34 | 35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/products-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Area, 5 | AreaChart, 6 | CartesianGrid, 7 | ResponsiveContainer, 8 | Tooltip, 9 | XAxis, 10 | YAxis, 11 | } from "recharts"; 12 | 13 | interface ChartData { 14 | week: string; 15 | products: number; 16 | } 17 | 18 | export default function ProductChart({ data }: { data: ChartData[] }) { 19 | console.log(data); 20 | return ( 21 |
22 | 23 | 27 | 28 | 35 | 42 | 43 | 53 | 54 | 63 | 64 | 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@stackframe/stack"; 2 | import { BarChart3, Package, Plus, Settings } from "lucide-react"; 3 | import Link from "next/link"; 4 | 5 | export default function Sidebar({ 6 | currentPath = "/dashboard", 7 | }: { 8 | currentPath: string; 9 | }) { 10 | const navigation = [ 11 | { name: "Dashboard", href: "/dashboard", icon: BarChart3 }, 12 | { name: "Inventory", href: "/inventory", icon: Package }, 13 | { name: "Add Product", href: "/add-product", icon: Plus }, 14 | { name: "Settings", href: "/settings", icon: Settings }, 15 | ]; 16 | return ( 17 |
18 |
19 |
20 | 21 | Inventory App 22 |
23 |
24 | 25 | 48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/pagination.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeft, ChevronRight } from "lucide-react"; 2 | import Link from "next/link"; 3 | 4 | interface PaginationProps { 5 | currentPage: number; 6 | totalPages: number; 7 | baseUrl: string; 8 | searchParams: Record; 9 | } 10 | 11 | export default function Pagination({ 12 | currentPage, 13 | totalPages, 14 | baseUrl, 15 | searchParams, 16 | }: PaginationProps) { 17 | if (totalPages <= 1) return null; 18 | 19 | const getPageUrl = (page: number) => { 20 | const params = new URLSearchParams({ ...searchParams, page: String(page) }); 21 | return `${baseUrl}?${params.toString()}`; 22 | }; 23 | 24 | const getVisiblePages = () => { 25 | const delta = 2; 26 | const range = []; 27 | const rangeWithDots = []; 28 | 29 | for ( 30 | let i = Math.max(2, currentPage - delta); 31 | i <= Math.min(totalPages - 1, currentPage + delta); 32 | i++ 33 | ) { 34 | range.push(i); 35 | } 36 | 37 | if (currentPage - delta > 2) { 38 | rangeWithDots.push(1, "..."); 39 | } else { 40 | rangeWithDots.push(1); 41 | } 42 | 43 | rangeWithDots.push(...range); 44 | 45 | if (currentPage + delta < totalPages - 1) { 46 | rangeWithDots.push("...", totalPages); 47 | } else { 48 | rangeWithDots.push(totalPages); 49 | } 50 | 51 | return rangeWithDots; 52 | }; 53 | 54 | const visiblePages = getVisiblePages(); 55 | 56 | return ( 57 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /app/add-product/page.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "@/components/sidebar"; 2 | import { createProduct } from "@/lib/actions/products"; 3 | import { getCurrentUser } from "@/lib/auth"; 4 | import Link from "next/link"; 5 | 6 | export default async function AddProductPage() { 7 | const user = await getCurrentUser(); 8 | return ( 9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 |

17 | Add Product 18 |

19 |

20 | Add a new product to your inventory 21 |

22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 | 36 | 44 |
45 | 46 |
47 |
48 | 54 | 63 |
64 |
65 | 71 | 81 |
82 |
83 | 84 |
85 | 91 | 98 |
99 | 100 |
101 | 107 | 115 |
116 | 117 |
118 | 124 | 128 | Cancel 129 | 130 |
131 |
132 |
133 |
134 |
135 |
136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /app/inventory/page.tsx: -------------------------------------------------------------------------------- 1 | import Pagination from "@/components/pagination"; 2 | import Sidebar from "@/components/sidebar"; 3 | import { deleteProduct } from "@/lib/actions/products"; 4 | import { getCurrentUser } from "@/lib/auth"; 5 | import { prisma } from "@/lib/prisma"; 6 | 7 | export default async function InventoryPage({ 8 | searchParams, 9 | }: { 10 | searchParams: Promise<{ q?: string; page?: string }>; 11 | }) { 12 | const user = await getCurrentUser(); 13 | const userId = user.id; 14 | 15 | const params = await searchParams; 16 | const q = (params.q ?? "").trim(); 17 | const page = Math.max(1, Number(params.page ?? 1)); 18 | const pageSize = 5; 19 | 20 | const where = { 21 | userId, 22 | ...(q ? { name: { contains: q, mode: "insensitive" as const } } : {}), 23 | }; 24 | 25 | const [totalCount, items] = await Promise.all([ 26 | prisma.product.count({ where }), 27 | prisma.product.findMany({ 28 | where, 29 | orderBy: { createdAt: "desc" }, 30 | skip: (page - 1) * pageSize, 31 | take: pageSize, 32 | }), 33 | ]); 34 | 35 | const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); 36 | 37 | return ( 38 |
39 | 40 |
41 |
42 |
43 |
44 |

45 | Inventory 46 |

47 |

48 | Manage your products and track inventory levels. 49 |

50 |
51 |
52 |
53 | 54 |
55 | {/* Search */} 56 |
57 |
58 | 63 | 66 |
67 |
68 | 69 | {/* Products Table */} 70 |
71 | 72 | 73 | 74 | 77 | 80 | 83 | 86 | 89 | 92 | 93 | 94 | 95 | 96 | {items.map((product, key) => ( 97 | 98 | 101 | 104 | 107 | 110 | 113 | 126 | 127 | ))} 128 | 129 |
75 | Name 76 | 78 | SKU 79 | 81 | Price 82 | 84 | Quantity 85 | 87 | Low Stock At 88 | 90 | Actions 91 |
99 | {product.name} 100 | 102 | {product.sku || "-"} 103 | 105 | ${Number(product.price).toFixed(2)} 106 | 108 | {product.quantity} 109 | 111 | {product.lowStockAt || "-"} 112 | 114 |
{ 116 | "use server"; 117 | await deleteProduct(formData); 118 | }} 119 | > 120 | 121 | 124 |
125 |
130 |
131 | 132 | {totalPages > 1 && ( 133 |
134 | 143 |
144 | )} 145 |
146 |
147 |
148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import { BarChart3, Package, Plus, Settings } from "lucide-react"; 6 | import { UserButton } from "@stackframe/stack"; 7 | 8 | // Skeleton component for loading states 9 | function Skeleton({ className = "" }: { className?: string }) { 10 | return ( 11 |
12 | ); 13 | } 14 | 15 | // Sidebar component for loading state 16 | function LoadingSidebar() { 17 | const navigation = [ 18 | { name: "Dashboard", href: "/dashboard", icon: BarChart3 }, 19 | { name: "Inventory", href: "/inventory", icon: Package }, 20 | { name: "Add Product", href: "/add-product", icon: Plus }, 21 | { name: "Settings", href: "/settings", icon: Settings }, 22 | ]; 23 | 24 | return ( 25 |
26 |
27 |
28 | 29 | Inventory App 30 |
31 |
32 | 33 | 51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | // Main content skeleton 68 | function MainContentSkeleton({ 69 | showSidebar = true, 70 | }: { 71 | showSidebar?: boolean; 72 | }) { 73 | return ( 74 |
75 | {/* Header skeleton */} 76 |
77 | 78 | 79 |
80 | 81 | {/* Key Metrics skeleton */} 82 |
83 |
84 | 85 |
86 | {[1, 2, 3].map((i) => ( 87 |
88 | 89 | 90 |
91 | 92 | 93 |
94 |
95 | ))} 96 |
97 |
98 | 99 |
100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 | {/* Bottom Row skeleton */} 108 |
109 | {/* Stock levels skeleton */} 110 |
111 |
112 | 113 |
114 |
115 | {[1, 2, 3, 4, 5].map((i) => ( 116 |
120 |
121 | 122 | 123 |
124 | 125 |
126 | ))} 127 |
128 |
129 | 130 | {/* Efficiency skeleton */} 131 |
132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 | {[1, 2, 3].map((i) => ( 140 |
141 |
142 | 143 | 144 |
145 |
146 | ))} 147 |
148 |
149 |
150 |
151 | ); 152 | } 153 | 154 | export default function Loading() { 155 | const pathname = usePathname(); 156 | 157 | // Don't show sidebar on public routes 158 | const showSidebar = !["/", "/sign-in", "/sign-up"].includes(pathname); 159 | 160 | return ( 161 |
162 | {showSidebar && } 163 | 164 |
165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a Full-Stack Inventory Management System with Next.js & Stack Auth 2 | 3 |
4 |
5 | 6 | Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of Copy of 10,000 REACT COMPONENTS (5) 7 | 8 |
9 |
10 | Next.js 11 | React 12 | Tailwind CSS 13 | Prisma 14 | PostgreSQL 15 | Stack Auth 16 | Lucide Icons 17 |
18 |

Create a Complete Inventory Management System with Authentication, Dashboard Analytics, and CRUD Operations

19 |
20 | Follow the full video tutorial on YouTube 21 |
22 |
23 |
24 | 25 | ## 📋 Table of Contents 26 | 27 | 1. [Introduction](#-introduction) 28 | 2. [Tech Stack](#-tech-stack) 29 | 3. [Features](#-features) 30 | 4. [Quick Start](#-quick-start) 31 | 5. [Screenshots](#-screenshots) 32 | 6. [Deployment](#-deployment) 33 | 7. [Course & Channel](#-course--channel) 34 | 35 | --- 36 | 37 | ## 🚀 Introduction 38 | 39 | In this comprehensive tutorial, you'll learn how to build a complete **inventory management system** using **Next.js 15**, **Stack Auth**, **Prisma**, and **PostgreSQL**. From user authentication to dashboard analytics, product management, and real-time inventory tracking—this video walks you through every step of building a production-ready full-stack application. 40 | 41 | Perfect for developers looking to master modern web development, learn full-stack architecture, or build their own business management tools. 42 | 43 | 🎥 **Watch the full tutorial**: [YouTube](https://youtu.be/YOUR_VIDEO_ID) 44 | 45 | --- 46 | 47 | ## ⚙️ Tech Stack 48 | 49 | - **Next.js 15** – React framework with App Router and Server Components 50 | - **React 19** – Component-based UI development with latest features 51 | - **TailwindCSS** – Utility-first CSS for modern styling 52 | - **Stack Auth** – Modern authentication solution (replaces NextAuth.js) 53 | - **Prisma** – Type-safe database ORM with migrations 54 | - **PostgreSQL** – Robust relational database 55 | - **Lucide Icons** – Clean and beautiful icon pack 56 | - **Recharts** – Data visualization for analytics 57 | - **TypeScript** – Type safety and enhanced developer experience 58 | - **Vercel** – Deployment and hosting platform 59 | 60 | --- 61 | 62 | ## ⚡️ Features 63 | 64 | - 🔐 **Modern Authentication** - Secure user registration and login with Stack Auth 65 | - 📊 **Dashboard Analytics** - Real-time metrics, charts, and inventory insights 66 | - 📦 **Product Management** - Complete CRUD operations for inventory items 67 | - 🔍 **Search & Filtering** - Find products quickly with search functionality 68 | - 📄 **Pagination** - Efficient data loading for large inventories 69 | - ⚠️ **Low Stock Alerts** - Automated notifications for inventory levels 70 | - 💰 **Value Tracking** - Monitor total inventory value and financial metrics 71 | - 📈 **Visual Analytics** - Interactive charts showing inventory trends 72 | - 📱 **Responsive Design** - Works perfectly on desktop and mobile devices 73 | - 🎨 **Modern UI** - Clean, professional interface with TailwindCSS 74 | - 🚀 **Server Actions** - Form handling with Next.js Server Actions 75 | - 🔄 **Real-time Updates** - Instant UI updates after data changes 76 | 77 | --- 78 | 79 | ## 👌 Quick Start 80 | 81 | ### Prerequisites 82 | 83 | - [Node.js](https://nodejs.org/) (v18 or higher) 84 | - [Git](https://git-scm.com/) 85 | - [PostgreSQL Database](https://www.postgresql.org/) (or use Neon for cloud hosting) 86 | 87 | ### Clone and Run 88 | 89 | ```bash 90 | git clone https://github.com/yourusername/nextjs-fullstack-inventory.git 91 | cd nextjs-fullstack-inventory 92 | npm install 93 | ``` 94 | 95 | ### Environment Setup 96 | 97 | 1. Create a `.env.local` file in the root directory: 98 | 99 | ```env 100 | DATABASE_URL="postgresql://username:password@localhost:5432/inventory_db" 101 | NEXT_PUBLIC_STACK_PROJECT_ID="your_stack_project_id" 102 | NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="your_publishable_key" 103 | STACK_SECRET_SERVER_KEY="your_secret_key" 104 | ``` 105 | 106 | 2. Set up your database: 107 | 108 | ```bash 109 | npx prisma migrate dev 110 | npx prisma generate 111 | ``` 112 | 113 | 3. Start the development server: 114 | 115 | ```bash 116 | npm run dev 117 | ``` 118 | 119 | Your app will be available at: [http://localhost:3000](http://localhost:3000) 120 | 121 | --- 122 | 123 | ## 🖼️ Screenshots 124 | 125 | > 📸 Add screenshots of your Dashboard, Inventory Management, Add Product form, and Analytics charts here to showcase the application. 126 | 127 | --- 128 | 129 | ## ☁️ Deployment 130 | 131 | ### Deploy on Vercel 132 | 133 | 1. Push your code to GitHub 134 | 2. Go to [vercel.com](https://vercel.com) 135 | 3. Import your repository 136 | 4. Add your environment variables in the Vercel dashboard 137 | 5. Click **Deploy** 138 | 139 | Your live application will be hosted on a custom subdomain (e.g. https://your-inventory-app.vercel.app) 140 | 141 | ### Database Setup 142 | 143 | For production, consider using: 144 | 145 | - [Neon](https://neon.tech/) - Serverless PostgreSQL 146 | - [Supabase](https://supabase.com/) - Open source Firebase alternative 147 | - [PlanetScale](https://planetscale.com/) - MySQL-compatible database 148 | 149 | --- 150 | 151 | ## 🎓 Course & Channel 152 | 153 | ### Learn More with Pedro Technologies 154 | 155 | - 🌐 **Course Website**: [www.webdevultra.com](https://www.webdevultra.com) 156 | - 📺 **YouTube Channel**: [www.youtube.com/@pedrotechnologies](https://www.youtube.com/@pedrotechnologies) 157 | 158 | Follow along for more full-stack development tutorials, modern web technologies, and practical coding projects! 159 | 160 | --- 161 | 162 | ## 🔗 Useful Links 163 | 164 | - [Next.js Documentation](https://nextjs.org/docs) 165 | - [Stack Auth Documentation](https://docs.stack-auth.com/) 166 | - [Prisma Documentation](https://www.prisma.io/docs) 167 | - [Tailwind CSS Docs](https://tailwindcss.com/docs) 168 | - [Lucide Icons](https://lucide.dev/) 169 | - [Recharts Documentation](https://recharts.org/) 170 | - [Vercel Deployment Guide](https://vercel.com/docs) 171 | 172 | --- 173 | 174 | ## 📝 License 175 | 176 | This project is open source and available under the [MIT License](LICENSE). 177 | 178 | --- 179 | 180 | **Happy Coding!** 🚀 181 | 182 | Let me know if you'd like me to generate a version with your actual GitHub repo, YouTube URL, or banner image suggestions! 183 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductsChart from "@/components/products-chart"; 2 | import Sidebar from "@/components/sidebar"; 3 | import { getCurrentUser } from "@/lib/auth"; 4 | import { prisma } from "@/lib/prisma"; 5 | import { TrendingUp } from "lucide-react"; 6 | 7 | export default async function DashboardPage() { 8 | const user = await getCurrentUser(); 9 | const userId = user.id; 10 | 11 | const [totalProducts, lowStock, allProducts] = await Promise.all([ 12 | prisma.product.count({ where: { userId } }), 13 | prisma.product.count({ 14 | where: { 15 | userId, 16 | lowStockAt: { not: null }, 17 | quantity: { lte: 5 }, 18 | }, 19 | }), 20 | prisma.product.findMany({ 21 | where: { userId }, 22 | select: { price: true, quantity: true, createdAt: true }, 23 | }), 24 | ]); 25 | 26 | const totalValue = allProducts.reduce( 27 | (sum, product) => sum + Number(product.price) * Number(product.quantity), 28 | 0 29 | ); 30 | 31 | const inStockCount = allProducts.filter((p) => Number(p.quantity) > 5).length; 32 | const lowStockCount = allProducts.filter( 33 | (p) => Number(p.quantity) <= 5 && Number(p.quantity) >= 1 34 | ).length; 35 | const outOfStockCount = allProducts.filter( 36 | (p) => Number(p.quantity) === 0 37 | ).length; 38 | 39 | const inStockPercentage = 40 | totalProducts > 0 ? Math.round((inStockCount / totalProducts) * 100) : 0; 41 | const lowStockPercentage = 42 | totalProducts > 0 ? Math.round((lowStockCount / totalProducts) * 100) : 0; 43 | const outOfStockPercentage = 44 | totalProducts > 0 ? Math.round((outOfStockCount / totalProducts) * 100) : 0; 45 | 46 | const now = new Date(); 47 | const weeklyProductsData = []; 48 | 49 | for (let i = 11; i >= 0; i--) { 50 | const weekStart = new Date(now); 51 | weekStart.setDate(weekStart.getDate() - i * 7); 52 | weekStart.setHours(0, 0, 0, 0); 53 | 54 | const weekEnd = new Date(weekStart); 55 | weekEnd.setDate(weekEnd.getDate() + 6); 56 | weekStart.setHours(23, 59, 59, 999); 57 | 58 | const weekLabel = `${String(weekStart.getMonth() + 1).padStart( 59 | 2, 60 | "0" 61 | )}/${String(weekStart.getDate() + 1).padStart(2, "0")}`; 62 | 63 | const weekProducts = allProducts.filter((product) => { 64 | const productDate = new Date(product.createdAt); 65 | return productDate >= weekStart && productDate <= weekEnd; 66 | }); 67 | 68 | weeklyProductsData.push({ 69 | week: weekLabel, 70 | products: weekProducts.length, 71 | }); 72 | } 73 | 74 | const recent = await prisma.product.findMany({ 75 | where: { userId }, 76 | orderBy: { createdAt: "desc" }, 77 | take: 5, 78 | }); 79 | 80 | console.log(totalValue); 81 | 82 | return ( 83 |
84 | 85 |
86 | {/* Header */} 87 |
88 |
89 |
90 |

91 | Dashboard 92 |

93 |

94 | Welcome back! Here is an overview of your inventory. 95 |

96 |
97 |
98 |
99 | 100 |
101 | {/* Key Metrics */} 102 |
103 |

104 | Key Metrics 105 |

106 |
107 |
108 |
109 | {totalProducts} 110 |
111 |
Total Products
112 |
113 | 114 | +{totalProducts} 115 | 116 | 117 |
118 |
119 | 120 |
121 |
122 | ${Number(totalValue).toFixed(0)} 123 |
124 |
Total Value
125 |
126 | 127 | +${Number(totalValue).toFixed(0)} 128 | 129 | 130 |
131 |
132 | 133 |
134 |
135 | {lowStock} 136 |
137 |
Low Stock
138 |
139 | +{lowStock} 140 | 141 |
142 |
143 |
144 |
145 | 146 | {/* Iventory over time */} 147 |
148 |
149 |

New products per week

150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 |
158 | {/* Stock Levels */} 159 |
160 |
161 |

162 | Stock Levels 163 |

164 |
165 |
166 | {recent.map((product, key) => { 167 | const stockLevel = 168 | product.quantity === 0 169 | ? 0 170 | : product.quantity <= (product.lowStockAt || 5) 171 | ? 1 172 | : 2; 173 | 174 | const bgColors = [ 175 | "bg-red-600", 176 | "bg-yellow-600", 177 | "bg-green-600", 178 | ]; 179 | const textColors = [ 180 | "text-red-600", 181 | "text-yellow-600", 182 | "text-green-600", 183 | ]; 184 | return ( 185 |
189 |
190 |
193 | 194 | {product.name} 195 | 196 |
197 |
200 | {product.quantity} units 201 |
202 |
203 | ); 204 | })} 205 |
206 |
207 | 208 | {/* Efficiency */} 209 |
210 |
211 |

212 | Efficiency 213 |

214 |
215 |
216 |
217 |
218 |
225 |
226 |
227 |
228 | {inStockPercentage}% 229 |
230 |
In Stock
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | In Stock ({inStockPercentage}%) 240 |
241 |
242 |
243 |
244 |
245 | Low Stock ({lowStockPercentage}%) 246 |
247 |
248 |
249 |
250 |
251 | Out of Stock ({outOfStockPercentage}%) 252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | ); 260 | } 261 | --------------------------------------------------------------------------------