@alfredmouelle/create-stack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/_stack/apps/next-base/.dockerignore +10 -0
- package/_stack/apps/next-base/Dockerfile +34 -0
- package/_stack/apps/next-base/README.md +32 -0
- package/_stack/apps/next-base/components.json +25 -0
- package/_stack/apps/next-base/drizzle.config.ts +16 -0
- package/_stack/apps/next-base/next.config.ts +8 -0
- package/_stack/apps/next-base/package.json +70 -0
- package/_stack/apps/next-base/postcss.config.mjs +7 -0
- package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
- package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
- package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
- package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
- package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
- package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
- package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
- package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
- package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
- package/_stack/apps/next-base/src/app/globals.css +171 -0
- package/_stack/apps/next-base/src/app/layout.tsx +23 -0
- package/_stack/apps/next-base/src/app/page.tsx +15 -0
- package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
- package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
- package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
- package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
- package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
- package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
- package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/next-base/src/env.ts +41 -0
- package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
- package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/next-base/src/lib/date.ts +4 -0
- package/_stack/apps/next-base/src/lib/utils.ts +6 -0
- package/_stack/apps/next-base/src/server/api/root.ts +10 -0
- package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
- package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
- package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
- package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
- package/_stack/apps/next-base/src/server/db/index.ts +6 -0
- package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/next-base/src/server/email/index.ts +36 -0
- package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
- package/_stack/apps/next-base/src/trpc/server.ts +23 -0
- package/_stack/apps/next-base/tsconfig.json +37 -0
- package/_stack/apps/tanstack-base/.dockerignore +13 -0
- package/_stack/apps/tanstack-base/Dockerfile +28 -0
- package/_stack/apps/tanstack-base/README.md +31 -0
- package/_stack/apps/tanstack-base/components.json +25 -0
- package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
- package/_stack/apps/tanstack-base/package.json +85 -0
- package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
- package/_stack/apps/tanstack-base/public/logo192.png +0 -0
- package/_stack/apps/tanstack-base/public/logo512.png +0 -0
- package/_stack/apps/tanstack-base/public/manifest.json +25 -0
- package/_stack/apps/tanstack-base/public/robots.txt +3 -0
- package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
- package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
- package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
- package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
- package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
- package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/tanstack-base/src/env.ts +41 -0
- package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
- package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
- package/_stack/apps/tanstack-base/src/router.tsx +40 -0
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
- package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
- package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
- package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
- package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
- package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
- package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
- package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
- package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
- package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
- package/_stack/apps/tanstack-base/src/styles.css +171 -0
- package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
- package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
- package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
- package/_stack/apps/tanstack-base/tsconfig.json +27 -0
- package/_stack/apps/tanstack-base/tsr.config.json +3 -0
- package/_stack/apps/tanstack-base/vite.config.ts +15 -0
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/mailer/capability.json +28 -0
- package/_stack/packages/mailer/package.json +37 -0
- package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
- package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
- package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
- package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/patterns/README.md +58 -0
- package/_stack/patterns/_baseline/README-author.md +10 -0
- package/_stack/patterns/_baseline/biome.jsonc +119 -0
- package/_stack/patterns/_baseline/env.ts +31 -0
- package/_stack/patterns/_baseline/tsconfig.json +27 -0
- package/_stack/patterns/better-auth/pattern.json +73 -0
- package/_stack/patterns/better-auth-next/pattern.json +76 -0
- package/_stack/patterns/data-table/pattern.json +43 -0
- package/_stack/patterns/drizzle/pattern.json +61 -0
- package/_stack/patterns/trpc/pattern.json +61 -0
- package/_stack/patterns/trpc-next/pattern.json +64 -0
- package/index.mjs +216 -0
- package/lib/build.mjs +64 -0
- package/lib/env.mjs +56 -0
- package/lib/identity.mjs +33 -0
- package/lib/mailer.mjs +95 -0
- package/lib/manifests.mjs +61 -0
- package/lib/scaffold.mjs +49 -0
- package/lib/strip.mjs +132 -0
- package/lib/util.mjs +82 -0
- package/package.json +51 -0
- package/templates/next/layout.no-trpc.tsx +22 -0
- package/templates/tanstack/__root.no-trpc.tsx +63 -0
- package/templates/tanstack/router.no-trpc.tsx +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# create-stack
|
|
2
|
+
|
|
3
|
+
Interactive, **deterministic** installer for the reference stack — the non-LLM
|
|
4
|
+
counterpart of the `bootstrap` skill's CREATE mode. It forks a base app
|
|
5
|
+
(`apps/tanstack-base` or `apps/next-base`) and strips it down to your selection.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# from anywhere
|
|
11
|
+
node /path/to/stack/cli/index.mjs my-app
|
|
12
|
+
|
|
13
|
+
# from the stack repo
|
|
14
|
+
pnpm create-stack my-app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The wizard asks for:
|
|
18
|
+
|
|
19
|
+
- **Framework** — TanStack Start or Next.js (App Router)
|
|
20
|
+
- **Foundations** — Drizzle, tRPC, better-auth, data-table (hard deps resolved:
|
|
21
|
+
tRPC/better-auth ⇒ Drizzle; better-auth ⇒ mailer)
|
|
22
|
+
- **Mailer provider** — Resend, Brevo, Amazon SES (or none, if no better-auth)
|
|
23
|
+
- **Extra capabilities** — storage, jobs, cache, … (added afterwards via the
|
|
24
|
+
`add-capability` skill; not baked into the base)
|
|
25
|
+
|
|
26
|
+
Then it forks, strips unselected foundations, swaps the mailer adapter, stamps
|
|
27
|
+
identity, generates `.env(.example)`, and optionally installs + verifies
|
|
28
|
+
(typecheck + Biome).
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
- **Single source of truth**: the base apps (`apps/*-base`) hold the real code;
|
|
33
|
+
the `patterns/*/pattern.json` manifests drive deps/scripts/env/file lists.
|
|
34
|
+
- **Deterministic strip**: whole-directory deletes + dep/env/script diffs from the
|
|
35
|
+
manifests, plus a few hardcoded code *seams* (tRPC↔auth context, root provider
|
|
36
|
+
wiring) handled via shipped reduced variants in `templates/`.
|
|
37
|
+
- If you edit a seam file in a base app (`server/api/trpc.ts`, the root
|
|
38
|
+
`router.tsx` / `__root.tsx` / `layout.tsx`, the schema barrel), update the
|
|
39
|
+
matching transform in `lib/strip.mjs` or the `templates/` variant.
|
|
40
|
+
|
|
41
|
+
## Layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
cli/
|
|
45
|
+
index.mjs wizard (prompts) + install/verify + report
|
|
46
|
+
lib/
|
|
47
|
+
build.mjs pure build phase (fork → strip → mailer → env → identity)
|
|
48
|
+
manifests.mjs load patterns + capabilities; logical→manifest mapping
|
|
49
|
+
scaffold.mjs fork base app, make it standalone
|
|
50
|
+
strip.mjs reverse-strip unselected foundations + code seams
|
|
51
|
+
mailer.mjs mailer provider swap
|
|
52
|
+
env.mjs rebuild src/env.ts blocks + generate .env files
|
|
53
|
+
identity.mjs title/meta + README with the # Author footer
|
|
54
|
+
util.mjs fs / exec / package.json helpers
|
|
55
|
+
templates/ reduced "no-trpc" root-wiring variants
|
|
56
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# Optimized multi-stage build for VPS deployment (Next.js standalone output).
|
|
3
|
+
# Build context = this app (fork it out of the monorepo, or build with this dir
|
|
4
|
+
# as context). Final image runs the standalone server with no dev deps.
|
|
5
|
+
|
|
6
|
+
FROM node:22-alpine AS base
|
|
7
|
+
RUN corepack enable
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
|
|
10
|
+
# ---- deps: install with a cached store ----
|
|
11
|
+
FROM base AS deps
|
|
12
|
+
COPY package.json pnpm-lock.yaml* ./
|
|
13
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
14
|
+
pnpm install --frozen-lockfile
|
|
15
|
+
|
|
16
|
+
# ---- build ----
|
|
17
|
+
FROM base AS build
|
|
18
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
19
|
+
COPY . .
|
|
20
|
+
# Env is validated at runtime, not build — skip it so the build needs no secrets.
|
|
21
|
+
ENV NEXT_TELEMETRY_DISABLED=1 SKIP_ENV_VALIDATION=1
|
|
22
|
+
RUN pnpm build
|
|
23
|
+
|
|
24
|
+
# ---- runner: minimal standalone image ----
|
|
25
|
+
FROM base AS runner
|
|
26
|
+
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
|
|
27
|
+
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -S nextjs
|
|
28
|
+
COPY --from=build /app/public ./public
|
|
29
|
+
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
|
|
30
|
+
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
31
|
+
USER nextjs
|
|
32
|
+
EXPOSE 3000
|
|
33
|
+
ENV PORT=3000 HOSTNAME=0.0.0.0
|
|
34
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# next-base
|
|
2
|
+
|
|
3
|
+
Reference Next.js (App Router) app — fork it to start a new project. Comes wired
|
|
4
|
+
with the full personal foundation:
|
|
5
|
+
|
|
6
|
+
- **baseline** — `~/*` alias, typed `env.ts`, strict Biome, Tailwind v4 + shadcn
|
|
7
|
+
(Geist, dark mode)
|
|
8
|
+
- **data** — Drizzle (Postgres) + drizzle-kit, faker seed harness
|
|
9
|
+
- **auth** — better-auth (email/password + verification, optional Google) + a full
|
|
10
|
+
auth UI (sign-in/up, forgot/reset, verify) with shadcn `Form` (react-hook-form +
|
|
11
|
+
valibot)
|
|
12
|
+
- **email** — mailer (Resend) + email-kit templates (`email:dev` studio)
|
|
13
|
+
- **API** — tRPC with the `api.x.useQuery` hooks + RSC hydration
|
|
14
|
+
- **UI utilities** — theme toggle (light/dark/system), DataTable, DatePicker
|
|
15
|
+
- **deploy** — multi-stage `Dockerfile` (standalone output) for a VPS
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm --filter @alfredmouelle/next-base dev # http://localhost:3000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add more swappable tools with **add-capability**.
|
|
22
|
+
|
|
23
|
+
# Author
|
|
24
|
+
|
|
25
|
+
Alfred MOUELLE | FullStack Developer
|
|
26
|
+
|
|
27
|
+
[](https://comeup.com/@alfredmouelle)
|
|
28
|
+
[](https://github.com/alfredmouelle)
|
|
29
|
+
[](https://www.linkedin.com/in/alfredmouelle)
|
|
30
|
+
[](https://twitter.com/kali47_)
|
|
31
|
+
[](mailto:alfredmouelle@gmail.com)
|
|
32
|
+
[](https://alfredmouelle.com)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-luma",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "~/components",
|
|
15
|
+
"utils": "~/lib/utils",
|
|
16
|
+
"ui": "~/components/ui",
|
|
17
|
+
"lib": "~/lib",
|
|
18
|
+
"hooks": "~/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide",
|
|
21
|
+
"rtl": false,
|
|
22
|
+
"menuColor": "default-translucent",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { config } from 'dotenv'
|
|
2
|
+
import { defineConfig } from 'drizzle-kit'
|
|
3
|
+
|
|
4
|
+
config({ path: ['.env.local', '.env'] })
|
|
5
|
+
|
|
6
|
+
const url = process.env.DATABASE_URL
|
|
7
|
+
if (!url) {
|
|
8
|
+
throw new Error('DATABASE_URL is not set')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
out: './drizzle',
|
|
13
|
+
schema: './src/server/db/schemas/*.schema.ts',
|
|
14
|
+
dialect: 'postgresql',
|
|
15
|
+
dbCredentials: { url },
|
|
16
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/next-base",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Alfred MOUELLE",
|
|
7
|
+
"email": "alfredmouelle@gmail.com",
|
|
8
|
+
"url": "https://alfredmouelle.com"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "next dev",
|
|
12
|
+
"build": "next build",
|
|
13
|
+
"start": "next start",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"check": "biome check .",
|
|
16
|
+
"check:unsafe": "biome check --write --unsafe .",
|
|
17
|
+
"check:write": "biome check --write .",
|
|
18
|
+
"email:dev": "email dev --dir src/emails --port 3001",
|
|
19
|
+
"db:generate": "drizzle-kit generate",
|
|
20
|
+
"db:migrate": "drizzle-kit migrate",
|
|
21
|
+
"db:push": "drizzle-kit push",
|
|
22
|
+
"db:studio": "drizzle-kit studio",
|
|
23
|
+
"db:seed": "tsx src/server/db/seed.ts"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@fontsource-variable/geist": "^5.2.9",
|
|
27
|
+
"@hookform/resolvers": "^5.2.2",
|
|
28
|
+
"@t3-oss/env-core": "^0.13.11",
|
|
29
|
+
"@tanstack/react-query": "^5.101.0",
|
|
30
|
+
"@tanstack/react-table": "^8.21.3",
|
|
31
|
+
"@trpc/client": "^11.17.0",
|
|
32
|
+
"@trpc/react-query": "^11.17.0",
|
|
33
|
+
"@trpc/server": "^11.17.0",
|
|
34
|
+
"better-auth": "^1.6.19",
|
|
35
|
+
"class-variance-authority": "^0.7.1",
|
|
36
|
+
"clsx": "^2.1.1",
|
|
37
|
+
"date-fns": "^4.4.0",
|
|
38
|
+
"drizzle-orm": "^0.45.2",
|
|
39
|
+
"lucide-react": "^0.545.0",
|
|
40
|
+
"next": "16.2.9",
|
|
41
|
+
"next-themes": "^0.4.6",
|
|
42
|
+
"pg": "^8.21.0",
|
|
43
|
+
"radix-ui": "^1.6.0",
|
|
44
|
+
"react": "19.2.4",
|
|
45
|
+
"react-dom": "19.2.4",
|
|
46
|
+
"react-day-picker": "^10.0.1",
|
|
47
|
+
"react-email": "^6.6.3",
|
|
48
|
+
"react-hook-form": "^7.54.2",
|
|
49
|
+
"resend": "^6.14.0",
|
|
50
|
+
"server-only": "^0.0.1",
|
|
51
|
+
"superjson": "^2.2.2",
|
|
52
|
+
"tailwind-merge": "^3.6.0",
|
|
53
|
+
"valibot": "^1.4.1"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "^2.5.0",
|
|
57
|
+
"@faker-js/faker": "^10.5.0",
|
|
58
|
+
"@tailwindcss/postcss": "^4.3.1",
|
|
59
|
+
"@types/node": "^22.10.2",
|
|
60
|
+
"@types/pg": "^8.15.6",
|
|
61
|
+
"@types/react": "^19",
|
|
62
|
+
"@types/react-dom": "^19",
|
|
63
|
+
"drizzle-kit": "^0.31.10",
|
|
64
|
+
"dotenv": "^17.2.4",
|
|
65
|
+
"tailwindcss": "^4.3.1",
|
|
66
|
+
"tsx": "^4.22.4",
|
|
67
|
+
"tw-animate-css": "^1.4.0",
|
|
68
|
+
"typescript": "^5.9.3"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
2
|
+
import type { NextRequest } from 'next/server'
|
|
3
|
+
import { env } from '~/env'
|
|
4
|
+
import { appRouter } from '~/server/api/root'
|
|
5
|
+
import { createTRPCContext } from '~/server/api/trpc'
|
|
6
|
+
|
|
7
|
+
const createContext = async (req: NextRequest) => createTRPCContext({ headers: req.headers })
|
|
8
|
+
|
|
9
|
+
const handler = (req: NextRequest) =>
|
|
10
|
+
fetchRequestHandler({
|
|
11
|
+
endpoint: '/api/trpc',
|
|
12
|
+
req,
|
|
13
|
+
router: appRouter,
|
|
14
|
+
createContext: () => createContext(req),
|
|
15
|
+
onError:
|
|
16
|
+
env.NODE_ENV === 'development'
|
|
17
|
+
? ({ path, error }) => {
|
|
18
|
+
console.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`)
|
|
19
|
+
}
|
|
20
|
+
: undefined,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export { handler as GET, handler as POST }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { valibotResolver } from '@hookform/resolvers/valibot'
|
|
4
|
+
import { Mail } from 'lucide-react'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { useForm } from 'react-hook-form'
|
|
7
|
+
import { Button } from '~/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Form,
|
|
10
|
+
FormControl,
|
|
11
|
+
FormField,
|
|
12
|
+
FormItem,
|
|
13
|
+
FormLabel,
|
|
14
|
+
FormMessage,
|
|
15
|
+
} from '~/components/ui/form'
|
|
16
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
|
|
17
|
+
import { Spinner } from '~/components/ui/spinner'
|
|
18
|
+
import { FormAlert } from '~/features/auth/form-alert'
|
|
19
|
+
import { type ForgotPasswordInput, ForgotPasswordSchema } from '~/features/auth/schemas'
|
|
20
|
+
import { authClient } from '~/server/better-auth/client'
|
|
21
|
+
|
|
22
|
+
export function ForgotPasswordForm() {
|
|
23
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
24
|
+
const [sent, setSent] = useState(false)
|
|
25
|
+
const form = useForm<ForgotPasswordInput>({
|
|
26
|
+
resolver: valibotResolver(ForgotPasswordSchema),
|
|
27
|
+
defaultValues: { email: '' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
31
|
+
setFormError(null)
|
|
32
|
+
const { error } = await authClient.requestPasswordReset({
|
|
33
|
+
email: values.email,
|
|
34
|
+
redirectTo: '/auth/reset-password',
|
|
35
|
+
})
|
|
36
|
+
if (error) {
|
|
37
|
+
setFormError(error.message ?? 'Could not send. Try again.')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
setSent(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (sent) {
|
|
44
|
+
return (
|
|
45
|
+
<FormAlert variant="success">
|
|
46
|
+
If an account exists for this address, a reset email has just been sent. Check your spam
|
|
47
|
+
folder.
|
|
48
|
+
</FormAlert>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Form {...form}>
|
|
54
|
+
<form className="grid gap-4" onSubmit={onSubmit}>
|
|
55
|
+
{formError ? <FormAlert>{formError}</FormAlert> : null}
|
|
56
|
+
|
|
57
|
+
<FormField
|
|
58
|
+
control={form.control}
|
|
59
|
+
name="email"
|
|
60
|
+
render={({ field }) => (
|
|
61
|
+
<FormItem>
|
|
62
|
+
<FormLabel>Email</FormLabel>
|
|
63
|
+
<InputGroup className="h-10">
|
|
64
|
+
<InputGroupAddon align="inline-start">
|
|
65
|
+
<Mail />
|
|
66
|
+
</InputGroupAddon>
|
|
67
|
+
<FormControl>
|
|
68
|
+
<InputGroupInput
|
|
69
|
+
autoComplete="email"
|
|
70
|
+
placeholder="you@example.com"
|
|
71
|
+
type="email"
|
|
72
|
+
{...field}
|
|
73
|
+
/>
|
|
74
|
+
</FormControl>
|
|
75
|
+
</InputGroup>
|
|
76
|
+
<FormMessage />
|
|
77
|
+
</FormItem>
|
|
78
|
+
)}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<Button
|
|
82
|
+
className="w-full cursor-pointer"
|
|
83
|
+
disabled={form.formState.isSubmitting}
|
|
84
|
+
type="submit"
|
|
85
|
+
>
|
|
86
|
+
{form.formState.isSubmitting ? <Spinner /> : null}
|
|
87
|
+
{form.formState.isSubmitting ? 'Sending…' : 'Send link'}
|
|
88
|
+
</Button>
|
|
89
|
+
</form>
|
|
90
|
+
</Form>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { valibotResolver } from '@hookform/resolvers/valibot'
|
|
4
|
+
import { Lock } from 'lucide-react'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { useForm } from 'react-hook-form'
|
|
8
|
+
import { Button } from '~/components/ui/button'
|
|
9
|
+
import {
|
|
10
|
+
Form,
|
|
11
|
+
FormControl,
|
|
12
|
+
FormField,
|
|
13
|
+
FormItem,
|
|
14
|
+
FormLabel,
|
|
15
|
+
FormMessage,
|
|
16
|
+
} from '~/components/ui/form'
|
|
17
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
|
|
18
|
+
import { Spinner } from '~/components/ui/spinner'
|
|
19
|
+
import { FormAlert } from '~/features/auth/form-alert'
|
|
20
|
+
import { type ResetPasswordInput, ResetPasswordSchema } from '~/features/auth/schemas'
|
|
21
|
+
import { authClient } from '~/server/better-auth/client'
|
|
22
|
+
|
|
23
|
+
export function ResetPasswordForm({ token }: { token?: string }) {
|
|
24
|
+
const router = useRouter()
|
|
25
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
26
|
+
const form = useForm<ResetPasswordInput>({
|
|
27
|
+
resolver: valibotResolver(ResetPasswordSchema),
|
|
28
|
+
defaultValues: { password: '', confirmPassword: '' },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
32
|
+
if (!token) return
|
|
33
|
+
setFormError(null)
|
|
34
|
+
const { error } = await authClient.resetPassword({ newPassword: values.password, token })
|
|
35
|
+
if (error) {
|
|
36
|
+
setFormError(error.message ?? 'Could not reset password.')
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
router.push('/auth/sign-in')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (!token) {
|
|
43
|
+
return <FormAlert>This reset link is invalid or expired. Please request a new one.</FormAlert>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Form {...form}>
|
|
48
|
+
<form className="grid gap-4" onSubmit={onSubmit}>
|
|
49
|
+
{formError ? <FormAlert>{formError}</FormAlert> : null}
|
|
50
|
+
|
|
51
|
+
<FormField
|
|
52
|
+
control={form.control}
|
|
53
|
+
name="password"
|
|
54
|
+
render={({ field }) => (
|
|
55
|
+
<FormItem>
|
|
56
|
+
<FormLabel>New password</FormLabel>
|
|
57
|
+
<InputGroup className="h-10">
|
|
58
|
+
<InputGroupAddon align="inline-start">
|
|
59
|
+
<Lock />
|
|
60
|
+
</InputGroupAddon>
|
|
61
|
+
<FormControl>
|
|
62
|
+
<InputGroupInput
|
|
63
|
+
autoComplete="new-password"
|
|
64
|
+
placeholder="At least 8 characters"
|
|
65
|
+
type="password"
|
|
66
|
+
{...field}
|
|
67
|
+
/>
|
|
68
|
+
</FormControl>
|
|
69
|
+
</InputGroup>
|
|
70
|
+
<FormMessage />
|
|
71
|
+
</FormItem>
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<FormField
|
|
76
|
+
control={form.control}
|
|
77
|
+
name="confirmPassword"
|
|
78
|
+
render={({ field }) => (
|
|
79
|
+
<FormItem>
|
|
80
|
+
<FormLabel>Confirm password</FormLabel>
|
|
81
|
+
<InputGroup className="h-10">
|
|
82
|
+
<InputGroupAddon align="inline-start">
|
|
83
|
+
<Lock />
|
|
84
|
+
</InputGroupAddon>
|
|
85
|
+
<FormControl>
|
|
86
|
+
<InputGroupInput autoComplete="new-password" type="password" {...field} />
|
|
87
|
+
</FormControl>
|
|
88
|
+
</InputGroup>
|
|
89
|
+
<FormMessage />
|
|
90
|
+
</FormItem>
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<Button
|
|
95
|
+
className="w-full cursor-pointer"
|
|
96
|
+
disabled={form.formState.isSubmitting}
|
|
97
|
+
type="submit"
|
|
98
|
+
>
|
|
99
|
+
{form.formState.isSubmitting ? <Spinner /> : null}
|
|
100
|
+
{form.formState.isSubmitting ? 'Saving…' : 'Reset password'}
|
|
101
|
+
</Button>
|
|
102
|
+
</form>
|
|
103
|
+
</Form>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { valibotResolver } from '@hookform/resolvers/valibot'
|
|
4
|
+
import { Lock, Mail } from 'lucide-react'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { useRouter } from 'next/navigation'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { useForm } from 'react-hook-form'
|
|
9
|
+
import { Button } from '~/components/ui/button'
|
|
10
|
+
import {
|
|
11
|
+
Form,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormField,
|
|
14
|
+
FormItem,
|
|
15
|
+
FormLabel,
|
|
16
|
+
FormMessage,
|
|
17
|
+
} from '~/components/ui/form'
|
|
18
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
|
|
19
|
+
import { Spinner } from '~/components/ui/spinner'
|
|
20
|
+
import { FormAlert } from '~/features/auth/form-alert'
|
|
21
|
+
import { AuthDivider, GoogleButton } from '~/features/auth/google-button'
|
|
22
|
+
import { type SignInInput, SignInSchema } from '~/features/auth/schemas'
|
|
23
|
+
import { authClient } from '~/server/better-auth/client'
|
|
24
|
+
|
|
25
|
+
export function SignInForm() {
|
|
26
|
+
const router = useRouter()
|
|
27
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
28
|
+
const form = useForm<SignInInput>({
|
|
29
|
+
resolver: valibotResolver(SignInSchema),
|
|
30
|
+
defaultValues: { email: '', password: '' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
34
|
+
setFormError(null)
|
|
35
|
+
const { error } = await authClient.signIn.email({
|
|
36
|
+
email: values.email,
|
|
37
|
+
password: values.password,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!error) {
|
|
41
|
+
router.push('/')
|
|
42
|
+
router.refresh()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error.code === 'EMAIL_NOT_VERIFIED') {
|
|
47
|
+
router.push(`/auth/verify-email?email=${encodeURIComponent(values.email)}`)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setFormError(
|
|
52
|
+
error.code === 'INVALID_EMAIL_OR_PASSWORD'
|
|
53
|
+
? 'Incorrect email or password.'
|
|
54
|
+
: (error.message ?? 'Could not sign in. Try again.'),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Form {...form}>
|
|
60
|
+
<form className="grid gap-4" onSubmit={onSubmit}>
|
|
61
|
+
<GoogleButton label="Continue with Google" />
|
|
62
|
+
<AuthDivider />
|
|
63
|
+
|
|
64
|
+
{formError ? <FormAlert>{formError}</FormAlert> : null}
|
|
65
|
+
|
|
66
|
+
<FormField
|
|
67
|
+
control={form.control}
|
|
68
|
+
name="email"
|
|
69
|
+
render={({ field }) => (
|
|
70
|
+
<FormItem>
|
|
71
|
+
<FormLabel>Email</FormLabel>
|
|
72
|
+
<InputGroup className="h-10">
|
|
73
|
+
<InputGroupAddon align="inline-start">
|
|
74
|
+
<Mail />
|
|
75
|
+
</InputGroupAddon>
|
|
76
|
+
<FormControl>
|
|
77
|
+
<InputGroupInput
|
|
78
|
+
autoComplete="email"
|
|
79
|
+
placeholder="you@example.com"
|
|
80
|
+
type="email"
|
|
81
|
+
{...field}
|
|
82
|
+
/>
|
|
83
|
+
</FormControl>
|
|
84
|
+
</InputGroup>
|
|
85
|
+
<FormMessage />
|
|
86
|
+
</FormItem>
|
|
87
|
+
)}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<FormField
|
|
91
|
+
control={form.control}
|
|
92
|
+
name="password"
|
|
93
|
+
render={({ field }) => (
|
|
94
|
+
<FormItem>
|
|
95
|
+
<FormLabel>Password</FormLabel>
|
|
96
|
+
<InputGroup className="h-10">
|
|
97
|
+
<InputGroupAddon align="inline-start">
|
|
98
|
+
<Lock />
|
|
99
|
+
</InputGroupAddon>
|
|
100
|
+
<FormControl>
|
|
101
|
+
<InputGroupInput autoComplete="current-password" type="password" {...field} />
|
|
102
|
+
</FormControl>
|
|
103
|
+
</InputGroup>
|
|
104
|
+
<Link
|
|
105
|
+
className="justify-self-end text-muted-foreground text-xs hover:underline"
|
|
106
|
+
href="/auth/forgot-password"
|
|
107
|
+
>
|
|
108
|
+
Forgot password?
|
|
109
|
+
</Link>
|
|
110
|
+
<FormMessage />
|
|
111
|
+
</FormItem>
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<Button
|
|
116
|
+
className="w-full cursor-pointer"
|
|
117
|
+
disabled={form.formState.isSubmitting}
|
|
118
|
+
type="submit"
|
|
119
|
+
>
|
|
120
|
+
{form.formState.isSubmitting ? <Spinner /> : null}
|
|
121
|
+
{form.formState.isSubmitting ? 'Signing in…' : 'Sign in'}
|
|
122
|
+
</Button>
|
|
123
|
+
</form>
|
|
124
|
+
</Form>
|
|
125
|
+
)
|
|
126
|
+
}
|