{"id":18895888,"url":"https://github.com/echobind/bison-remix","last_synced_at":"2025-07-13T08:38:11.073Z","repository":{"id":206561567,"uuid":"709938879","full_name":"echobind/bison-remix","owner":"echobind","description":"A full-stack starter by Echobind","archived":false,"fork":false,"pushed_at":"2023-10-25T18:09:39.000Z","size":180,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-02-16T12:13:56.122Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/echobind.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-25T17:37:00.000Z","updated_at":"2023-11-10T23:18:55.000Z","dependencies_parsed_at":"2023-11-10T20:25:34.487Z","dependency_job_id":"216bc4bf-62a1-40c0-a08d-93573faa8623","html_url":"https://github.com/echobind/bison-remix","commit_stats":null,"previous_names":["echobind/bison-remix"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echobind%2Fbison-remix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echobind%2Fbison-remix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echobind%2Fbison-remix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echobind%2Fbison-remix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/echobind","download_url":"https://codeload.github.com/echobind/bison-remix/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239870516,"owners_count":19710736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T08:31:00.936Z","updated_at":"2025-02-20T16:24:30.333Z","avatar_url":"https://github.com/echobind.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bison Remix\n\n## Clone this starter\n\n```bash\nnpx create-remix@latest ./my-app --template echobind/bison-remix\n```\n\n## What's Inside?\n\n- Built with [Remix](https://remix.run)\n- Database with Postgres and [Prisma](https://www.prisma.io)\n- Styled with [Tailwind](https://tailwindcss.com)\n- Components from [shadcn/ui](https://ui.shadcn.com) and built with [Radix](https://www.radix-ui.com/primitives)\n- Icons from [Sly](https://sly-cli.fly.dev)\n- Forms validated with [Conform](https://conform.guide) and [Zod](https://zod.dev)\n- Auth with [remix-auth](https://github.com/sergiodxa/remix-auth)\n- i18n with [remix-i18next](https://github.com/sergiodxa/remix-i18next)\n- Testing with [Vitest](https://vitest.dev), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), and [Playwright](https://playwright.dev)\n- Built with Typescript, eslint, and Prettier\n\n## Getting Started Tutorial\n\nThis checklist and mini-tutorial will make sure you make the most of your shiny new Bison Remix app.\n\n## Migrate your database and start the dev server\n\n- [ ] Run `npm run setup:dev` to prep and migrate your local database, as well as\n      generate the prisma client. If this fails, make sure you have Postgres running and\n      the generated `DATABASE_URL` values are correct in your `.env` files.\n- [ ] Run `npm run dev` to start your development server\n\n## Complete a Bison workflow\n\nWhile not a requirement, Bison works best when you start development with the database and API layer.\nWe will illustrate how to use this by adding the concept of an organization to our app.\nThe workflow below assumes you already have `npm run dev` running.\n\n### The Database\n\nBison uses Prisma for database operations. We've added a few conveniences around the default Prisma\nsetup, but if you're familiar with Prisma, you're familiar with databases in Bison.\n\n- [ ] Define an Organization table in `prisma/schema.prisma`.\n\nWe suggest copying the `id`, `createdAt` and `updatedAt` fields from the `User` model.\n\n```prisma\nmodel Organization {\n  id        String   @id @default(cuid())\n  name      String\n  users     User[]\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n}\n```\n\nIf you use VSCode and have the [Prisma extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma)\ninstalled, saving the file should automatically add the inverse relationship to the `User` model!\n\n```prisma\nmodel User {\n  id             String        @id @default(cuid())\n  email          String        @unique\n  password       String\n  roles          Role[]\n  profile        Profile?\n  createdAt      DateTime      @default(now())\n  updatedAt      DateTime      @updatedAt\n  organization   Organization? @relation(fields: [organizationId], references: [id])\n  organizationId String?\n}\n```\n\n- [ ] Generate a migration with `npm run db:migrate`.\n\nYou should see a new folder in `prisma/migrations` and the migration should have been performed.\n\nFor more on Prisma, [view the docs](https://www.prisma.io/docs/).\n\n### Add a Frontend page and form that creates an organization\n\nNow that we have the API finished, we can move to the frontend changes.\n\n- [ ] Create a new route to create organizations in `app/routes/_main.organization.create.tsx`\n- [ ] Create an `OrganizationForm` route component.\n- [ ] Add a simple form with a name input. See the [Conform docs](https://conform.guide)\n      for detailed information.\n\nWe'll use [zod](https://github.com/colinhacks/zod) to ensure type safety form inputs.\n\n```tsx\n// app/routes/_main.organization.create.tsx\nimport { z } from \"zod\";\nimport { conform, useForm } from \"@conform-to/react\";\nimport { getFieldsetConstraint, parse } from \"@conform-to/zod\";\nimport { Form, useActionData } from \"@remix-run/react\";\nimport { useIsPending } from \"~/utils/misc\";\nimport { ErrorList } from \"~/components/ui/error-list\";\nimport { Label } from \"~/components/ui/label\";\nimport { Input } from \"~/components/ui/input\";\nimport { StatusButton } from \"~/components/ui/status-button\";\n\nconst OrganizationFormSchema = z.object({\n  name: z\n    .string({ required_error: \"Name is required\" })\n    .min(3, { message: \"Name is too short\" })\n    .max(100, { message: \"Name is too long\" }),\n});\n\nexport default function OrganizationForm() {\n  const actionData = useActionData\u003ctypeof action\u003e();\n  const isPending = useIsPending();\n\n  const [form, fields] = useForm({\n    id: \"organization-form\",\n    constraint: getFieldsetConstraint(OrganizationFormSchema),\n    lastSubmission: actionData?.submission,\n    onValidate({ formData }) {\n      return parse(formData, { schema: OrganizationFormSchema });\n    },\n    shouldRevalidate: \"onBlur\",\n  });\n\n  return (\n    \u003cForm method=\"post\" className=\"flex flex-col gap-6\" {...form.props}\u003e\n      \u003cdiv\u003e\n        \u003cLabel htmlFor=\"organization-form-name\"\u003eName\u003c/Label\u003e\n        \u003cInput\n          {...conform.input(fields.name)}\n          autoFocus\n          isInvalid={!!fields.name.errors?.length}\n        /\u003e\n        \u003cdiv className=\"min-h-[32px] px-4 pb-3 pt-1\"\u003e\n          {fields.name.errors?.length ? (\n            \u003cErrorList errors={fields.name.errors} /\u003e\n          ) : null}\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cStatusButton\n        className=\"w-full\"\n        status={isPending ? \"pending\" : actionData?.status ?? \"idle\"}\n        type=\"submit\"\n        disabled={isPending}\n      \u003e\n        Create\n      \u003c/StatusButton\u003e\n      \u003cErrorList errors={form.errors} id={form.errorId} /\u003e\n    \u003c/Form\u003e\n  );\n}\n```\n\n- [ ] Add a loader to make sure the user is authenticated.\n\n```tsx\n// app/routes/_main.organization.create.tsx\nimport { authenticator } from \"~/utils/auth.server\";\n\n// ...\n\nexport async function loader({ request }: DataFunctionArgs) {\n  await authenticator.isAuthenticated(request, { failureRedirect: \"/login\" });\n\n  return {};\n}\n\n// ...\n```\n\n- [ ] Add an action to perform validation and create the organization;\n\n```tsx\n// app/routes/_main.organization.create.tsx\nimport { DataFunctionArgs, json, redirect } from \"@remix-run/node\";\nimport { getFieldsetConstraint, parse } from \"@conform-to/zod\";\nimport { prisma } from \"~/utils/db.server\";\n\n// ...\n\nexport async function action({ request }: DataFunctionArgs) {\n  const formData = await request.formData();\n  const submission = await parse(formData, {\n    schema: OrganizationFormSchema,\n  });\n\n  if (!submission.value || submission.intent !== \"submit\") {\n    return json({ status: \"error\", submission } as const);\n  }\n\n  const user = await authenticator.isAuthenticated(request, {\n    failureRedirect: \"/login\",\n  });\n\n  const org = await prisma.organization.create({\n    data: {\n      name: submission.value.name,\n      users: { connect: [{ id: user.id }] },\n    },\n    select: { id: true },\n  });\n\n  return redirect(`/organization/${org.id}`);\n}\n\n// ...\n```\n\nYou should now have a fully working form that creates a new database entry on submit!\n\n### Adding a new page that shows the organization\n\n- [ ] Generate a new route `app/routes/_main.organization.$id.tsx`.\n- [ ] Create a loader to display the organization.\n- [ ] Render the loader data to the component.\n- [ ] Add an error boundary to handle the not found and error cases.\n\n```tsx\n// app/routes/_main.organization.$id.tsx\nimport {\n  useLoaderData,\n  isRouteErrorResponse,\n  useRouteError,\n} from \"@remix-run/react\";\nimport { DataFunctionArgs, MetaFunction, json } from \"@remix-run/node\";\nimport { prisma } from \"~/utils/db.server\";\n\nexport async function loader({ params }: DataFunctionArgs) {\n  const organization = await prisma.organization.findUnique({\n    where: { id: params.id },\n  });\n\n  if (!organization) throw new Response(\"Not Found\", { status: 404 });\n\n  return json({ organization });\n}\n\nexport const meta: MetaFunction\u003ctypeof loader\u003e = ({ data }) =\u003e {\n  return [{ title: `An organization named ${data?.organization.name}` }];\n};\n\nexport default function OrganizationPage() {\n  const { organization } = useLoaderData\u003ctypeof loader\u003e();\n  return \u003cspan\u003eAwesome! {organization.name}\u003c/span\u003e;\n}\n\nexport function ErrorBoundary() {\n  const error = useRouteError();\n\n  if (isRouteErrorResponse(error)) {\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003e\n          {error.status} {error.statusText}\n        \u003c/h1\u003e\n        \u003cp\u003e{error.data}\u003c/p\u003e\n      \u003c/div\u003e\n    );\n  } else if (error instanceof Error) {\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003eError\u003c/h1\u003e\n        \u003cp\u003e{error.message}\u003c/p\u003e\n        \u003cp\u003eThe stack trace is:\u003c/p\u003e\n        \u003cpre\u003e{error.stack}\u003c/pre\u003e\n      \u003c/div\u003e\n    );\n  } else {\n    return \u003ch1\u003eUnknown Error\u003c/h1\u003e;\n  }\n}\n```\n\n## Congrats\n\nOutside of e2e tests, you've used just about every feature in Bison. But don't worry.\nWe've got your back there too.\n\nBonus:\n\n- [ ] View the login and logout e2e tests\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fechobind%2Fbison-remix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fechobind%2Fbison-remix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fechobind%2Fbison-remix/lists"}