├── lib ├── constants.ts ├── utils.ts ├── types.ts ├── prisma.ts ├── data.ts ├── actionResponse.ts ├── createMetadataObject.ts └── actions.ts ├── app ├── icon.png ├── favicon.ico ├── apple-icon.png ├── page.tsx ├── loading.tsx ├── layout.tsx ├── providers.tsx ├── api │ ├── images │ │ └── route.ts │ └── seed │ │ └── route.ts └── globals.css ├── postcss.config.js ├── vercel.json ├── .prettierrc ├── .eslintrc.json ├── components ├── modal │ ├── useModal.tsx │ ├── ModalFooter.tsx │ ├── index.tsx │ ├── hooks │ │ ├── useFixBackground.ts │ │ └── useWindowSize.ts │ ├── ModalContext.tsx │ ├── ModalTitle.tsx │ ├── ModalHeader.tsx │ ├── InterceptionModal.tsx │ ├── ModalContent.tsx │ ├── DesktopModal.tsx │ ├── Modal.tsx │ ├── ModalCloseButton.tsx │ ├── ModalBackground.tsx │ ├── ModalPortal.tsx │ ├── ModalProvider.tsx │ └── MobileModal.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── checkbox.tsx │ ├── badge.tsx │ ├── popover.tsx │ ├── button.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── form.tsx │ └── command.tsx ├── CreateProductButton.tsx ├── EmptyState.tsx ├── layout │ └── Container.tsx ├── Header.tsx ├── form │ ├── CheckboxForm.tsx │ ├── ImageUploadPlaceholder.tsx │ ├── FileInput.tsx │ ├── ImageUploadField.tsx │ └── CategoryComboBox.tsx ├── SearchInput.tsx ├── ProductsTable.tsx └── ProductForm.tsx ├── components.json ├── .gitignore ├── next.config.js ├── public ├── vercel.svg ├── next.svg ├── image-placeholder 1.svg └── image-placeholder.svg ├── tsconfig.json ├── prisma └── schema.prisma ├── package.json ├── tailwind.config.ts └── README.md /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE = 25; 2 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasjoho/inventory-app/HEAD/app/icon.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasjoho/inventory-app/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasjoho/inventory-app/HEAD/app/apple-icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "crons": [ 3 | { 4 | "path": "/api/seed", 5 | "schedule": "0 5 * * *" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Product = { 2 | id: string; 3 | name: string; 4 | description: string; 5 | price: number; 6 | stock: number; 7 | isDeal: boolean; 8 | imageUrl: string; 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals", "eslint:recommended"], 3 | "globals": { 4 | "React": "readonly" 5 | }, 6 | "rules": { 7 | "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/modal/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ModalContext } from "./ModalContext"; 3 | 4 | export function useModal() { 5 | const context = useContext(ModalContext); 6 | if (!context) { 7 | throw new Error("useModal must be used within a ModalProvider"); 8 | } 9 | return context; 10 | } 11 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components/modal/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | interface ModalFooterProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export function ModalFooter({ children }: ModalFooterProps) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | 13 | // -mt-4 md:-mt-6 14 | -------------------------------------------------------------------------------- /components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | export { Modal } from './Modal'; 3 | export { ModalContent } from './ModalContent'; 4 | export { ModalFooter } from './ModalFooter'; 5 | export { ModalHeader } from './ModalHeader'; 6 | export { ModalProvider } from './ModalProvider'; 7 | export { ModalTitle } from './ModalTitle'; 8 | export { useModal } from './useModal'; 9 | -------------------------------------------------------------------------------- /components/modal/hooks/useFixBackground.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | const useFixBackground = (open: any) => { 4 | return useEffect(() => { 5 | if (open) { 6 | document.body.style.overflow = "hidden"; 7 | } 8 | if (!open) { 9 | document.body.style.overflow = "unset"; 10 | } 11 | }, [open]); 12 | }; 13 | 14 | export default useFixBackground; 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | prisma = new PrismaClient(); 7 | } else { 8 | // @ts-ignore 9 | if (!global.prisma) { 10 | // @ts-ignore 11 | 12 | global.prisma = new PrismaClient(); 13 | } 14 | // @ts-ignore 15 | 16 | prisma = global.prisma; 17 | } 18 | 19 | export { prisma }; 20 | -------------------------------------------------------------------------------- /components/CreateProductButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { Button } from './ui/button'; 4 | import { useModal } from './modal'; 5 | import ProductForm from './ProductForm'; 6 | 7 | const CreateProductButton = () => { 8 | const { show } = useModal(); 9 | return ; 10 | }; 11 | 12 | export default CreateProductButton; 13 | -------------------------------------------------------------------------------- /components/modal/ModalContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const ModalContext = createContext<{ 4 | show: (content: React.ReactNode) => void; 5 | hide: () => void; 6 | isOpen: boolean; 7 | 8 | isActiveInterception: boolean; 9 | setIsActiveInterception: (value: boolean) => void; 10 | }>({ 11 | show: () => {}, 12 | hide: () => {}, 13 | isOpen: false, 14 | 15 | isActiveInterception: false, 16 | setIsActiveInterception: () => {}, 17 | }); 18 | -------------------------------------------------------------------------------- /components/modal/ModalTitle.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { forwardRef } from 'react'; 3 | 4 | export const ModalTitle = forwardRef< 5 | HTMLParagraphElement, 6 | React.HTMLAttributes 7 | >(({ className, ...props }, ref) => ( 8 |

16 | )); 17 | 18 | ModalTitle.displayName = 'ModalTitle'; 19 | -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | export const products = [ 2 | { 3 | name: "Portable Speakerz", 4 | description: "Wireless portable speaker with a long battery life.", 5 | price: 100, 6 | stock: 22, 7 | categoryId: null, 8 | visibility: false, 9 | }, 10 | ]; 11 | 12 | export const categories = [ 13 | { 14 | name: "T-Shirt", 15 | }, 16 | { 17 | name: "Shirt", 18 | }, 19 | { 20 | name: "Pants", 21 | }, 22 | { 23 | name: "Sweatshirt", 24 | }, 25 | { 26 | name: "Shoes", 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /.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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EmptyState = () => { 4 | return ( 5 |
6 |

7 | No products available for given search query. 8 |

9 | 14 |
15 | ); 16 | }; 17 | 18 | export default EmptyState; 19 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true, 5 | }, 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: 'https', 10 | hostname: 'uplift-images.s3.amazonaws.com', 11 | }, 12 | { 13 | protocol: 'https', 14 | hostname: 'fakestoreapi.com', 15 | }, 16 | { 17 | protocol: 'https', 18 | hostname: 'loremflickr.com', 19 | }, 20 | ], 21 | }, 22 | }; 23 | 24 | module.exports = nextConfig; 25 | -------------------------------------------------------------------------------- /components/modal/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ModalCloseButton } from './ModalCloseButton'; 2 | 3 | interface ModalHeaderProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export function ModalHeader({ children }: ModalHeaderProps) { 8 | return ( 9 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/modal/InterceptionModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect } from 'react'; 3 | import { Modal, useModal } from '.'; 4 | 5 | const InterceptionModal = ({ children }: any) => { 6 | const { show, hide, setIsActiveInterception } = useModal(); 7 | useEffect(() => { 8 | setIsActiveInterception(true); 9 | return () => setIsActiveInterception(false); 10 | }, []); 11 | useEffect(() => { 12 | show({children}); 13 | return () => hide(); 14 | }, []); 15 | return <>; 16 | }; 17 | 18 | export default InterceptionModal; 19 | -------------------------------------------------------------------------------- /components/modal/ModalContent.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | interface ModalContentProps extends React.HTMLAttributes { 4 | children: React.ReactNode; 5 | } 6 | 7 | export function ModalContent({ children, ...props }: ModalContentProps) { 8 | const { className, ...rest } = props; 9 | return ( 10 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/layout/Container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React, { FC } from "react"; 3 | 4 | interface ContainerProps extends React.HTMLProps { 5 | children: React.ReactNode | React.ReactNode[]; 6 | } 7 | 8 | const Container: FC = ({ children, ...props }) => { 9 | return ( 10 |
16 | {children} 17 |
18 | ); 19 | }; 20 | 21 | export default Container; 22 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/modal/DesktopModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | 4 | interface DesktopModalProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export function DesktopModal({ children }: DesktopModalProps) { 9 | return ( 10 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 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 | -------------------------------------------------------------------------------- /lib/actionResponse.ts: -------------------------------------------------------------------------------- 1 | type ActionResponseType = { 2 | success: boolean; 3 | message: string; 4 | data?: any; 5 | status?: number; 6 | }; 7 | 8 | class ActionResponse { 9 | success: boolean; 10 | message: string; 11 | data: any; 12 | 13 | private constructor(success: boolean, message: string, data: any) { 14 | this.success = success; 15 | this.message = message; 16 | this.data = data; 17 | } 18 | 19 | static success(message: string, data?: any): ActionResponseType { 20 | return new ActionResponse(true, message, data); 21 | } 22 | static error(message: string, data?: any): ActionResponseType { 23 | return new ActionResponse(false, message, data); 24 | } 25 | } 26 | 27 | export default ActionResponse; 28 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from '@/components/layout/Container'; 2 | import CreateProductButton from '@/components/CreateProductButton'; 3 | import ProductsTable from '@/components/ProductsTable'; 4 | import SearchInput from '@/components/SearchInput'; 5 | import { prisma } from '@/lib/prisma'; 6 | import { getProducts } from '@/lib/actions'; 7 | import Header from '@/components/Header'; 8 | 9 | export default async function Home({ searchParams }: { searchParams: any }) { 10 | const { search } = searchParams; 11 | const products = await getProducts({ search }); 12 | return ( 13 | 14 |
15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface ModalProps 6 | extends React.HTMLAttributes { 7 | children: React.ReactNode; 8 | as?: 'div' | 'form'; 9 | id?: string; 10 | } 11 | 12 | export function Modal({ children, as = 'div', id, ...props }: ModalProps) { 13 | const { className, ...rest } = props; 14 | const defaultClassNames = 15 | 'relative flex flex-col justify-between bg-background'; 16 | if (as === 'form') { 17 | return ( 18 |
19 | {children} 20 |
21 | ); 22 | } 23 | return ( 24 |
25 | {children} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchInput from './SearchInput'; 3 | import CreateProductButton from './CreateProductButton'; 4 | 5 | const Header = () => { 6 | return ( 7 |
8 |
9 |

Inventory App

10 | 15 | View Github project 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |