diff --git a/.aiignore b/.aiignore new file mode 100644 index 0000000..7d17f6f --- /dev/null +++ b/.aiignore @@ -0,0 +1,27 @@ +# An .aiignore file follows the same syntax as a .gitignore file. +# .gitignore documentation: https://git-scm.com/docs/gitignore + +# you can ignore files +.DS_Store +*.log +*.tmp + +# or folders +dist/ +build/ +out/ + +.cursor +.cursorignore +database.types.ts +playwright-report +test-results +web/supabase/migrations +pnpm-lock.yaml +.env.local +.env.production.local +.idea +.vscode +.zed +tsconfig.tsbuildinfo +.windsurfrules \ No newline at end of file diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..8ce3491 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,12 @@ +database.types.ts +playwright-report +test-results +web/supabase/migrations +pnpm-lock.yaml +.env.local +.env.production.local +.idea +.vscode +.zed +tsconfig.tsbuildinfo +.windsurfrules \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..73c9a38 --- /dev/null +++ b/.env @@ -0,0 +1,49 @@ +# SHARED ENVIROMENT VARIABLES +# HERE YOU CAN ADD ALL THE **PUBLIC** ENVIRONMENT VARIABLES THAT ARE SHARED ACROSS ALL THE ENVIROMENTS +# PLEASE DO NOT ADD ANY CONFIDENTIAL KEYS OR SENSITIVE INFORMATION HERE +# ONLY CONFIGURATION, PATH, FEATURE FLAGS, ETC. +# TO OVERRIDE THESE VARIABLES IN A SPECIFIC ENVIRONMENT, PLEASE ADD THEM TO THE SPECIFIC ENVIRONMENT FILE (e.g. .env.development, .env.production) + +# SITE +NEXT_PUBLIC_SITE_URL=http://localhost:3000 +NEXT_PUBLIC_PRODUCT_NAME=MedReport +NEXT_PUBLIC_SITE_TITLE="MedReport" +NEXT_PUBLIC_SITE_DESCRIPTION="MedReport." +NEXT_PUBLIC_DEFAULT_THEME_MODE=light +NEXT_PUBLIC_THEME_COLOR="#ffffff" +NEXT_PUBLIC_THEME_COLOR_DARK="#0a0a0a" + +# AUTH +NEXT_PUBLIC_AUTH_PASSWORD=true +NEXT_PUBLIC_AUTH_MAGIC_LINK=false +NEXT_PUBLIC_CAPTCHA_SITE_KEY= + +# BILLING +NEXT_PUBLIC_BILLING_PROVIDER=stripe + +# CMS +CMS_CLIENT=keystatic + +# KEYSTATIC +NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content + +# LOCALES PATH +NEXT_PUBLIC_LOCALES_PATH=apps/web/public/locales + +# FEATURE FLAGS +NEXT_PUBLIC_ENABLE_THEME_TOGGLE=true +NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION=true +NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING=true +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true +NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true +NEXT_PUBLIC_LANGUAGE_PRIORITY=application +NEXT_PUBLIC_ENABLE_NOTIFICATIONS=true + +# NEXTJS +NEXT_TELEMETRY_DISABLED=1 + +LOGGER=pino + +NEXT_PUBLIC_DEFAULT_LOCALE=et \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..82a3969 --- /dev/null +++ b/.env.development @@ -0,0 +1,27 @@ +# This file is used to define environment variables for the development environment. +# These values are only used when running the app in development mode. + +# SUPABASE +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU + +## THIS IS FOR DEVELOPMENT ONLY - DO NOT USE IN PRODUCTION +SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET + +# EMAILS +EMAIL_SENDER="Makerkit " +EMAIL_PORT=54325 +EMAIL_HOST=localhost +EMAIL_TLS=false +EMAIL_USER=user +EMAIL_PASSWORD=password + +# CONTACT FORM +CONTACT_EMAIL=test@makerkit.dev + +# STRIPE +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= + +# MAILER +MAILER_PROVIDER=nodemailer \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..417308e --- /dev/null +++ b/.env.production @@ -0,0 +1,12 @@ +# PRODUCTION ENVIRONMENT VARIABLES + +## DO NOT ADD VARS HERE UNLESS THEY ARE PUBLIC OR NOT SENSITIVE +## THIS ENV IS USED FOR PRODUCTION AND IS COMMITED TO THE REPO +## AVOID PLACING SENSITIVE DATA IN THIS FILE. +## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE. + +# SUPABASE +NEXT_PUBLIC_SUPABASE_URL= + +# STRIPE +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..3d047a0 --- /dev/null +++ b/.env.test @@ -0,0 +1,22 @@ +# TEST ENVIRONMENT VARIABLES +NEXT_PUBLIC_CI=true + +# SUPABASE +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU + +## THIS IS FOR DEVELOPMENT ONLY - DO NOT USE IN PRODUCTION +SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET + +EMAIL_SENDER=test@makerkit.dev +EMAIL_PORT=54325 +EMAIL_HOST=localhost +EMAIL_TLS=false +EMAIL_USER=user +EMAIL_PASSWORD=password + +# STRIPE +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf + +CONTACT_EMAIL=test@makerkit.dev \ No newline at end of file diff --git a/.gitignore b/.gitignore index f0f18d6..173aa33 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +**/node_modules/ /.pnp .pnp.* .yarn/* @@ -32,7 +33,7 @@ yarn-error.log* # local env files .env*.local -.env + # vercel .vercel diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..dad7c34 --- /dev/null +++ b/.npmrc @@ -0,0 +1,10 @@ +peer-legacy-deps=true +dedupe-peer-dependents=true +use-lockfile-v6=true +resolution-mode=highest +package-manager-strict=true +public-hoist-pattern[]=*i18next* +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +public-hoist-pattern[]=*require-in-the-middle* +public-hoist-pattern[]=*import-in-the-middle* \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..89e0c3d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.10 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dca02d0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +database.types.ts +playwright-report +*.hbs \ No newline at end of file diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..6c7a0b7 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,224 @@ +# Makerkit Guidelines + +## Project Stack +- Framework: Next.js 15 App Router, TypeScript, React, Node.js +- Backend: Supabase with Postgres +- UI: Shadcn UI, Tailwind CSS +- Key libraries: React Hook Form, React Query, Zod, Lucide React +- Focus: Code clarity, Readability, Best practices, Maintainability + +## Project Structure +``` + /app + /home # protected routes + /(user) # user workspace + /[account] # team workspace + /(marketing) # marketing pages + /auth # auth pages + /components # global components + /config # global config + /lib # global utils + /content # markdoc content + /supabase # supabase root +``` + +## Core Principles + +### Data Flow +1. Server Components + - Use Supabase Client directly via `getSupabaseServerClient` + - Handle errors with proper boundaries + - Example: + ```tsx + async function ServerComponent() { + const client = getSupabaseServerClient(); + const { data, error } = await client.from('notes').select('*'); + if (error) return ; + return ; + } + ``` + +2. Client Components + - Use React Query for data fetching + - Implement proper loading states + - Example: + ```tsx + function useNotes() { + const { data, isLoading } = useQuery({ + queryKey: ['notes'], + queryFn: async () => { + const { data } = await fetch('/api/notes'); + return data; + } + }); + return { data, isLoading }; + } + ``` + +### Server Actions +- Name files as "server-actions.ts" in `_lib/server` folder +- Export with "Action" suffix +- Use `enhanceAction` with proper typing +- Example: + ```tsx + export const createNoteAction = enhanceAction( + async function (data, user) { + const client = getSupabaseServerClient(); + const { error } = await client + .from('notes') + .insert({ ...data, user_id: user.id }); + if (error) throw error; + return { success: true }; + }, + { + auth: true, + schema: NoteSchema, + } + ); + ``` + +### Route Handlers + +- Use `enhanceRouteHandler` to wrap route handlers +- Use Route Handlers when data fetching from Client Components + +## Database & Security + +### RLS Policies +- Strive to create a safe, robust, secure and consistent database schema +- Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them. +- Enable RLS by default and propose the required RLS policies +- `public.accounts` are the root tables for the application +- Implement cascading deletes when appropriate +- Ensure strong consistency considering triggers and constraints +- Always use Postgres schemas explicitly (e.g., `public.accounts`) + +## Forms Pattern + +### 1. Schema Definition +```tsx +// schema/note.schema.ts +import { z } from 'zod'; + +export const NoteSchema = z.object({ + title: z.string().min(1).max(100), + content: z.string().min(1), + category: z.enum(['work', 'personal']), +}); +``` + +### 2. Form Component +```tsx +'use client'; + +export function NoteForm() { + const [pending, startTransition] = useTransition(); + const form = useForm({ + resolver: zodResolver(NoteSchema), + defaultValues: { title: '', content: '', category: 'personal' } + }); + + const onSubmit = (data: z.infer) => { + startTransition(async () => { + try { + await createNoteAction(data); + form.reset(); + } catch (error) { + // Handle error + } + }); + }; + + return ( +
+ ( + + Title + + + + + + )} /> + {/* Other fields */} + + ); +} +``` + +## Error Handling + +- Consider logging asynchronous requests in server code using the `@kit/shared/logger` +- Handle promises and async/await gracefully +- Consider the unhappy path and handle errors appropriately + +### Structured Logging +```tsx +const ctx = { + name: 'create-note', + userId: user.id, + noteId: note.id +}; + +logger.info(ctx, 'Creating new note...'); + +try { + await createNote(); + logger.info(ctx, 'Note created successfully'); +} catch (error) { + logger.error(ctx, 'Failed to create note', { error }); + throw error; +} +``` + +## Context Management + +In client components, we can use the `useUserWorkspace` hook to access the user's workspace data. + +### Personal Account +```tsx +'use client'; + +function PersonalDashboard() { + const { workspace, user } = useUserWorkspace(); + if (!workspace) return null; + + return ( +
+

Welcome, {user.email}

+ +
+ ); +} +``` + +### Team Account +In client components, we can use the `useTeamAccountWorkspace` hook to access the team account's workspace data. It only works under the `/home/[account]` route. + +```tsx +'use client'; + +function TeamDashboard() { + const { account, user } = useTeamAccountWorkspace(); + + return ( +
+

{account.name}

+ + +
+ ); +} +``` + +## UI Components + +- Reusable UI components are defined in the "packages/ui" package named "@kit/ui". +- By exporting the component from the "exports" field, we can import it using the "@kit/ui/{component-name}" format. + +## Creating Pages + +When creating new pages ensure: +- The page is exported using `withI18n(Page)` to enable i18n. +- The page has the required and correct metadata using the `metadata` or `generateMetadata` function. +- Don't worry about authentication, it's handled in the middleware. diff --git a/README.md b/README.md index bb6a4ed..a2cea56 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,46 @@ - - Next.js and Supabase Starter Kit - the fastest way to build apps with Next.js and Supabase -

Next.js and Supabase Starter Kit

-
-

- The fastest way to build apps with Next.js and Supabase -

+## Prerequisites + "node": ">=20.0.0", + "pnpm": ">=9.0.0" -

- Features · - Demo · - Deploy to Vercel · - Clone and run locally · - Feedback and issues - More Examples -

-
+## Project structure +``` +/ app - pages +/ components - custom components, helper components that not provided by any package. Place to extend an redefine components from packages +/ config - bunch of configs, that are provided by starter kit. +/ content - (temporary?) - to be removed when cleaned all dependencies +/ fonts - (temporary) - contains fonts, should be relocated to another place (maybe public) +/ lib - diffirent libs, services, utils + - fonts.ts - project fonts setup, which becomes available as a global css variable + / i18n - translations/localization setup +/ public - public assets + / locales - translations under a corresponding local - at a specific namespace +/ styles - all styles of the projects, including tailwind variable setup + - global.css - Global styles for the entire application, a place where should apply variables to global selectors + - shadcn-ui.css - A place where all global variables are defined for color, sizes and etc, that are used in theme.css. Variables defined here and in theme.css are available as tailwindcss property-class + - theme.css - more specific variables, available as tailwindcss property-class + - makerkit.css - Makerkit-specific global styles + - markdoc.css - Styles for Markdoc Markdown files. + - +/ supabase - primary supabase +/ tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config +/ utils -## Features -- Works across the entire [Next.js](https://nextjs.org) stack - - App Router - - Pages Router - - Middleware - - Client - - Server - - It just works! -- supabase-ssr. A package to configure Supabase Auth to use cookies -- Styling with [Tailwind CSS](https://tailwindcss.com) -- Components with [shadcn/ui](https://ui.shadcn.com/) -- Optional deployment with [Supabase Vercel Integration and Vercel deploy](#deploy-your-own) - - Environment variables automatically assigned to Vercel project +``` -## Demo -You can view a fully working demo at [demo-nextjs-with-supabase.vercel.app](https://demo-nextjs-with-supabase.vercel.app/). +## Migration from old structure +```bash +pnpm clean +pnpm i +``` -## Deploy to Vercel +## Adding new dependency -Vercel deployment will guide you through creating a Supabase account and project. +```bash +pnpm add -w +``` -After installation of the Supabase integration, all relevant environment variables will be assigned to the project so the deployment is fully functioning. - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This+starter+configures+Supabase+Auth+to+use+cookies%2C+making+the+user%27s+session+available+throughout+the+entire+Next.js+app+-+Client+Components%2C+Server+Components%2C+Route+Handlers%2C+Server+Actions+and+Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png) - -The above will also clone the Starter kit to your GitHub, you can clone that locally and develop locally. - -If you wish to just develop locally and not deploy to Vercel, [follow the steps below](#clone-and-run-locally). - -## Clone and run locally - -1. You'll first need a Supabase project which can be made [via the Supabase dashboard](https://database.new) - -2. Create a Next.js app using the Supabase Starter template npx command - - ```bash - npx create-next-app --example with-supabase with-supabase-app - ``` - - ```bash - yarn create next-app --example with-supabase with-supabase-app - ``` - - ```bash - pnpm create next-app --example with-supabase with-supabase-app - ``` - -3. Use `cd` to change into the app's directory - - ```bash - cd with-supabase-app - ``` - -4. Rename `.env.example` to `.env.local` and update the following: - - ``` - NEXT_PUBLIC_SUPABASE_URL=[INSERT SUPABASE PROJECT URL] - NEXT_PUBLIC_SUPABASE_ANON_KEY=[INSERT SUPABASE PROJECT API ANON KEY] - ``` - - Both `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api) - -5. You can now run the Next.js local development server: - - ```bash - npm run dev - ``` - - The starter kit should now be running on [localhost:3000](http://localhost:3000/). - -6. This template comes with the default shadcn/ui style initialized. If you instead want other ui.shadcn styles, delete `components.json` and [re-install shadcn/ui](https://ui.shadcn.com/docs/installation/next) - -> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally. - -## Feedback and issues - -Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose). - -## More Supabase examples - -- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) -- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF) -- [Supabase Auth and the Next.js App Router](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) +## Supabase +TODO \ No newline at end of file diff --git a/app/(marketing)/(legal)/cookie-policy/page.tsx b/app/(marketing)/(legal)/cookie-policy/page.tsx new file mode 100644 index 0000000..d3c5eee --- /dev/null +++ b/app/(marketing)/(legal)/cookie-policy/page.tsx @@ -0,0 +1,30 @@ +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('marketing:cookiePolicy'), + }; +} + +async function CookiePolicyPage() { + const { t } = await createI18nServerInstance(); + + return ( +
+ + +
+
Your terms of service content here
+
+
+ ); +} + +export default withI18n(CookiePolicyPage); diff --git a/app/(marketing)/(legal)/privacy-policy/page.tsx b/app/(marketing)/(legal)/privacy-policy/page.tsx new file mode 100644 index 0000000..b8ff856 --- /dev/null +++ b/app/(marketing)/(legal)/privacy-policy/page.tsx @@ -0,0 +1,30 @@ +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('marketing:privacyPolicy'), + }; +} + +async function PrivacyPolicyPage() { + const { t } = await createI18nServerInstance(); + + return ( +
+ + +
+
Your terms of service content here
+
+
+ ); +} + +export default withI18n(PrivacyPolicyPage); diff --git a/app/(marketing)/(legal)/terms-of-service/page.tsx b/app/(marketing)/(legal)/terms-of-service/page.tsx new file mode 100644 index 0000000..ee7d0cb --- /dev/null +++ b/app/(marketing)/(legal)/terms-of-service/page.tsx @@ -0,0 +1,30 @@ +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('marketing:termsOfService'), + }; +} + +async function TermsOfServicePage() { + const { t } = await createI18nServerInstance(); + + return ( +
+ + +
+
Your terms of service content here
+
+
+ ); +} + +export default withI18n(TermsOfServicePage); diff --git a/app/(marketing)/_components/site-footer.tsx b/app/(marketing)/_components/site-footer.tsx new file mode 100644 index 0000000..bd8fdb4 --- /dev/null +++ b/app/(marketing)/_components/site-footer.tsx @@ -0,0 +1,58 @@ +import { Footer } from '@kit/ui/marketing'; +import { Trans } from '@kit/ui/trans'; + +import { AppLogo } from '~/components/app-logo'; +import appConfig from '~/config/app.config'; + +export function SiteFooter() { + return ( +