├── app ├── components │ ├── atoms │ │ ├── navbar.tsx │ │ ├── README.md │ │ ├── language-item.tsx │ │ ├── summary-card.tsx │ │ ├── kbd.tsx │ │ ├── nav-item.tsx │ │ └── collapsible-item.tsx │ ├── blocks │ │ ├── README.md │ │ ├── crud-list.tsx │ │ ├── profile-dropdown.tsx │ │ ├── date-range-picker.tsx │ │ ├── data-table.tsx │ │ ├── md-sm-sidebar.tsx │ │ └── sidebar.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── toggle.tsx │ │ ├── avatar.tsx │ │ ├── toggle-group.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── calendar.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── sheet.tsx │ │ ├── command.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ ├── global-pending-indicator.tsx │ ├── shell.tsx │ ├── layouts │ │ └── MainLayout.tsx │ └── theme-switcher.tsx ├── auth │ └── README.md ├── lib │ ├── constants.ts │ ├── fonts.ts │ ├── styles.ts │ ├── seed.ts │ ├── handle-error.ts │ ├── utils.ts │ ├── id.ts │ ├── validations.ts │ ├── filter-column.ts │ ├── export.ts │ ├── xlsx.ts │ └── queries.ts ├── routes │ ├── contributors.create │ │ └── route.tsx │ ├── healthcheck.ts │ ├── contributors.$id.view │ │ └── route.tsx │ ├── configure.categories.create │ │ └── route.tsx │ ├── configure │ │ ├── route.tsx │ │ └── components │ │ │ └── columns.tsx │ ├── contributors │ │ ├── components │ │ │ ├── progress-bar.tsx │ │ │ ├── contributor-summary-card.tsx │ │ │ ├── attendee-heat-map.tsx │ │ │ ├── detail.tsx │ │ │ ├── contributor-dropdown.tsx │ │ │ └── columns.tsx │ │ └── route.tsx │ ├── _index │ │ ├── route.tsx │ │ └── components │ │ │ ├── recent-salse.tsx │ │ │ ├── dashboard.tsx │ │ │ └── transactions-table.tsx │ ├── contributors.$id.edit │ │ └── route.tsx │ └── inventory.tsx ├── config.shared.ts ├── data │ ├── db │ │ ├── index.ts │ │ ├── seed.ts │ │ ├── migrate.ts │ │ ├── utils.ts │ │ ├── schema.ts │ │ └── dummy-data.ts │ └── contributors │ │ ├── contributors-summary.ts │ │ └── data.ts ├── entry.client.tsx ├── hooks │ └── use-debounce.ts ├── @types │ ├── category.d.ts │ ├── index.d.ts │ └── contributors.d.ts ├── services │ ├── contributors │ │ └── contributor-service.ts │ └── category │ │ └── category-service.ts ├── config │ ├── nav.ts │ └── data-table.ts ├── root.tsx ├── entry.server.tsx └── globals.css ├── .vscode ├── extensions.json └── settings.json ├── public ├── favicon.ico └── famcon-logo.png ├── .dockerignore ├── .gitignore ├── postcss.config.js ├── biome.json ├── components.json ├── keynotes.md ├── .github └── workflows │ └── code_quality.yaml ├── vite.config.ts ├── tsconfig.json ├── fly.toml ├── Dockerfile ├── server.js ├── package.json ├── docs └── CRUD.md ├── README.md └── tailwind.config.cjs /app/components/atoms/navbar.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/auth/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Auth 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwoo/cell-connect/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .database 3 | .DS_Store 4 | .env 5 | *.log 6 | build 7 | node_modules 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .database 3 | .DS_Store 4 | .env 5 | *.log 6 | build 7 | node_modules 8 | -------------------------------------------------------------------------------- /public/famcon-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwoo/cell-connect/HEAD/public/famcon-logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const unknownError = "An unknown error occurred. Please try again later." 2 | 3 | export const databasePrefix = "shadcn" 4 | -------------------------------------------------------------------------------- /app/routes/contributors.create/route.tsx: -------------------------------------------------------------------------------- 1 | export default function ContributorCreate() { 2 | // Implement create form 3 | return
Create Contributor
; 4 | } 5 | -------------------------------------------------------------------------------- /app/routes/healthcheck.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return new Response("OK", { 3 | status: 200, 4 | headers: { 5 | "Content-Type": "text/plain", 6 | }, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /app/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { GeistMono } from "geist/font/mono"; 2 | import { GeistSans } from "geist/font/sans"; 3 | 4 | export const fontSans = GeistSans; 5 | export const fontMono = GeistMono; 6 | -------------------------------------------------------------------------------- /app/routes/contributors.$id.view/route.tsx: -------------------------------------------------------------------------------- 1 | export default function ContributorView({ id }: { id: string }) { 2 | // Implement view details 3 | return
View Contributor {id}
; 4 | } 5 | -------------------------------------------------------------------------------- /app/components/blocks/README.md: -------------------------------------------------------------------------------- 1 | ## Blocks 2 | Blocks are components that belong to a specific page. They are reusable and can be used in multiple pages. They are located in the `app/blocks` directory. 3 | -------------------------------------------------------------------------------- /app/config.shared.ts: -------------------------------------------------------------------------------- 1 | export const APP_NAME = "remix-shadcn"; 2 | 3 | export function title(pageTitle?: string) { 4 | if (!pageTitle) return APP_NAME; 5 | 6 | return `${pageTitle} | ${APP_NAME}`; 7 | } 8 | -------------------------------------------------------------------------------- /app/routes/configure.categories.create/route.tsx: -------------------------------------------------------------------------------- 1 | export default function CreateCategory() { 2 | return ( 3 |
4 |

Create Category

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/lib/styles.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 | -------------------------------------------------------------------------------- /app/data/db/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env.js"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import postgres from "postgres"; 4 | 5 | import * as schema from "./schema"; 6 | 7 | const client = postgres(env.DATABASE_URL); 8 | export const db = drizzle(client, { schema }); 9 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "files": { 4 | "ignore": ["build/**"] 5 | }, 6 | "organizeImports": { 7 | "enabled": true 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { StrictMode, startTransition } from "react"; 3 | import { hydrateRoot } from "react-dom/client"; 4 | 5 | startTransition(() => { 6 | hydrateRoot( 7 | document, 8 | 9 | 10 | , 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "[yaml]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[markdown]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[toml]": { 10 | "editor.defaultFormatter": "tamasfe.even-better-toml" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/routes/configure/route.tsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@/components/layouts/MainLayout'; 2 | 3 | export default function Configure() { 4 | return ( 5 | 6 |
7 |

Configure

8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/styles" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /app/components/atoms/README.md: -------------------------------------------------------------------------------- 1 | ## Atoms 2 | Atoms are very small components that are used to build more complex components. They are the basic building blocks of matter. They are the simplest form of components and cannot be broken down any further. They often include things like buttons, inputs, or headings. They are the basic elements that make up all other components. 3 | -------------------------------------------------------------------------------- /app/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /app/components/atoms/language-item.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLProps } from "react"; 2 | 3 | interface LanguageItemProps extends HTMLProps { 4 | languageProp: string; 5 | } 6 | 7 | export const LanguageItem = ({ languageProp, ...props }: LanguageItemProps) => { 8 | return ( 9 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/styles" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /keynotes.md: -------------------------------------------------------------------------------- 1 | DYNAMIC FILL-UP GRID: 2 | grid-cols-[320px_1fr]: This part defines the custom configuration for the number of columns in a grid layout. In this case, it sets two columns with specific sizes: 3 | 4 | 320px: The first column is a fixed width of 320 pixels. 5 | 1fr: The second column takes up the remaining available space (fractional unit), which means it will grow to fill the remaining area of the container. 6 | -------------------------------------------------------------------------------- /.github/workflows/code_quality.yaml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | quality: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Biome 14 | uses: biomejs/setup-biome@v2 15 | with: 16 | version: latest 17 | - name: Run Biome 18 | run: biome ci . 19 | -------------------------------------------------------------------------------- /app/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value) 5 | 6 | React.useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500) 8 | 9 | return () => { 10 | clearTimeout(timer) 11 | } 12 | }, [value, delay]) 13 | 14 | return debouncedValue 15 | } -------------------------------------------------------------------------------- /app/@types/category.d.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id: string; 3 | title: string; 4 | description: string; 5 | minimumIndivudualContribution: number; 6 | createAt: Date; 7 | deletedAt: Date; 8 | createdBy: string; 9 | isPublished: boolean; 10 | endDate: Date; 11 | } 12 | 13 | export interface ListableCategory { 14 | id: string; 15 | name: string; 16 | description: string; 17 | finishedDate: string; 18 | } 19 | -------------------------------------------------------------------------------- /app/data/contributors/contributors-summary.ts: -------------------------------------------------------------------------------- 1 | import { ContributorsDetailsProps } from '@/@types/contributors'; 2 | 3 | export const ContributorsSummaryData: ContributorsDetailsProps[] = [ 4 | { title: 'Male Contributors', value: '1.382', color: 'blue' }, 5 | { title: 'Female Contributors', value: '194', color: 'green' }, 6 | { title: 'Average Contributors', value: '283', color: 'red' }, 7 | { title: 'Average Dependents', value: '35', color: 'orange' }, 8 | ]; 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from "@remix-run/dev"; 2 | import { defineConfig } from "vite"; 3 | import envOnly from "vite-env-only"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | envOnly(), 9 | tsconfigPaths(), 10 | remix({ 11 | future: { 12 | v3_fetcherPersist: true, 13 | v3_relativeSplatPath: true, 14 | v3_throwAbortReason: true, 15 | }, 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /app/data/db/seed.ts: -------------------------------------------------------------------------------- 1 | import { seedTasks } from "@/app/_lib/seeds"; 2 | 3 | async function runSeed() { 4 | console.log("⏳ Running seed..."); 5 | 6 | const start = Date.now(); 7 | 8 | await seedTasks({ count: 100 }); 9 | 10 | const end = Date.now(); 11 | 12 | console.log(`✅ Seed completed in ${end - start}ms`); 13 | 14 | process.exit(0); 15 | } 16 | 17 | runSeed().catch((err) => { 18 | console.error("❌ Seed failed"); 19 | console.error(err); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /app/routes/contributors/components/progress-bar.tsx: -------------------------------------------------------------------------------- 1 | export const ProgressBarPrototype = () => { 2 | return ( 3 |
4 | 5 | 6 | 7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /app/routes/_index/route.tsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@/components/layouts/MainLayout'; 2 | import { MetaFunction } from '@remix-run/react'; 3 | import Dashboard from './components/dashboard'; 4 | 5 | export const meta: MetaFunction = () => { 6 | return [ 7 | { title: 'Welcome to FamCon' }, 8 | { name: 'description', content: 'Welcome to Family Contributor!' }, 9 | ]; 10 | }; 11 | 12 | export default function Index() { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/components/global-pending-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@remix-run/react"; 2 | 3 | import { cn } from "@/lib/styles"; 4 | 5 | export function GlobalPendingIndicator() { 6 | const navigation = useNavigation(); 7 | const pending = navigation.state !== "idle"; 8 | 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/data/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/postgres-js/migrator"; 2 | 3 | import { db } from "."; 4 | 5 | export async function runMigrate() { 6 | console.log("⏳ Running migrations..."); 7 | 8 | const start = Date.now(); 9 | 10 | await migrate(db, { migrationsFolder: "drizzle" }); 11 | 12 | const end = Date.now(); 13 | 14 | console.log(`✅ Migrations completed in ${end - start}ms`); 15 | 16 | process.exit(0); 17 | } 18 | 19 | runMigrate().catch((err) => { 20 | console.error("❌ Migration failed"); 21 | console.error(err); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /app/routes/configure/components/columns.tsx: -------------------------------------------------------------------------------- 1 | import { ListableCategory } from '@/@types/category'; 2 | import { ColumnDef } from '@tanstack/react-table'; 3 | 4 | export const categoryColumns: ColumnDef[] = [ 5 | { 6 | accessorKey: 'id', 7 | header: 'Id', 8 | }, 9 | { 10 | accessorKey: 'name', 11 | header: 'Name', 12 | }, 13 | { 14 | accessorKey: 'description', 15 | header: 'Description', 16 | }, 17 | { 18 | accessorKey: 'finishedDate', 19 | header: 'Finished Date', 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /app/lib/seed.ts: -------------------------------------------------------------------------------- 1 | // import { db } from "@/db"; 2 | // import { tasks, type Task } from "@/db/schema"; 3 | 4 | // import { generateRandomTask } from "./utils"; 5 | 6 | // export async function seedTasks(input: { count: number }) { 7 | // const count = input.count ?? 100; 8 | 9 | // try { 10 | // const allTasks: Task[] = []; 11 | 12 | // for (let i = 0; i < count; i++) { 13 | // allTasks.push(generateRandomTask()); 14 | // } 15 | 16 | // await db.delete(tasks); 17 | 18 | // console.log("📝 Inserting tasks", allTasks.length); 19 | 20 | // await db.insert(tasks).values(allTasks); 21 | // } catch (err) { 22 | // console.error(err); 23 | // } 24 | // } 25 | -------------------------------------------------------------------------------- /app/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | 3 | export interface Identifiable { 4 | id: string; 5 | } 6 | 7 | interface NavItemProps { 8 | label: string; 9 | icon: LucideIcon; 10 | href: string; 11 | children?: NavItemProps[]; 12 | } 13 | 14 | export interface ICrudService< 15 | TListable extends Identifiable, 16 | TCreateUpdate extends Identifiable, 17 | TModel extends Identifiable 18 | > { 19 | getAll(): Promise; 20 | getById(id: string): Promise; 21 | create(item: Omit): Promise; 22 | update(id: string, item: Partial): Promise; 23 | delete(id: string): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx", "server.js"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "types": ["@remix-run/node", "node", "vite/client"], 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "resolveJsonModule": true, 12 | "target": "ES2022", 13 | "strict": true, 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./app/*"] 20 | }, 21 | 22 | // Vite takes care of building everything, not tsc. 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/handle-error.ts: -------------------------------------------------------------------------------- 1 | import { isRedirectError } from "next/dist/client/components/redirect"; 2 | import { toast } from "sonner"; 3 | import { z } from "zod"; 4 | 5 | export function getErrorMessage(err: unknown) { 6 | const unknownError = "Something went wrong, please try again later."; 7 | 8 | if (err instanceof z.ZodError) { 9 | const errors = err.issues.map((issue) => { 10 | return issue.message; 11 | }); 12 | return errors.join("\n"); 13 | } else if (err instanceof Error) { 14 | return err.message; 15 | } else if (isRedirectError(err)) { 16 | throw err; 17 | } else { 18 | return unknownError; 19 | } 20 | } 21 | 22 | export function showErrorToast(err: unknown) { 23 | const errorMessage = getErrorMessage(err); 24 | return toast.error(errorMessage); 25 | } 26 | -------------------------------------------------------------------------------- /app/routes/contributors.$id.edit/route.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, json } from '@remix-run/node'; 2 | import { useLoaderData } from '@remix-run/react'; 3 | 4 | // -> Loaders get called when the route is hit 5 | // from the server side 6 | export const loader: LoaderFunction = async ({ params }) => { 7 | // Fetch data from the server 8 | const userId = params.id as string; 9 | 10 | return json({ userId }); 11 | }; 12 | 13 | // -> Actions get called when a form is submitted 14 | // from the server side 15 | 16 | // -> Default components get rendered AFTER the 17 | // page is loaded on the client side 18 | export default function ContributorEdit() { 19 | const { userId } = useLoaderData(); 20 | // Implement edit form 21 | return
Edit Contributor {userId}
; 22 | } 23 | -------------------------------------------------------------------------------- /app/routes/contributors/components/contributor-summary-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@/components/ui/card'; 2 | import { AttendeeHeatMap } from './attendee-heat-map'; 3 | import { ContributorDropdown } from './contributor-dropdown'; 4 | 5 | export const ContributorSummaryCard = () => { 6 | return ( 7 | 8 |
9 |
10 |

11 | Total Contributors 12 |

13 | 14 |
15 | 16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/styles" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /app/routes/inventory.tsx: -------------------------------------------------------------------------------- 1 | import MainLayout from "@/components/layouts/MainLayout"; 2 | import { Button } from "@/components/ui/button"; 3 | 4 | export default function Inventory() { 5 | return ( 6 | 7 |
11 |
12 |

13 | You have no products Inventory 14 |

15 |

16 | You can start selling as soon as you add a product. 17 |

18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/data/db/utils.ts: -------------------------------------------------------------------------------- 1 | import { pgTableCreator } from "drizzle-orm/pg-core"; 2 | 3 | import { databasePrefix } from "@/lib/constants"; 4 | 5 | /** 6 | * This lets us use the multi-project schema feature of Drizzle ORM. So the same 7 | * database instance can be used for multiple projects. 8 | * 9 | * @see https://orm.drizzle.team/docs/goodies#multi-project-schema 10 | */ 11 | export const pgTable = pgTableCreator((name) => `${databasePrefix}_${name}`); 12 | 13 | // @see https://gist.github.com/rphlmr/0d1722a794ed5a16da0fdf6652902b15 14 | 15 | export function takeFirst(items: T[]) { 16 | return items.at(0); 17 | } 18 | 19 | export function takeFirstOrThrow(items: T[]) { 20 | const first = takeFirst(items); 21 | 22 | if (!first) { 23 | throw new Error("First item not found"); 24 | } 25 | 26 | return first; 27 | } 28 | -------------------------------------------------------------------------------- /app/@types/contributors.d.ts: -------------------------------------------------------------------------------- 1 | import { Identifiable } from '.'; 2 | 3 | export interface TPartialContributor extends Identifiable { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | email: string; 8 | gender: string; 9 | dependent: boolean; 10 | contributionAmount: number; 11 | contributiionMethod: 'monthly' | 'annual'; 12 | dateJoined: Date; 13 | } 14 | 15 | export interface TContributor extends Identifiable { 16 | id: string; 17 | firstName: string; 18 | lastName: string; 19 | email: string; 20 | gender: string; 21 | dependent: boolean; 22 | contributionAmount: number; 23 | contributiionMethod: 'monthly' | 'annual'; 24 | dateJoined: Date; 25 | } 26 | 27 | interface ContributorsDetailsProps { 28 | title: string; 29 | value: string; 30 | color: string; 31 | } 32 | -------------------------------------------------------------------------------- /app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/styles" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |