mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add clerk auth support with convex (#548)
This commit is contained in:
@@ -49,8 +49,8 @@ Add `export BTS_TELEMETRY_DISABLED=1` to your shell profile to make it permanent
|
||||
## Where to view analytics
|
||||
|
||||
- Charts: [`/analytics`](/analytics)
|
||||
- Raw JSON snapshot: `https://r2.amanv.dev/analytics-data.json`
|
||||
- CSV export: `https://r2.amanv.dev/export.csv`
|
||||
- Raw JSON snapshot: `https://r2.better-t-stack.dev/analytics-data.json`
|
||||
- CSV export: `https://r2.better-t-stack.dev/export.csv`
|
||||
|
||||
Notes:
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ The file is JSONC with comments enabled and includes a `$schema` URL for tooling
|
||||
"database": "sqlite",
|
||||
"orm": "drizzle",
|
||||
"api": "trpc",
|
||||
"auth": true,
|
||||
"auth": "better-auth",
|
||||
"addons": ["turborepo"],
|
||||
"examples": [],
|
||||
"dbSetup": "none",
|
||||
|
||||
@@ -59,7 +59,7 @@ create-better-t-stack --runtime workers --backend hono --database sqlite --orm d
|
||||
#### Convex Backend
|
||||
When using `--backend convex`, the following options are automatically set:
|
||||
|
||||
- `--auth false` (Convex handles auth)
|
||||
- `--auth clerk` (if compatible frontends selected, otherwise `none`)
|
||||
- `--database none` (Convex provides database)
|
||||
- `--orm none` (Convex provides data layer)
|
||||
- `--api none` (Convex provides API)
|
||||
@@ -67,10 +67,12 @@ When using `--backend convex`, the following options are automatically set:
|
||||
- `--db-setup none` (Convex manages hosting)
|
||||
- `--examples todo` (Todo example works with Convex)
|
||||
|
||||
**Note:** Convex supports Clerk authentication with compatible frontends (React frameworks, Next.js, TanStack Start, and native frameworks). Nuxt, Svelte, and Solid are not compatible with Clerk.
|
||||
|
||||
#### No Backend
|
||||
When using `--backend none`, the following options are automatically set:
|
||||
|
||||
- `--auth false` (No backend for auth)
|
||||
- `--auth none` (No backend for auth)
|
||||
- `--database none` (No backend for database)
|
||||
- `--orm none` (No database)
|
||||
- `--api none` (No backend for API)
|
||||
@@ -158,17 +160,30 @@ create-better-t-stack --frontend next native-nativewind
|
||||
|
||||
## Authentication Requirements
|
||||
|
||||
Authentication requires:
|
||||
### Better-Auth Requirements
|
||||
Better-Auth authentication requires:
|
||||
- A backend framework (cannot be `none`)
|
||||
- A database (cannot be `none`)
|
||||
- An ORM (cannot be `none`)
|
||||
|
||||
```bash
|
||||
# ❌ Invalid - Auth without database
|
||||
create-better-t-stack --auth --database none
|
||||
### Clerk Requirements
|
||||
Clerk authentication requires:
|
||||
- Convex backend (`--backend convex`)
|
||||
- Compatible frontends (React frameworks, Next.js, TanStack Start, native frameworks)
|
||||
- Not compatible with Nuxt, Svelte, or Solid
|
||||
|
||||
# ✅ Valid - Auth with full stack
|
||||
create-better-t-stack --auth --database postgres --orm drizzle --backend hono
|
||||
```bash
|
||||
# ❌ Invalid - Better-Auth without database
|
||||
create-better-t-stack --auth better-auth --database none
|
||||
|
||||
# ✅ Valid - Better-Auth with full stack
|
||||
create-better-t-stack --auth better-auth --database postgres --orm drizzle --backend hono
|
||||
|
||||
# ✅ Valid - Clerk with Convex
|
||||
create-better-t-stack --auth clerk --backend convex --frontend tanstack-router
|
||||
|
||||
# ❌ Invalid - Clerk without Convex
|
||||
create-better-t-stack --auth clerk --backend hono
|
||||
```
|
||||
|
||||
## Example Compatibility
|
||||
@@ -203,8 +218,11 @@ create-better-t-stack --frontend tanstack-router
|
||||
|
||||
### "Authentication requires a database"
|
||||
```bash
|
||||
# Fix by adding database
|
||||
create-better-t-stack --auth --database postgres --orm drizzle
|
||||
# Fix by adding database for Better-Auth
|
||||
create-better-t-stack --auth better-auth --database postgres --orm drizzle
|
||||
|
||||
# Or use Clerk with Convex (no database required)
|
||||
create-better-t-stack --auth clerk --backend convex
|
||||
```
|
||||
|
||||
## Validation Strategy
|
||||
|
||||
@@ -33,6 +33,7 @@ create-better-t-stack [project-directory] [options]
|
||||
- `--database <type>`: `none`, `sqlite`, `postgres`, `mysql`, `mongodb`
|
||||
- `--orm <type>`: `none`, `drizzle`, `prisma`, `mongoose`
|
||||
- `--api <type>`: `none`, `trpc`, `orpc`
|
||||
- `--auth <provider>`: `better-auth`, `clerk`, `none` (see [Options](/docs/cli/options#authentication))
|
||||
- `--db-setup <setup>`: `none`, `turso`, `d1`, `neon`, `supabase`, `prisma-postgres`, `mongodb-atlas`, `docker`
|
||||
- `--examples <types...>`: `none`, `todo`, `ai`
|
||||
- `--web-deploy <setup>`: `none`, `wrangler`, `alchemy`
|
||||
|
||||
@@ -217,16 +217,24 @@ create-better-t-stack --frontend none
|
||||
|
||||
## Authentication
|
||||
|
||||
### `--auth / --no-auth`
|
||||
### `--auth <provider>`
|
||||
|
||||
Include or exclude authentication setup using Better-Auth.
|
||||
Choose authentication provider:
|
||||
|
||||
- `better-auth`: Better-Auth authentication (default)
|
||||
- `clerk`: Clerk authentication (only with Convex backend)
|
||||
- `none`: No authentication
|
||||
|
||||
```bash
|
||||
create-better-t-stack --auth
|
||||
create-better-t-stack --no-auth
|
||||
create-better-t-stack --auth better-auth
|
||||
create-better-t-stack --auth clerk
|
||||
create-better-t-stack --auth none
|
||||
```
|
||||
|
||||
**Note:** Authentication requires both a database and backend framework to be selected. It is automatically disabled when using Convex backend or when no backend is selected.
|
||||
**Note:**
|
||||
- `better-auth` requires both a database and backend framework
|
||||
- `clerk` is only available with Convex backend
|
||||
- Authentication is automatically set to `none` when using `--backend none` or `--database none` (unless using Convex)
|
||||
|
||||
## Addons
|
||||
|
||||
@@ -312,7 +320,7 @@ create-better-t-stack \
|
||||
--runtime bun \
|
||||
--frontend tanstack-router \
|
||||
--api trpc \
|
||||
--auth \
|
||||
--auth better-auth \
|
||||
--addons pwa biome \
|
||||
--examples todo \
|
||||
--package-manager bun \
|
||||
|
||||
@@ -33,7 +33,7 @@ async function createProject() {
|
||||
backend: "hono",
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
auth: true,
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
install: false, // Don't install deps automatically
|
||||
disableAnalytics: true, // Disable analytics
|
||||
@@ -258,7 +258,7 @@ create-better-t-stack my-app \
|
||||
--backend hono \
|
||||
--database postgres \
|
||||
--orm drizzle \
|
||||
--auth \
|
||||
--auth better-auth \
|
||||
--yes
|
||||
```
|
||||
|
||||
@@ -269,7 +269,7 @@ const result = await init("my-app", {
|
||||
backend: "hono",
|
||||
database: "postgres",
|
||||
orm: "drizzle",
|
||||
auth: true,
|
||||
auth: "better-auth",
|
||||
yes: true
|
||||
});
|
||||
```
|
||||
@@ -286,7 +286,7 @@ const result = await init("my-app", {
|
||||
backend: "hono",
|
||||
database: "postgres",
|
||||
orm: "drizzle",
|
||||
auth: true,
|
||||
auth: "better-auth",
|
||||
addons: ["biome", "turborepo"],
|
||||
examples: ["todo"],
|
||||
packageManager: "bun",
|
||||
|
||||
@@ -5,13 +5,14 @@ description: Valid and invalid combinations across frontend, backend, runtime, d
|
||||
|
||||
## Rules
|
||||
|
||||
- **Convex backend**: Disables authentication, database, ORM, and API options
|
||||
- **Convex backend**: Sets database, ORM, and API to `none`; auth to `clerk` (if compatible frontends) or `none`
|
||||
- **Backend `none`**: Forces API, ORM, database, authentication, and runtime to `none`; disables examples
|
||||
- **Frontend `none`**: Backend-only project; PWA/Tauri/examples may be disabled
|
||||
- **API `none`**: No tRPC/oRPC setup; use framework-native APIs
|
||||
- **Database `none`**: Disables ORM and authentication
|
||||
- **Database `none`**: Disables ORM and Better-Auth (but allows Clerk with Convex)
|
||||
- **ORM `none`**: No ORM setup; manage DB manually
|
||||
- **Runtime `none`**: Only with Convex backend or when backend is `none`
|
||||
- **Auth `clerk`**: Only available with Convex backend and compatible frontends
|
||||
|
||||
## Cloudflare Workers
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth \
|
||||
--auth better-auth \
|
||||
--addons turborepo
|
||||
```
|
||||
</Tab>
|
||||
@@ -102,7 +102,7 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth \
|
||||
--auth better-auth \
|
||||
--addons turborepo
|
||||
```
|
||||
</Tab>
|
||||
@@ -113,34 +113,37 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth \
|
||||
--auth better-auth \
|
||||
--addons turborepo
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Convex + React
|
||||
### Convex + React + Clerk
|
||||
|
||||
<Tabs items={['bun', 'pnpm', 'npm']}>
|
||||
<Tab value="bun">
|
||||
```bash
|
||||
bun create better-t-stack@latest my-convex-app \
|
||||
--frontend tanstack-router \
|
||||
--backend convex
|
||||
--backend convex \
|
||||
--auth clerk
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="pnpm">
|
||||
```bash
|
||||
pnpm create better-t-stack@latest my-convex-app \
|
||||
--frontend tanstack-router \
|
||||
--backend convex
|
||||
--backend convex \
|
||||
--auth clerk
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="npm">
|
||||
```bash
|
||||
npx create-better-t-stack@latest my-convex-app \
|
||||
--frontend tanstack-router \
|
||||
--backend convex
|
||||
--backend convex \
|
||||
--auth clerk
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -193,7 +196,7 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth
|
||||
--auth better-auth
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="pnpm">
|
||||
@@ -203,7 +206,7 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth
|
||||
--auth better-auth
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="npm">
|
||||
@@ -213,7 +216,7 @@ Skip prompts and use the default stack:
|
||||
--backend hono \
|
||||
--database sqlite \
|
||||
--orm drizzle \
|
||||
--auth
|
||||
--auth better-auth
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -254,6 +257,7 @@ See the full list in the [CLI Reference](/docs/cli). Key flags:
|
||||
- `--database`: sqlite, postgres, mysql, mongodb, none
|
||||
- `--orm`: drizzle, prisma, mongoose, none
|
||||
- `--api`: trpc, orpc, none
|
||||
- `--auth`: better-auth, clerk, none
|
||||
- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, fumadocs, ultracite, oxlint, ruler, none
|
||||
- `--examples`: todo, ai, none
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ apps/docs/
|
||||
"frontend": ["<next|tanstack-router|react-router|tanstack-start|nuxt|svelte|solid>"] ,
|
||||
"addons": ["<turborepo|biome|husky|pwa|starlight>"] ,
|
||||
"examples": ["<ai|todo|none>"] ,
|
||||
"auth": <true|false>,
|
||||
"auth": <"better-auth"|"clerk"|"none">,
|
||||
"packageManager": "<bun|pnpm|npm>",
|
||||
"dbSetup": "<none|docker|d1>",
|
||||
"api": "<none|trpc|orpc>",
|
||||
@@ -336,7 +336,9 @@ Notes:
|
||||
- **Monorepo**: `apps/*` always; `packages/*` only when needed (Convex)
|
||||
- **React web base**: shadcn/ui primitives, `components.json`, common utilities
|
||||
- **API clients**: `src/utils/trpc.ts` or `src/utils/orpc.ts` added to web/native when selected
|
||||
- **Auth**: Adds `src/lib/auth.ts` on the server and login/dashboard pages on the web app
|
||||
- **Auth**: Adds authentication setup based on provider:
|
||||
- `better-auth`: `src/lib/auth.ts` on server and login/dashboard pages on web app
|
||||
- `clerk`: Clerk provider setup and authentication components
|
||||
- **ORM/DB**: Drizzle/Prisma/Mongoose files added only when selected
|
||||
- **Extras**: `pnpm-workspace.yaml`, `bunfig.toml`, or `.npmrc` added based on package manager and choices
|
||||
- **Deploy**: Workers deploy adds `wrangler.jsonc` templates to the appropriate app(s)
|
||||
|
||||
@@ -62,7 +62,7 @@ async function generateAnalyticsData() {
|
||||
try {
|
||||
console.log("🔄 Fetching analytics data...");
|
||||
|
||||
const response = await fetch("https://r2.amanv.dev/export.csv");
|
||||
const response = await fetch("https://r2.better-t-stack.dev/export.csv");
|
||||
let csvText = await response.text();
|
||||
|
||||
// Fix malformed CSV data - if it's all on one line, try to split it properly
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { Check, ChevronDown, ChevronRight, Copy, Terminal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
|
||||
"use client";
|
||||
import type { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { type Preloaded, usePreloadedQuery } from "convex/react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -25,77 +26,17 @@ import {
|
||||
sortSponsors,
|
||||
} from "@/lib/sponsor-utils";
|
||||
|
||||
export default function SponsorsSection() {
|
||||
export default function SponsorsSection({
|
||||
preloadedSponsors,
|
||||
}: {
|
||||
preloadedSponsors: Preloaded<typeof api.sponsors.getSponsors>;
|
||||
}) {
|
||||
const sponsorsData = usePreloadedQuery(preloadedSponsors);
|
||||
|
||||
const [showPastSponsors, setShowPastSponsors] = useState(false);
|
||||
|
||||
const sponsorsQuery = useQueryWithStatus(api.sponsors.getSponsors);
|
||||
|
||||
if (sponsorsQuery.isPending) {
|
||||
return (
|
||||
<div className="mb-12">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
SPONSORS_DATABASE.JSON
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[LOADING... RECORDS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">LOADING_SPONSORS.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sponsorsQuery.isError) {
|
||||
return (
|
||||
<div className="mb-12">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
SPONSORS_DATABASE.JSON
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[ERROR RECORDS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-destructive">
|
||||
ERROR_LOADING_SPONSORS.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Please try again later!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sponsors =
|
||||
sponsorsQuery.data?.map((sponsor) => ({
|
||||
sponsorsData.map((sponsor) => ({
|
||||
...sponsor,
|
||||
sponsor: {
|
||||
...sponsor.sponsor,
|
||||
@@ -106,7 +47,7 @@ export default function SponsorsSection() {
|
||||
const visibleSponsors = filterVisibleSponsors(sponsors);
|
||||
const sortedSponsors = sortSponsors(visibleSponsors);
|
||||
const currentSponsors = filterCurrentSponsors(sortedSponsors);
|
||||
const pastSponsors = filterPastSponsors(sortedSponsors);
|
||||
const pastSponsors = filterPastSponsors(sortSponsors(sponsors));
|
||||
const specialSponsors = sortSpecialSponsors(
|
||||
filterSpecialSponsors(currentSponsors),
|
||||
);
|
||||
|
||||
@@ -157,7 +157,8 @@ function TechIcon({
|
||||
theme === "light" &&
|
||||
(icon.includes("drizzle") ||
|
||||
icon.includes("prisma") ||
|
||||
icon.includes("express"))
|
||||
icon.includes("express") ||
|
||||
icon.includes("clerk"))
|
||||
) {
|
||||
iconSrc = icon.replace(".svg", "-light.svg");
|
||||
}
|
||||
@@ -205,11 +206,24 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
database: "none",
|
||||
orm: "none",
|
||||
api: "none",
|
||||
auth: "false",
|
||||
dbSetup: "none",
|
||||
examples: ["todo"],
|
||||
};
|
||||
|
||||
const hasClerkCompatibleFrontend =
|
||||
nextStack.webFrontend.some((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||
f,
|
||||
),
|
||||
) ||
|
||||
nextStack.nativeFrontend.some((f) =>
|
||||
["native-nativewind", "native-unistyles"].includes(f),
|
||||
);
|
||||
|
||||
if (nextStack.auth !== "clerk" || !hasClerkCompatibleFrontend) {
|
||||
convexOverrides.auth = "none";
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(convexOverrides)) {
|
||||
const catKey = key as keyof StackState;
|
||||
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
|
||||
@@ -257,7 +271,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
}
|
||||
} else if (isBackendNone) {
|
||||
const noneOverrides: Partial<StackState> = {
|
||||
auth: "false",
|
||||
auth: "none",
|
||||
database: "none",
|
||||
orm: "none",
|
||||
api: "none",
|
||||
@@ -336,20 +350,20 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
message: "ORM set to 'None' (requires a database)",
|
||||
});
|
||||
}
|
||||
if (nextStack.auth === "true") {
|
||||
if (nextStack.auth !== "none" && nextStack.backend !== "convex") {
|
||||
notes.database.notes.push(
|
||||
"Database 'None' selected: Auth will be disabled.",
|
||||
);
|
||||
notes.auth.notes.push(
|
||||
"Authentication requires a database. It will be disabled.",
|
||||
"Authentication requires a database. It will be set to 'None'.",
|
||||
);
|
||||
notes.database.hasIssue = true;
|
||||
notes.auth.hasIssue = true;
|
||||
nextStack.auth = "false";
|
||||
nextStack.auth = "none";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "database",
|
||||
message: "Authentication disabled (requires a database)",
|
||||
message: "Authentication set to 'None' (requires a database)",
|
||||
});
|
||||
}
|
||||
if (nextStack.dbSetup !== "none") {
|
||||
@@ -696,6 +710,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
notes.runtime.hasIssue = true;
|
||||
notes.dbSetup.hasIssue = true;
|
||||
nextStack.dbSetup = "d1";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "runtime",
|
||||
message:
|
||||
@@ -725,6 +740,57 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
});
|
||||
}
|
||||
|
||||
if (nextStack.auth === "clerk") {
|
||||
const hasClerkCompatibleFrontend =
|
||||
nextStack.webFrontend.some((f) =>
|
||||
[
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
].includes(f),
|
||||
) ||
|
||||
nextStack.nativeFrontend.some((f) =>
|
||||
["native-nativewind", "native-unistyles"].includes(f),
|
||||
);
|
||||
|
||||
if (!hasClerkCompatibleFrontend) {
|
||||
notes.auth.notes.push(
|
||||
"Clerk auth is not compatible with the selected frontends. Auth will be set to 'None'.",
|
||||
);
|
||||
notes.webFrontend.notes.push(
|
||||
"Selected frontends are not compatible with Clerk auth. Auth will be disabled.",
|
||||
);
|
||||
notes.auth.hasIssue = true;
|
||||
notes.webFrontend.hasIssue = true;
|
||||
nextStack.auth = "none";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "auth",
|
||||
message:
|
||||
"Auth set to 'None' (Clerk not compatible with selected frontends)",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStack.backend === "convex" && nextStack.auth === "better-auth") {
|
||||
notes.auth.notes.push(
|
||||
"Better-Auth is not compatible with Convex backend. Auth will be set to 'None'.",
|
||||
);
|
||||
notes.backend.notes.push(
|
||||
"Convex backend only supports Clerk auth or no auth. Auth will be disabled.",
|
||||
);
|
||||
notes.auth.hasIssue = true;
|
||||
notes.backend.hasIssue = true;
|
||||
nextStack.auth = "none";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "auth",
|
||||
message:
|
||||
"Auth set to 'None' (Better-Auth not compatible with Convex)",
|
||||
});
|
||||
}
|
||||
|
||||
const incompatibleAddons: string[] = [];
|
||||
const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend);
|
||||
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend);
|
||||
@@ -1120,9 +1186,7 @@ const generateCommand = (stackState: StackState): string => {
|
||||
flags.push(`--orm ${stackState.orm}`);
|
||||
}
|
||||
if (!checkDefault("auth", stackState.auth)) {
|
||||
if (stackState.auth === "false" && DEFAULT_STACK.auth === "true") {
|
||||
flags.push("--no-auth");
|
||||
}
|
||||
flags.push(`--auth ${stackState.auth}`);
|
||||
}
|
||||
if (!checkDefault("dbSetup", stackState.dbSetup)) {
|
||||
flags.push(`--db-setup ${stackState.dbSetup}`);
|
||||
@@ -1527,16 +1591,12 @@ const StackBuilder = () => {
|
||||
update[catKey] = techId;
|
||||
} else {
|
||||
if (
|
||||
(category === "git" ||
|
||||
category === "install" ||
|
||||
category === "auth") &&
|
||||
(category === "git" || category === "install") &&
|
||||
techId === "false"
|
||||
) {
|
||||
update[catKey] = "true";
|
||||
} else if (
|
||||
(category === "git" ||
|
||||
category === "install" ||
|
||||
category === "auth") &&
|
||||
(category === "git" || category === "install") &&
|
||||
techId === "true"
|
||||
) {
|
||||
update[catKey] = "false";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { useNpmDownloadCounter } from "@erquhart/convex-oss-stats/react";
|
||||
import NumberFlow, { continuous } from "@number-flow/react";
|
||||
@@ -12,33 +13,16 @@ import {
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function StatsSection() {
|
||||
const [analyticsData, setAnalyticsData] = useState<{
|
||||
export default function StatsSection({
|
||||
analyticsData,
|
||||
}: {
|
||||
analyticsData: {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: string;
|
||||
lastUpdated: string | null;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAnalytics = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://r2.amanv.dev/analytics-minimal.json",
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAnalyticsData(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch analytics data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAnalytics();
|
||||
}, []);
|
||||
|
||||
};
|
||||
}) {
|
||||
const githubRepo = useQuery(api.stats.getGithubRepo, {
|
||||
name: "AmanVarshney01/create-better-t-stack",
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
|
||||
import type { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { type Preloaded, usePreloadedQuery } from "convex/react";
|
||||
import { Play, Terminal } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { Suspense } from "react";
|
||||
import { Tweet, TweetSkeleton, type TwitterComponents } from "react-tweet";
|
||||
import { Tweet, type TwitterComponents } from "react-tweet";
|
||||
|
||||
export const components: TwitterComponents = {
|
||||
AvatarImg: (props) => {
|
||||
@@ -92,101 +91,27 @@ const TweetCard = ({ tweetId, index }: { tweetId: string; index: number }) => (
|
||||
</div>
|
||||
<div className="w-full min-w-0 overflow-hidden">
|
||||
<div style={{ width: "100%", minWidth: 0, maxWidth: "100%" }}>
|
||||
<Suspense fallback={<TweetSkeleton />}>
|
||||
<Tweet id={tweetId} components={components} />
|
||||
</Suspense>
|
||||
{/* <Suspense fallback={<TweetSkeleton />}> */}
|
||||
<Tweet id={tweetId} components={components} />
|
||||
{/* </Suspense> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default function Testimonials() {
|
||||
const videosQuery = useQueryWithStatus(api.testimonials.getVideos);
|
||||
const tweetsQuery = useQueryWithStatus(api.testimonials.getTweets);
|
||||
export default function Testimonials({
|
||||
preloadedTestimonialsTweet,
|
||||
preloadedTestimonialsVideos,
|
||||
}: {
|
||||
preloadedTestimonialsTweet: Preloaded<typeof api.testimonials.getTweets>;
|
||||
preloadedTestimonialsVideos: Preloaded<typeof api.testimonials.getVideos>;
|
||||
}) {
|
||||
const videosData = usePreloadedQuery(preloadedTestimonialsVideos);
|
||||
const tweetsData = usePreloadedQuery(preloadedTestimonialsTweet);
|
||||
|
||||
const videos = videosQuery.data || [];
|
||||
const tweets = tweetsQuery.data || [];
|
||||
|
||||
if (videosQuery.isPending || tweetsQuery.isPending) {
|
||||
return (
|
||||
<div className="mb-12 w-full max-w-full overflow-hidden px-4">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
VIDEO_TESTIMONIALS.LOG
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[LOADING... ENTRIES]
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-6 rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">LOADING_VIDEOS.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
DEVELOPER_TESTIMONIALS.LOG
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[LOADING... ENTRIES]
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">LOADING_TWEETS.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (videosQuery.isError || tweetsQuery.isError) {
|
||||
return (
|
||||
<div className="mb-12 w-full max-w-full overflow-hidden px-4">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
VIDEO_TESTIMONIALS.LOG
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[ERROR ENTRIES]
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-destructive">
|
||||
ERROR_LOADING_TESTIMONIALS.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Please try again later!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const videos = videosData || [];
|
||||
const tweets = tweetsData || [];
|
||||
|
||||
const getResponsiveColumns = (numCols: number) => {
|
||||
const columns: string[][] = Array(numCols)
|
||||
|
||||
@@ -3,17 +3,13 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import discordIcon from "@/public/icon/discord.svg";
|
||||
|
||||
interface AnalyticsHeaderProps {
|
||||
totalProjects: number;
|
||||
lastUpdated: string | null;
|
||||
loadingLastUpdated: boolean;
|
||||
}
|
||||
|
||||
export function AnalyticsHeader({
|
||||
totalProjects,
|
||||
lastUpdated,
|
||||
loadingLastUpdated,
|
||||
}: AnalyticsHeaderProps) {
|
||||
}: {
|
||||
totalProjects: number;
|
||||
lastUpdated: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
@@ -57,7 +53,7 @@ export function AnalyticsHeader({
|
||||
</Link>
|
||||
{" | "}
|
||||
<Link
|
||||
href="https://r2.amanv.dev/export.csv"
|
||||
href="https://r2.better-t-stack.dev/export.csv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent underline hover:text-primary"
|
||||
@@ -69,12 +65,7 @@ export function AnalyticsHeader({
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Last updated:{" "}
|
||||
{loadingLastUpdated
|
||||
? "CHECKING..."
|
||||
: lastUpdated
|
||||
? `${lastUpdated} UTC`
|
||||
: "UNKNOWN"}
|
||||
Last updated: {lastUpdated ? `${lastUpdated} UTC` : "UNKNOWN"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import Footer from "../../_components/footer";
|
||||
import { AddonsExamplesCharts } from "./addons-examples-charts";
|
||||
import { AnalyticsHeader } from "./analytics-header";
|
||||
import { DevEnvironmentCharts } from "./dev-environment-charts";
|
||||
import { MetricsCards } from "./metrics-cards";
|
||||
import { StackConfigurationCharts } from "./stack-configuration-charts";
|
||||
import { TimelineCharts } from "./timeline-charts";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
|
||||
export default function AnalyticsPage({
|
||||
data,
|
||||
}: {
|
||||
data: AggregatedAnalyticsData | null;
|
||||
}) {
|
||||
const totalProjects = data?.summary?.totalProjects || 0;
|
||||
const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0;
|
||||
const authEnabledPercent = data?.summary?.authEnabledPercent || 0;
|
||||
const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None";
|
||||
const mostPopularBackend = data?.summary?.mostPopularBackend || "None";
|
||||
const mostPopularORM = data?.summary?.mostPopularORM || "None";
|
||||
const mostPopularAPI = data?.summary?.mostPopularAPI || "None";
|
||||
const mostPopularPackageManager =
|
||||
data?.summary?.mostPopularPackageManager || "npm";
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<AnalyticsHeader
|
||||
totalProjects={totalProjects}
|
||||
lastUpdated={data?.lastUpdated || null}
|
||||
/>
|
||||
|
||||
<MetricsCards
|
||||
totalProjects={totalProjects}
|
||||
avgProjectsPerDay={avgProjectsPerDay}
|
||||
authEnabledPercent={authEnabledPercent}
|
||||
mostPopularFrontend={mostPopularFrontend}
|
||||
mostPopularBackend={mostPopularBackend}
|
||||
mostPopularORM={mostPopularORM}
|
||||
mostPopularAPI={mostPopularAPI}
|
||||
mostPopularPackageManager={mostPopularPackageManager}
|
||||
/>
|
||||
|
||||
<TimelineCharts data={data} />
|
||||
|
||||
<StackConfigurationCharts data={data} />
|
||||
|
||||
<AddonsExamplesCharts data={data} />
|
||||
|
||||
<DevEnvironmentCharts data={data} />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export { AddonsExamplesCharts } from "./addons-examples-charts";
|
||||
export { AnalyticsHeader } from "./analytics-header";
|
||||
export * from "./data-utils";
|
||||
export { DevEnvironmentCharts } from "./dev-environment-charts";
|
||||
export { MetricsCards } from "./metrics-cards";
|
||||
export { StackConfigurationCharts } from "./stack-configuration-charts";
|
||||
export { TimelineCharts } from "./timeline-charts";
|
||||
export * from "./types";
|
||||
@@ -1,78 +1,10 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Footer from "../_components/footer";
|
||||
import {
|
||||
AddonsExamplesCharts,
|
||||
type AggregatedAnalyticsData,
|
||||
AnalyticsHeader,
|
||||
DevEnvironmentCharts,
|
||||
MetricsCards,
|
||||
StackConfigurationCharts,
|
||||
TimelineCharts,
|
||||
} from "./_components";
|
||||
import AnalyticsPage from "./_components/analytics-page";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [data, setData] = useState<AggregatedAnalyticsData | null>(null);
|
||||
const [loadingLastUpdated, setLoadingLastUpdated] = useState(true);
|
||||
|
||||
const loadAnalyticsData = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("https://r2.amanv.dev/analytics-data.json");
|
||||
const analyticsData = await response.json();
|
||||
|
||||
setData(analyticsData);
|
||||
console.log("Loaded aggregated analytics data from R2 bucket");
|
||||
console.log(`Data generated at: ${analyticsData.generatedAt}`);
|
||||
} catch (error: unknown) {
|
||||
console.error("Error loading analytics data:", error);
|
||||
} finally {
|
||||
setLoadingLastUpdated(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAnalyticsData();
|
||||
}, [loadAnalyticsData]);
|
||||
|
||||
const totalProjects = data?.summary?.totalProjects || 0;
|
||||
const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0;
|
||||
const authEnabledPercent = data?.summary?.authEnabledPercent || 0;
|
||||
const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None";
|
||||
const mostPopularBackend = data?.summary?.mostPopularBackend || "None";
|
||||
const mostPopularORM = data?.summary?.mostPopularORM || "None";
|
||||
const mostPopularAPI = data?.summary?.mostPopularAPI || "None";
|
||||
const mostPopularPackageManager =
|
||||
data?.summary?.mostPopularPackageManager || "npm";
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<AnalyticsHeader
|
||||
totalProjects={totalProjects}
|
||||
lastUpdated={data?.lastUpdated || null}
|
||||
loadingLastUpdated={loadingLastUpdated}
|
||||
/>
|
||||
|
||||
<MetricsCards
|
||||
totalProjects={totalProjects}
|
||||
avgProjectsPerDay={avgProjectsPerDay}
|
||||
authEnabledPercent={authEnabledPercent}
|
||||
mostPopularFrontend={mostPopularFrontend}
|
||||
mostPopularBackend={mostPopularBackend}
|
||||
mostPopularORM={mostPopularORM}
|
||||
mostPopularAPI={mostPopularAPI}
|
||||
mostPopularPackageManager={mostPopularPackageManager}
|
||||
/>
|
||||
|
||||
<TimelineCharts data={data} />
|
||||
|
||||
<StackConfigurationCharts data={data} />
|
||||
|
||||
<AddonsExamplesCharts data={data} />
|
||||
|
||||
<DevEnvironmentCharts data={data} />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
export default async function Analytics() {
|
||||
const response = await fetch(
|
||||
"https://r2.better-t-stack.dev/analytics-data.json",
|
||||
);
|
||||
const analyticsData = await response.json();
|
||||
|
||||
return <AnalyticsPage data={analyticsData} />;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { baseOptions } from "@/app/layout.config";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const header = document.querySelector("#nd-nav");
|
||||
if (!header) return;
|
||||
|
||||
const main = document.querySelector("main");
|
||||
if (!main) return;
|
||||
|
||||
if (pathname === "/new") {
|
||||
header.classList.remove("*:mx-auto", "*:max-w-fd-container");
|
||||
} else {
|
||||
header.classList.add("*:mx-auto", "*:max-w-fd-container");
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<HomeLayout
|
||||
{...baseOptions}
|
||||
style={
|
||||
{
|
||||
"--spacing-fd-container": "1280px",
|
||||
"--spacing-fd-container": "100%",
|
||||
} as object
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { preloadQuery } from "convex/nextjs";
|
||||
import CommandSection from "./_components/command-section";
|
||||
import Footer from "./_components/footer";
|
||||
import HeroSection from "./_components/hero-section";
|
||||
@@ -6,15 +7,32 @@ import SponsorsSection from "./_components/sponsors-section";
|
||||
import StatsSection from "./_components/stats-section";
|
||||
import Testimonials from "./_components/testimonials";
|
||||
|
||||
export default function HomePage() {
|
||||
export default async function HomePage() {
|
||||
const preloadedSponsors = await preloadQuery(api.sponsors.getSponsors);
|
||||
const preloadedTestimonialsTweet = await preloadQuery(
|
||||
api.testimonials.getTweets,
|
||||
);
|
||||
const preloadedTestimonialsVideos = await preloadQuery(
|
||||
api.testimonials.getVideos,
|
||||
);
|
||||
|
||||
const minimalAnalytics = await fetch(
|
||||
"https://r2.better-t-stack.dev/analytics-minimal.json",
|
||||
);
|
||||
|
||||
const minimalAnalyticsData = await minimalAnalytics.json();
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<main className="mx-auto px-4 pt-12">
|
||||
<HeroSection />
|
||||
<CommandSection />
|
||||
<StatsSection />
|
||||
<SponsorsSection />
|
||||
<Testimonials />
|
||||
<StatsSection analyticsData={minimalAnalyticsData} />
|
||||
<SponsorsSection preloadedSponsors={preloadedSponsors} />
|
||||
<Testimonials
|
||||
preloadedTestimonialsTweet={preloadedTestimonialsTweet}
|
||||
preloadedTestimonialsVideos={preloadedTestimonialsVideos}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import type { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { type Preloaded, usePreloadedQuery } from "convex/react";
|
||||
import { Terminal } from "lucide-react";
|
||||
import Footer from "../../_components/footer";
|
||||
import ShowcaseItem from "../_components/ShowcaseItem";
|
||||
|
||||
export default function ShowcasePage({
|
||||
preloadedShowcase,
|
||||
}: {
|
||||
preloadedShowcase: Preloaded<typeof api.showcase.getShowcaseProjects>;
|
||||
}) {
|
||||
const showcaseProjects = usePreloadedQuery(preloadedShowcase);
|
||||
|
||||
return (
|
||||
<main className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
PROJECT_SHOWCASE.SH
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[{showcaseProjects.length} PROJECTS FOUND]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showcaseProjects.length === 0 ? (
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
NO_SHOWCASE_PROJECTS_FOUND.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Be the first to showcase your project!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{showcaseProjects.map((project, index) => (
|
||||
<ShowcaseItem key={project._id} {...project} index={index} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="rounded border border-border p-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Want to showcase your project? Submit via GitHub issues
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "@better-t-stack/backend/convex/_generated/api";
|
||||
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
|
||||
import { Terminal } from "lucide-react";
|
||||
import Footer from "../_components/footer";
|
||||
import ShowcaseItem from "./_components/ShowcaseItem";
|
||||
import { preloadQuery } from "convex/nextjs";
|
||||
import ShowcasePage from "./_components/showcase-page";
|
||||
|
||||
export default function ShowcasePage() {
|
||||
const showcaseQuery = useQueryWithStatus(api.showcase.getShowcaseProjects);
|
||||
|
||||
if (showcaseQuery.isPending) {
|
||||
return (
|
||||
<main className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
PROJECT_SHOWCASE.SH
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[LOADING... PROJECTS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">LOADING_SHOWCASE.SH</span>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (showcaseQuery.isError) {
|
||||
return (
|
||||
<main className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
PROJECT_SHOWCASE.SH
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[ERROR PROJECTS]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-destructive">
|
||||
ERROR_LOADING_SHOWCASE.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Please try again later!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const showcaseProjects = showcaseQuery.data || [];
|
||||
|
||||
return (
|
||||
<main className="mx-auto min-h-svh max-w-[1280px]">
|
||||
<div className="container mx-auto space-y-8 px-4 py-8 pt-16">
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
PROJECT_SHOWCASE.SH
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-muted-foreground text-xs">
|
||||
[{showcaseProjects.length} PROJECTS FOUND]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showcaseProjects.length === 0 ? (
|
||||
<div className="rounded border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
NO_SHOWCASE_PROJECTS_FOUND.NULL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Be the first to showcase your project!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{showcaseProjects.map((project, index) => (
|
||||
<ShowcaseItem key={project._id} {...project} index={index} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="rounded border border-border p-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className="text-muted-foreground">
|
||||
Want to showcase your project? Submit via GitHub issues
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
export default async function Showcase() {
|
||||
const preloadedShowcase = await preloadQuery(
|
||||
api.showcase.getShowcaseProjects,
|
||||
);
|
||||
return <ShowcasePage preloadedShowcase={preloadedShowcase} />;
|
||||
}
|
||||
|
||||
@@ -396,15 +396,23 @@ export const TECH_OPTIONS: Record<
|
||||
],
|
||||
auth: [
|
||||
{
|
||||
id: "true",
|
||||
name: "Better Auth",
|
||||
description: "Simple authentication",
|
||||
id: "better-auth",
|
||||
name: "Better-Auth",
|
||||
description:
|
||||
"The most comprehensive authentication framework for TypeScript",
|
||||
icon: `${ICON_BASE_URL}/better-auth.svg`,
|
||||
color: "from-green-400 to-green-600",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: "false",
|
||||
id: "clerk",
|
||||
name: "Clerk",
|
||||
description: "More than authentication, Complete User Management",
|
||||
icon: `${ICON_BASE_URL}/clerk.svg`,
|
||||
color: "from-blue-400 to-blue-600",
|
||||
},
|
||||
{
|
||||
id: "none",
|
||||
name: "No Auth",
|
||||
description: "Skip authentication",
|
||||
icon: "",
|
||||
@@ -587,7 +595,7 @@ export const PRESET_TEMPLATES = [
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
dbSetup: "none",
|
||||
auth: "true",
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
@@ -609,7 +617,7 @@ export const PRESET_TEMPLATES = [
|
||||
database: "none",
|
||||
orm: "none",
|
||||
dbSetup: "none",
|
||||
auth: "false",
|
||||
auth: "none",
|
||||
packageManager: "bun",
|
||||
addons: ["turborepo"],
|
||||
examples: ["todo"],
|
||||
@@ -631,7 +639,7 @@ export const PRESET_TEMPLATES = [
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
dbSetup: "none",
|
||||
auth: "true",
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
@@ -653,7 +661,7 @@ export const PRESET_TEMPLATES = [
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
dbSetup: "none",
|
||||
auth: "true",
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
@@ -675,7 +683,7 @@ export const PRESET_TEMPLATES = [
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
dbSetup: "turso",
|
||||
auth: "true",
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
addons: ["pwa", "biome", "husky", "tauri", "starlight", "turborepo"],
|
||||
examples: ["todo", "ai"],
|
||||
@@ -715,7 +723,7 @@ export const DEFAULT_STACK: StackState = {
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
dbSetup: "none",
|
||||
auth: "true",
|
||||
auth: "better-auth",
|
||||
packageManager: "bun",
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
@@ -738,7 +746,7 @@ export const isStackDefault = <K extends keyof StackState>(
|
||||
if (key === "database" && value === "none") return true;
|
||||
if (key === "orm" && value === "none") return true;
|
||||
if (key === "api" && value === "none") return true;
|
||||
if (key === "auth" && value === "false") return true;
|
||||
if (key === "auth" && value === "none") return true;
|
||||
if (key === "dbSetup" && value === "none") return true;
|
||||
if (
|
||||
key === "examples" &&
|
||||
|
||||
@@ -36,7 +36,7 @@ export const stackParsers = {
|
||||
dbSetup: parseAsStringEnum<StackState["dbSetup"]>(
|
||||
getValidIds("dbSetup"),
|
||||
).withDefault(DEFAULT_STACK.dbSetup),
|
||||
auth: parseAsStringEnum<StackState["auth"]>(["true", "false"]).withDefault(
|
||||
auth: parseAsStringEnum<StackState["auth"]>(getValidIds("auth")).withDefault(
|
||||
DEFAULT_STACK.auth,
|
||||
),
|
||||
packageManager: parseAsStringEnum<StackState["packageManager"]>(
|
||||
|
||||
Reference in New Issue
Block a user