├── 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 |
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 |
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 |
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 |
67 |
68 |
69 | {/* Products Table */}
70 |
71 |
72 |
73 |
74 | |
75 | Name
76 | |
77 |
78 | SKU
79 | |
80 |
81 | Price
82 | |
83 |
84 | Quantity
85 | |
86 |
87 | Low Stock At
88 | |
89 |
90 | Actions
91 | |
92 |
93 |
94 |
95 |
96 | {items.map((product, key) => (
97 |
98 | |
99 | {product.name}
100 | |
101 |
102 | {product.sku || "-"}
103 | |
104 |
105 | ${Number(product.price).toFixed(2)}
106 | |
107 |
108 | {product.quantity}
109 | |
110 |
111 | {product.lowStockAt || "-"}
112 | |
113 |
114 |
125 | |
126 |
127 | ))}
128 |
129 |
130 |
131 |
132 | {totalPages > 1 && (
133 |
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 |
7 |
8 |
9 |
10 |

11 |

12 |

13 |

14 |

15 |

16 |

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