feat(cli): add alchemy and improve cli tooling and structure (#520)

This commit is contained in:
Aman Varshney
2025-08-20 23:43:58 +05:30
committed by GitHub
parent c5430ae4fd
commit 5788876c47
152 changed files with 5804 additions and 2264 deletions

1
apps/web/.gitignore vendored
View File

@@ -15,6 +15,7 @@ public/analytics-minimal.json
*.tsbuildinfo
/.open-next/
/.wrangler/
.alchemy
# misc
.DS_Store

View File

@@ -149,11 +149,11 @@ create-better-t-stack --frontend next native-nativewind
### Tauri (Desktop Apps)
- Requires web frontend
- Compatible frontends: `tanstack-router`, `react-router`, `nuxt`, `svelte`, `solid`
- Compatible frontends: `tanstack-router`, `react-router`, `nuxt`, `svelte`, `solid`, `next`
- Cannot be combined with native frameworks
### Web Deployment
- `--web-deploy workers` requires a web frontend
- `--web-deploy wrangler` requires a web frontend
- Cannot be used with native-only projects
## Authentication Requirements

View File

@@ -35,9 +35,11 @@ create-better-t-stack [project-directory] [options]
- `--api <type>`: `none`, `trpc`, `orpc`
- `--db-setup <setup>`: `none`, `turso`, `d1`, `neon`, `supabase`, `prisma-postgres`, `mongodb-atlas`, `docker`
- `--examples <types...>`: `none`, `todo`, `ai`
- `--web-deploy <setup>`: `none`, `workers`
- `--web-deploy <setup>`: `none`, `wrangler`, `alchemy`
- `--server-deploy <setup>`: `none`, `wrangler`, `alchemy`
- `--directory-conflict <strategy>`: `merge`, `overwrite`, `increment`, `error`
- `--render-title / --no-render-title`: Show/hide ASCII art title
- `--disable-analytics / --no-disable-analytics`: Control analytics collection
See the full reference in [Options](/docs/cli/options).
@@ -45,7 +47,7 @@ See the full reference in [Options](/docs/cli/options).
```bash
# Default setup with prompts
create-better-t-stack
create-better-t-stack
# Quick setup with defaults
create-better-t-stack --yes
@@ -65,7 +67,8 @@ create-better-t-stack add [options]
### Options
- `--addons <types...>`: Addons to add (see [Addons](/docs/cli/options#addons))
- `--web-deploy <setup>`: Web deployment setup (`workers`, `none`)
- `--web-deploy <setup>`: Web deployment setup (`none`, `wrangler`, `alchemy`)
- `--server-deploy <setup>`: Server deployment setup (`none`, `wrangler`, `alchemy`)
- `--project-dir <path>`: Project directory (defaults to current directory)
- `--install`: Install dependencies after adding
- `--package-manager <pm>`: Package manager to use
@@ -80,7 +83,7 @@ create-better-t-stack add
create-better-t-stack add --addons pwa tauri --install
# Add deployment setup
create-better-t-stack add --web-deploy workers
create-better-t-stack add --web-deploy wrangler
```
## `sponsors`

View File

@@ -79,6 +79,20 @@ create-better-t-stack my-app --yes --directory-conflict overwrite
create-better-t-stack my-app --yes --directory-conflict increment
```
### `--disable-analytics / --no-disable-analytics`
Control whether analytics and telemetry data is collected.
```bash
# Disable analytics collection
create-better-t-stack --disable-analytics
# Enable analytics collection (default)
create-better-t-stack --no-disable-analytics
```
Analytics help improve Better-T Stack by providing insights into usage patterns. When disabled, no data is collected or transmitted.
## Database Options
### `--database <type>`
@@ -212,6 +226,8 @@ create-better-t-stack --auth
create-better-t-stack --no-auth
```
**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.
## Addons
### `--addons <types...>`
@@ -255,12 +271,31 @@ create-better-t-stack --examples todo ai
Web deployment configuration:
- `none`: No deployment setup
- `workers`: Cloudflare Workers deployment
- `wrangler`: Cloudflare Workers deployment
- `alchemy`: Cloudflare Workers deployment (via Alchemy infrastructure as code)
```bash
create-better-t-stack --web-deploy workers
create-better-t-stack --web-deploy wrangler
create-better-t-stack --web-deploy alchemy
```
**Note:** Alchemy uses TypeScript to define infrastructure programmatically. See the [Infrastructure as Code with Alchemy Guide](/docs/guides/alchemy-deployment) for details.
### `--server-deploy <setup>`
Server deployment configuration:
- `none`: No deployment setup
- `wrangler`: Cloudflare Workers deployment (when runtime is workers)
- `alchemy`: Cloudflare Workers deployment (when runtime is workers, via Alchemy infrastructure as code)
```bash
create-better-t-stack --server-deploy wrangler
create-better-t-stack --server-deploy alchemy
```
**Note:** Alchemy uses TypeScript to define infrastructure programmatically. See the [Infrastructure as Code with Alchemy Guide](/docs/guides/alchemy-deployment) for details.
## Option Validation
The CLI validates option combinations and will show errors for incompatible selections. See the [Compatibility](/docs/cli/compatibility) page for detailed rules.
@@ -281,6 +316,8 @@ create-better-t-stack \
--addons pwa biome \
--examples todo \
--package-manager bun \
--web-deploy wrangler \
--server-deploy alchemy \
--install
```

View File

@@ -146,6 +146,7 @@ interface CreateInput {
runtime?: Runtime; // Runtime environment
api?: API; // API type
webDeploy?: WebDeploy; // Web deployment setup
serverDeploy?: ServerDeploy; // Server deployment setup
directoryConflict?: DirectoryConflict; // "merge" | "overwrite" | "increment" | "error"
renderTitle?: boolean; // Show ASCII art title
disableAnalytics?: boolean; // Disable analytics and telemetry

View File

@@ -254,7 +254,6 @@ 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
- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, none
- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, fumadocs, ultracite, oxlint, ruler, none
- `--examples`: todo, ai, none

View File

@@ -260,7 +260,8 @@ apps/docs/
"packageManager": "<bun|pnpm|npm>",
"dbSetup": "<none|docker|d1>",
"api": "<none|trpc|orpc>",
"webDeploy": "<none|workers>"
"webDeploy": "<none|wrangler|alchemy>",
"serverDeploy": "<none|wrangler|alchemy>"
}
```

View File

@@ -10,6 +10,7 @@ const config = {
{ protocol: "https", hostname: "pbs.twimg.com" },
{ protocol: "https", hostname: "abs.twimg.com" },
{ protocol: "https", hostname: "r2.better-t-stack.dev" },
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
],
},
outputFileTracingExcludes: {

View File

@@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"convex": "^1.25.4",
"convex-helpers": "^0.1.104",
"date-fns": "^4.1.0",
"fumadocs-core": "15.6.7",
"fumadocs-mdx": "11.7.3",

View File

@@ -1,178 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://better-t-stack.dev/schema.json",
"title": "Better-T-Stack Configuration",
"description": "Configuration file for Better-T-Stack projects",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference for validation"
},
"version": {
"type": "string",
"description": "CLI version used to create this project",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the project was created"
},
"database": {
"type": "string",
"enum": [
"none",
"sqlite",
"postgres",
"mysql",
"mongodb"
],
"description": "Database type"
},
"orm": {
"type": "string",
"enum": [
"drizzle",
"prisma",
"mongoose",
"none"
],
"description": "ORM type"
},
"backend": {
"type": "string",
"enum": [
"hono",
"express",
"fastify",
"next",
"elysia",
"convex",
"none"
],
"description": "Backend framework"
},
"runtime": {
"type": "string",
"enum": [
"bun",
"node",
"workers",
"none"
],
"description": "Runtime environment (workers only available with hono backend and drizzle orm)"
},
"frontend": {
"type": "array",
"items": {
"type": "string",
"enum": [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"native-nativewind",
"native-unistyles",
"svelte",
"solid",
"none"
]
},
"description": "Frontend framework"
},
"addons": {
"type": "array",
"items": {
"type": "string",
"enum": [
"pwa",
"tauri",
"starlight",
"biome",
"husky",
"turborepo",
"fumadocs",
"ultracite",
"oxlint",
"none"
]
},
"description": "Additional addons"
},
"examples": {
"type": "array",
"items": {
"type": "string",
"enum": [
"todo",
"ai",
"none"
]
},
"description": "Example templates to include"
},
"auth": {
"type": "boolean",
"description": "Whether authentication is enabled"
},
"packageManager": {
"type": "string",
"enum": [
"npm",
"pnpm",
"bun"
],
"description": "Package manager"
},
"dbSetup": {
"type": "string",
"enum": [
"turso",
"neon",
"prisma-postgres",
"mongodb-atlas",
"supabase",
"d1",
"docker",
"none"
],
"description": "Database hosting setup"
},
"api": {
"type": "string",
"enum": [
"trpc",
"orpc",
"none"
],
"description": "API type"
},
"webDeploy": {
"type": "string",
"enum": [
"workers",
"none"
],
"description": "Web deployment"
}
},
"required": [
"version",
"createdAt",
"database",
"orm",
"backend",
"runtime",
"frontend",
"addons",
"examples",
"auth",
"packageManager",
"dbSetup",
"api",
"webDeploy"
],
"additionalProperties": false
}

View File

@@ -13,6 +13,7 @@ import {
ORMSchema,
PackageManagerSchema,
RuntimeSchema,
ServerDeploySchema,
WebDeploySchema,
} from "../../cli/src/types";
@@ -27,6 +28,7 @@ const PACKAGE_MANAGER_VALUES = PackageManagerSchema.options;
const DATABASE_SETUP_VALUES = DatabaseSetupSchema.options;
const API_VALUES = APISchema.options;
const WEB_DEPLOY_VALUES = WebDeploySchema.options;
const SERVER_DEPLOY_VALUES = ServerDeploySchema.options;
const configSchema = {
$schema: "http://json-schema.org/draft-07/schema#",
@@ -117,6 +119,11 @@ const configSchema = {
enum: WEB_DEPLOY_VALUES,
description: WebDeploySchema.description,
},
serverDeploy: {
type: "string",
enum: SERVER_DEPLOY_VALUES,
description: ServerDeploySchema.description,
},
},
required: [
"version",
@@ -133,6 +140,7 @@ const configSchema = {
"dbSetup",
"api",
"webDeploy",
"serverDeploy",
],
additionalProperties: false,
};

View File

@@ -26,7 +26,7 @@ export default function CustomizableSection() {
transition={{ duration: 0.5, delay: 0.2 }}
className="mx-auto max-w-3xl space-y-6"
>
<p className=" text-lg text-muted-foreground leading-relaxed sm:text-xl">
<p className="text-lg text-muted-foreground leading-relaxed sm:text-xl">
Build your perfect TypeScript stack.
</p>
</motion.div>

View File

@@ -98,7 +98,7 @@ export default function Navbar() {
className={cn(
"fixed top-0 z-[100] w-full transition-all duration-300 ease-in-out",
scrolled
? " border- border-border shadow-sm backdrop-blur-md"
? "border- border-border shadow-sm backdrop-blur-md"
: "border-transparent border-b bg-transparent",
)}
>
@@ -183,7 +183,7 @@ export default function Navbar() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className=" fixed inset-0 z-[98 backdrop-blur-sm lg:hidden"
className="fixed inset-0 z-[98 backdrop-blur-sm lg:hidden"
onClick={closeMobileMenu}
aria-hidden="true"
/>

View File

@@ -1,3 +1,5 @@
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
import {
ChevronDown,
ChevronUp,
@@ -8,12 +10,10 @@ import {
Terminal,
} from "lucide-react";
import Image from "next/image";
// import Link from "next/link";
import { useEffect, useState } from "react";
import { useState } from "react";
import {
filterCurrentSponsors,
filterPastSponsors,
filterRegularSponsors,
filterSpecialSponsors,
formatSponsorUrl,
getSponsorUrl,
@@ -21,39 +21,91 @@ import {
sortSpecialSponsors,
sortSponsors,
} from "@/lib/sponsor-utils";
import type { Sponsor } from "@/lib/types";
export default function SponsorsSection() {
const [sponsors, setSponsors] = useState<Sponsor[]>([]);
const [loadingSponsors, setLoadingSponsors] = useState(true);
const [sponsorError, setSponsorError] = useState<string | null>(null);
const [showPastSponsors, setShowPastSponsors] = useState(false);
useEffect(() => {
fetch("https://sponsors.amanv.dev/sponsors.json")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch sponsors");
return res.json();
})
.then((data) => {
const sponsorsData = Array.isArray(data) ? data : [];
const sortedSponsors = sortSponsors(sponsorsData);
setSponsors(sortedSponsors);
setLoadingSponsors(false);
})
.catch(() => {
setSponsorError("Could not load sponsors");
setLoadingSponsors(false);
});
}, []);
const sponsorsQuery = useQueryWithStatus(api.sponsors.getSponsors);
const currentSponsors = filterCurrentSponsors(sponsors);
const pastSponsors = filterPastSponsors(sponsors);
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) => ({
...sponsor,
sponsor: {
...sponsor.sponsor,
customLogoUrl: sponsor.sponsor.customLogoUrl || "",
},
})) || [];
const sortedSponsors = sortSponsors(sponsors);
const currentSponsors = filterCurrentSponsors(sortedSponsors);
const pastSponsors = filterPastSponsors(sortedSponsors);
const specialSponsors = sortSpecialSponsors(
filterSpecialSponsors(currentSponsors),
);
const regularSponsors = filterRegularSponsors(currentSponsors);
return (
<div className="mb-12">
@@ -65,37 +117,24 @@ export default function SponsorsSection() {
</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">
[{loadingSponsors ? "LOADING..." : sponsors.length} RECORDS]
</span>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
[{sponsors.length} RECORDS]
</span>
</div>
</div>
{loadingSponsors ? (
<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>
) : sponsorError ? (
<div className="rounded border border-border bg-destructive/10 p-8">
<div className="flex items-center justify-center gap-2">
<span className="text-destructive"></span>
<span className=" text-destructive">ERROR: {sponsorError}</span>
</div>
</div>
) : sponsors.length === 0 ? (
{sponsors.length === 0 ? (
<div className="space-y-4">
<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">
<span className="text-muted-foreground">
NO_SPONSORS_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">
<span className="text-muted-foreground">
Be the first to support this project!
</span>
</div>
@@ -162,7 +201,7 @@ export default function SponsorsSection() {
{entry.sponsor.name || entry.sponsor.login}
</h3>
{entry.tierName && (
<p className=" text-primary text-xs">
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
@@ -203,90 +242,93 @@ export default function SponsorsSection() {
</div>
</div>
)}
{regularSponsors.length > 0 && (
{currentSponsors.filter((s) => !isSpecialSponsor(s)).length > 0 && (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{regularSponsors.map((entry, index) => {
const since = new Date(entry.createdAt).toLocaleDateString(
undefined,
{ year: "numeric", month: "short" },
);
return (
<div
key={entry.sponsor.login}
className="rounded border border-border"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="border-border border-b px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
<span>SINCE {since.toUpperCase()}</span>
{currentSponsors
.filter((s) => !isSpecialSponsor(s))
.map((entry, index) => {
const since = new Date(entry.createdAt).toLocaleDateString(
undefined,
{ year: "numeric", month: "short" },
);
return (
<div
key={entry.sponsor.login}
className="rounded border border-border"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="border-border border-b px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
<span>SINCE {since.toUpperCase()}</span>
</div>
</div>
</div>
</div>
<div className="p-4">
<div className="flex gap-4">
<div className="flex-shrink-0">
<Image
src={entry.sponsor.avatarUrl}
alt={entry.sponsor.name || entry.sponsor.login}
width={100}
height={100}
className="rounded border border-border transition-colors duration-300"
unoptimized
/>
</div>
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
<div>
<h3 className="truncate font-semibold text-foreground text-sm">
{entry.sponsor.name || entry.sponsor.login}
</h3>
{entry.tierName && (
<p className=" text-primary text-xs">
{entry.tierName}
</p>
)}
<div className="p-4">
<div className="flex gap-4">
<div className="flex-shrink-0">
<Image
src={entry.sponsor.avatarUrl}
alt={entry.sponsor.name || entry.sponsor.login}
width={100}
height={100}
className="rounded border border-border transition-colors duration-300"
unoptimized
/>
</div>
<div className="flex flex-col gap-1">
<a
href={`https://github.com/${entry.sponsor.login}`}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Github className="h-4 w-4" />
<span className="truncate">
{entry.sponsor.login}
</span>
</a>
{(entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl) && (
<div className="grid grid-cols-1 grid-rows-[1fr_auto] justify-between py-2">
<div>
<h3 className="truncate font-semibold text-foreground text-sm">
{entry.sponsor.name || entry.sponsor.login}
</h3>
{entry.tierName && (
<p className="text-primary text-xs">
{entry.tierName}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<a
href={
entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl
}
href={`https://github.com/${entry.sponsor.login}`}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Globe className="h-4 w-4" />
<Github className="h-4 w-4" />
<span className="truncate">
{formatSponsorUrl(
entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl,
)}
{entry.sponsor.login}
</span>
</a>
)}
{(entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl) && (
<a
href={
entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl
}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2 text-muted-foreground text-xs transition-colors hover:text-primary"
>
<Globe className="h-4 w-4" />
<span className="truncate">
{formatSponsorUrl(
entry.sponsor.websiteUrl ||
entry.sponsor.linkUrl,
)}
</span>
</a>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
})}
);
})}
</div>
</div>
)}

View File

@@ -70,6 +70,7 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"orm",
"dbSetup",
"webDeploy",
"serverDeploy",
"auth",
"packageManager",
"addons",
@@ -122,6 +123,7 @@ const getBadgeColors = (category: string): string => {
return "border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700/30 dark:bg-orange-900/30 dark:text-orange-300";
case "git":
case "webDeploy":
case "serverDeploy":
case "install":
return "border-gray-300 bg-gray-100 text-gray-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400";
default:
@@ -716,10 +718,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
if (!isPWACompat && nextStack.addons.includes("pwa")) {
incompatibleAddons.push("pwa");
notes.webFrontend.notes.push(
"PWA addon requires TanStack/React Router or Solid. Addon will be removed.",
"PWA addon requires TanStack Router, React Router, Solid, or Next.js. Addon will be removed.",
);
notes.addons.notes.push(
"PWA requires TanStack/React Router/Solid. It will be removed.",
"PWA requires TanStack Router, React Router, Solid, or Next.js. It will be removed.",
);
notes.webFrontend.hasIssue = true;
notes.addons.hasIssue = true;
@@ -731,10 +733,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
if (!isTauriCompat && nextStack.addons.includes("tauri")) {
incompatibleAddons.push("tauri");
notes.webFrontend.notes.push(
"Tauri addon requires TanStack/React Router, Nuxt, Svelte, Solid, or Next.js. Addon will be removed.",
"Tauri addon requires TanStack Router, React Router, Nuxt, Svelte, Solid, or Next.js. Addon will be removed.",
);
notes.addons.notes.push(
"Tauri requires TanStack/React Router/Nuxt/Svelte/Solid/Next.js. It will be removed.",
"Tauri requires TanStack Router, React Router, Nuxt, Svelte, Solid, or Next.js. It will be removed.",
);
notes.webFrontend.hasIssue = true;
notes.addons.hasIssue = true;
@@ -881,6 +883,160 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
});
}
// Server deployment requires a backend (and not Convex)
if (
nextStack.serverDeploy !== "none" &&
(nextStack.backend === "none" || nextStack.backend === "convex")
) {
notes.serverDeploy.notes.push(
"Server deployment requires a supported backend. It will be disabled.",
);
notes.backend.notes.push(
"No compatible backend selected: Server deployment has been disabled.",
);
notes.serverDeploy.hasIssue = true;
notes.backend.hasIssue = true;
nextStack.serverDeploy = "none";
changed = true;
changes.push({
category: "serverDeploy",
message: "Server deployment set to 'none' (requires backend)",
});
}
// Cloudflare server deployments (wrangler/alchemy) require Workers runtime
if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") {
notes.serverDeploy.notes.push(
"Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.",
);
notes.runtime.notes.push(
"Server deployment requires Cloudflare Workers runtime. It will be selected.",
);
notes.serverDeploy.hasIssue = true;
notes.runtime.hasIssue = true;
nextStack.runtime = "workers";
changed = true;
changes.push({
category: "serverDeploy",
message:
"Runtime set to 'Cloudflare Workers' (required by server deployment)",
});
// Apply Workers runtime compatibility adjustments
if (nextStack.backend !== "hono") {
notes.runtime.notes.push(
"Cloudflare Workers runtime requires Hono backend. Hono will be selected.",
);
notes.backend.notes.push(
"Cloudflare Workers runtime requires Hono backend. It will be selected.",
);
notes.runtime.hasIssue = true;
notes.backend.hasIssue = true;
nextStack.backend = "hono";
changes.push({
category: "runtime",
message: "Backend set to 'Hono' (required by Cloudflare Workers)",
});
}
if (nextStack.orm !== "drizzle" && nextStack.orm !== "none") {
notes.runtime.notes.push(
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
);
notes.orm.notes.push(
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
);
notes.runtime.hasIssue = true;
notes.orm.hasIssue = true;
nextStack.orm = "drizzle";
changes.push({
category: "runtime",
message: "ORM set to 'Drizzle' (required by Cloudflare Workers)",
});
}
if (nextStack.database === "mongodb") {
notes.runtime.notes.push(
"Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.",
);
notes.database.notes.push(
"MongoDB is not compatible with Cloudflare Workers runtime. SQLite will be selected.",
);
notes.runtime.hasIssue = true;
notes.database.hasIssue = true;
nextStack.database = "sqlite";
changes.push({
category: "runtime",
message:
"Database set to 'SQLite' (MongoDB not compatible with Workers)",
});
}
if (nextStack.dbSetup === "docker") {
notes.runtime.notes.push(
"Cloudflare Workers runtime does not support Docker setup. D1 will be selected.",
);
notes.dbSetup.notes.push(
"Docker setup is not compatible with Cloudflare Workers runtime. D1 will be selected.",
);
notes.runtime.hasIssue = true;
notes.dbSetup.hasIssue = true;
nextStack.dbSetup = "d1";
changes.push({
category: "runtime",
message: "DB Setup set to 'D1' (Docker not compatible with Workers)",
});
}
}
// Alchemy deployment validation - temporarily not compatible with Next.js and React Router
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
const incompatibleFrontends = nextStack.webFrontend.filter(
(f) => f === "next" || f === "react-router",
);
if (incompatibleFrontends.length > 0) {
const deployType =
isAlchemyWebDeploy && isAlchemyServerDeploy
? "web and server deployment"
: isAlchemyWebDeploy
? "web deployment"
: "server deployment";
notes.webFrontend.notes.push(
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}. These frontends will be removed.`,
);
notes.webDeploy.notes.push(
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
);
notes.serverDeploy.notes.push(
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
);
notes.webFrontend.hasIssue = true;
notes.webDeploy.hasIssue = true;
notes.serverDeploy.hasIssue = true;
// Remove incompatible frontends
nextStack.webFrontend = nextStack.webFrontend.filter(
(f) => f !== "next" && f !== "react-router",
);
// If no web frontends remain, set to default
if (nextStack.webFrontend.length === 0) {
nextStack.webFrontend = ["tanstack-router"];
}
changed = true;
changes.push({
category: "alchemy",
message: `Removed ${incompatibleFrontends.join(" and ")} (not compatible with Alchemy ${deployType})`,
});
}
}
return {
adjustedStack: changed ? nextStack : null,
notes,
@@ -985,6 +1141,13 @@ const generateCommand = (stackState: StackState): string => {
flags.push(`--web-deploy ${stackState.webDeploy}`);
}
if (
stackState.serverDeploy &&
!checkDefault("serverDeploy", stackState.serverDeploy)
) {
flags.push(`--server-deploy ${stackState.serverDeploy}`);
}
if (!checkDefault("install", stackState.install)) {
if (stackState.install === "false" && DEFAULT_STACK.install === "true") {
flags.push("--no-install");
@@ -1461,6 +1624,19 @@ const StackBuilder = () => {
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
const finalStack = adjustedStack ?? simulatedStack;
// Additional check for Alchemy compatibility with Next.js and React Router
if (
category === "webFrontend" &&
(optionId === "next" || optionId === "react-router")
) {
const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy";
const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy";
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
return false;
}
}
if (
category === "webFrontend" ||
category === "nativeFrontend" ||

View File

@@ -1,127 +1,13 @@
"use client";
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
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";
const YOUTUBE_VIDEOS = [
{
embedId: "VL6zJH6z8wY",
title: "Advanced Vibe Coding - setup for new projects",
},
{
embedId: "cdivzGRhsYk",
title:
"MY UPGRADED AI Coding Workflow + Free APIs: How I DO AI Coding! (Stitch, Better T3, SuperNinja)",
},
{
embedId: "azhw_iq8SIA",
title: "This CLI Lets You Choose Your Entire Tech Stack Instantly",
},
{
embedId: "CWwkWJmT_zU",
title: "The BEST Way To Start a Project (Better-T-Stack)",
},
{
embedId: "MGmPTcgJYIo",
title: "This new CLI tool makes scaffolding projects easy",
},
{
embedId: "g-ynSAdL6Ak",
title: "This tool cured my JavaScript fatigue",
},
{
embedId: "uHUgw-Hi8HE",
title: "I tried React again after 2 years of Svelte",
},
];
const TWEET_IDS = [
"1930194170418999437",
"1907728148294447538",
"1936942642069455037",
"1931029815047455149",
"1933149770639614324",
"1937599252173128103",
"1947357370302304559",
"1930511724702285885",
"1945591420657532994",
"1945204056063913989",
"1912836377365905496",
"1947973299805561005",
"1949843350250738126",
"1949907407657992231",
"1907817662215757853",
"1933216760896934060",
"1949912886958301546",
"1942558041704182158",
"1947636576118304881",
"1951704580691304693",
"1937383786637094958",
"1931709370003583004",
"1929147326955704662",
"1948050877454938549",
"1951599045383770386",
"1904228496144269699",
"1949851365435469889",
"1950457707632214136",
"1930257410259616057",
"1937258706279817570",
"1917815700980391964",
"1949921211586400419",
"1947812547551498466",
"1928317790588403953",
"1917640304758514093",
"1951703990896570459",
"1907831059275735353",
"1912924558522524039",
"1945054982870282575",
"1933150129738981383",
"1949907577611145726",
"1911490975173607495",
"1930104047845158972",
"1913773945523953713",
"1951540684340469950",
"1944937093387706572",
"1904241046898556970",
"1913834145471672652",
"1946245671880966269",
"1930514202260635807",
"1931589579749892480",
"1904144343125860404",
"1917610656477348229",
"1904215768272654825",
"1931830211013718312",
"1944895251811893680",
"1913833079342522779",
"1930449311848087708",
"1942680754384953790",
"1907723601731530820",
"1944553262792810603",
"1904233896851521980",
"1930294868808515726",
"1943290033383047237",
"1913801258789491021",
"1907841646513005038",
"1904301540422070671",
"1944208789617471503",
"1912837026925195652",
"1904338606409531710",
"1942965795920679188",
"1904318186750652606",
"1943656585294643386",
"1908568583799484519",
"1913018977321693448",
"1904179661086556412",
"1908558365128876311",
"1907772878139072851",
"1906149740095705265",
"1906001923456790710",
"1906570888897777847",
];
export const components: TwitterComponents = {
AvatarImg: (props) => {
if (!props.src || props.src === "") {
@@ -143,15 +29,173 @@ export const components: TwitterComponents = {
},
};
const VideoCard = ({
video,
index,
}: {
video: { embedId: string; title: string };
index: number;
}) => (
<motion.div
className="w-full min-w-0"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
delay: index * 0.1,
duration: 0.4,
ease: "easeOut",
}}
>
<div className="w-full min-w-0 overflow-hidden rounded border border-border">
<div className="sticky top-0 z-10 border-border border-b px-2 py-2">
<div className="flex items-center gap-2">
<Play className="h-3 w-3 text-primary" />
<span className="font-semibold text-xs">
[VIDEO_{String(index + 1).padStart(3, "0")}]
</span>
</div>
</div>
<div className="w-full min-w-0 overflow-hidden">
<div className="relative aspect-video w-full">
<iframe
src={`https://www.youtube.com/embed/${video.embedId}`}
title={video.title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="absolute inset-0 h-full w-full"
/>
</div>
</div>
</div>
</motion.div>
);
const TweetCard = ({ tweetId, index }: { tweetId: string; index: number }) => (
<motion.div
className="w-full min-w-0"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
delay: index * 0.05,
duration: 0.4,
ease: "easeOut",
}}
>
<div className="w-full min-w-0 overflow-hidden rounded border border-border">
<div className="sticky top-0 z-10 border-border border-b px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-semibold text-xs">
[TWEET_{String(index + 1).padStart(3, "0")}]
</span>
</div>
</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>
</div>
</div>
</div>
</motion.div>
);
export default function Testimonials() {
const videosQuery = useQueryWithStatus(api.testimonials.getVideos);
const tweetsQuery = useQueryWithStatus(api.testimonials.getTweets);
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 getResponsiveColumns = (numCols: number) => {
const columns: string[][] = Array(numCols)
.fill(null)
.map(() => []);
TWEET_IDS.forEach((tweetId, index) => {
tweets.forEach((tweet, index) => {
const colIndex = index % numCols;
columns[colIndex].push(tweetId);
columns[colIndex].push(tweet.tweetId);
});
return columns;
@@ -173,84 +217,6 @@ export default function Testimonials() {
},
};
const VideoCard = ({
video,
index,
}: {
video: (typeof YOUTUBE_VIDEOS)[0];
index: number;
}) => (
<motion.div
className="w-full min-w-0"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
delay: index * 0.1,
duration: 0.4,
ease: "easeOut",
}}
>
<div className="w-full min-w-0 overflow-hidden rounded border border-border">
<div className="sticky top-0 z-10 border-border border-b px-2 py-2">
<div className="flex items-center gap-2">
<Play className="h-3 w-3 text-primary" />
<span className="font-semibold text-xs">
[VIDEO_{String(index + 1).padStart(3, "0")}]
</span>
</div>
</div>
<div className="w-full min-w-0 overflow-hidden">
<div className="relative aspect-video w-full">
<iframe
src={`https://www.youtube.com/embed/${video.embedId}`}
title={video.title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="absolute inset-0 h-full w-full"
/>
</div>
</div>
</div>
</motion.div>
);
const TweetCard = ({
tweetId,
index,
}: {
tweetId: string;
index: number;
}) => (
<motion.div
className="w-full min-w-0"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
delay: index * 0.05,
duration: 0.4,
ease: "easeOut",
}}
>
<div className="w-full min-w-0 overflow-hidden rounded border border-border">
<div className="sticky top-0 z-10 border-border border-b px-3 py-2">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-semibold text-xs">
[TWEET_{String(index + 1).padStart(3, "0")}]
</span>
</div>
</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>
</div>
</div>
</div>
</motion.div>
);
return (
<div className="mb-12 w-full max-w-full overflow-hidden px-4">
<div className="mb-8">
@@ -263,7 +229,7 @@ export default function Testimonials() {
</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">
[{YOUTUBE_VIDEOS.length} ENTRIES]
[{videos.length} ENTRIES]
</span>
</div>
@@ -274,7 +240,7 @@ export default function Testimonials() {
initial="hidden"
animate="visible"
>
{YOUTUBE_VIDEOS.map((video, index) => (
{videos.map((video, index) => (
<VideoCard
key={`video-${video.embedId}`}
video={video}
@@ -291,7 +257,7 @@ export default function Testimonials() {
initial="hidden"
animate="visible"
>
{YOUTUBE_VIDEOS.map((video, index) => (
{videos.map((video, index) => (
<VideoCard
key={`video-${video.embedId}`}
video={video}
@@ -311,7 +277,7 @@ export default function Testimonials() {
</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">
[{TWEET_IDS.length} ENTRIES]
[{tweets.length} ENTRIES]
</span>
</div>
<div className="block sm:hidden">
@@ -321,8 +287,12 @@ export default function Testimonials() {
initial="hidden"
animate="visible"
>
{TWEET_IDS.map((tweetId, index) => (
<TweetCard key={tweetId} tweetId={tweetId} index={index} />
{tweets.map((tweet, index) => (
<TweetCard
key={tweet.tweetId}
tweetId={tweet.tweetId}
index={index}
/>
))}
</motion.div>
</div>
@@ -336,7 +306,7 @@ export default function Testimonials() {
>
{getResponsiveColumns(2).map((column, colIndex) => (
<motion.div
key={column.join("-")}
key={`col-2-${column.length > 0 ? column[0] : `empty-${colIndex}`}`}
className="flex min-w-0 flex-col gap-4"
variants={columnVariants}
>
@@ -364,7 +334,7 @@ export default function Testimonials() {
>
{getResponsiveColumns(3).map((column, colIndex) => (
<motion.div
key={column.join("-")}
key={`col-3-${column.length > 0 ? column[0] : `empty-${colIndex}`}`}
className="flex min-w-0 flex-col gap-4"
variants={columnVariants}
>

View File

@@ -22,7 +22,7 @@ export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">ADDONS_USAGE.BAR</span>
<span className="font-semibold text-sm">ADDONS_USAGE.BAR</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Additional features and tooling adoption
@@ -37,7 +37,7 @@ export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
@@ -72,7 +72,7 @@ export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">EXAMPLES_USAGE.BAR</span>
<span className="font-semibold text-sm">EXAMPLES_USAGE.BAR</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Example applications included in projects
@@ -87,7 +87,7 @@ export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />

View File

@@ -32,20 +32,20 @@ export function AnalyticsHeader({
<div className="rounded rounded-b-none border border-border p-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className=" text-foreground">
<span className="text-foreground">
Analytics from Better-T-Stack CLI usage data
</span>
</div>
<div className="mt-2 flex items-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className=" text-muted-foreground">
<span className="text-muted-foreground">
Uses PostHog - no personal info tracked, runs on each project
creation
</span>
</div>
<div className="mt-2 flex items-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className=" text-muted-foreground">
<span className="text-muted-foreground">
Source:{" "}
<Link
href="https://github.com/AmanVarshney01/create-better-t-stack/blob/main/apps/cli/src/utils/analytics.ts"
@@ -68,7 +68,7 @@ export function AnalyticsHeader({
</div>
<div className="mt-2 flex items-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className=" text-muted-foreground">
<span className="text-muted-foreground">
Last updated:{" "}
{loadingLastUpdated
? "CHECKING..."
@@ -93,17 +93,17 @@ export function AnalyticsHeader({
className="h-4 w-4 invert-0 dark:invert"
/>
<div>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
DISCORD_NOTIFICATIONS.IRC
</span>
<p className=" text-muted-foreground text-xs">
<p className="text-muted-foreground text-xs">
Join for LIVE project creation alerts
</p>
</div>
</div>
<div className="flex items-center gap-1 rounded border border-border bg-primary/10 px-2 py-1">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-primary text-xs">JOIN</span>
<span className="font-semibold text-primary text-xs">JOIN</span>
</div>
</div>
</Link>

View File

@@ -54,7 +54,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
GIT_INITIALIZATION.PIE
</span>
</div>
@@ -100,9 +100,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
PACKAGE_MANAGER.BAR
</span>
<span className="font-semibold text-sm">PACKAGE_MANAGER.BAR</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Package manager usage distribution
@@ -150,7 +148,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
INSTALL_PREFERENCE.PIE
</span>
</div>
@@ -196,7 +194,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">NODE_VERSIONS.BAR</span>
<span className="font-semibold text-sm">NODE_VERSIONS.BAR</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Node.js version distribution (major versions)
@@ -214,7 +212,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
@@ -229,7 +227,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">CLI_VERSIONS.BAR</span>
<span className="font-semibold text-sm">CLI_VERSIONS.BAR</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
CLI version distribution across project creations
@@ -247,7 +245,7 @@ export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />

View File

@@ -32,7 +32,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOTAL_PROJECTS</span>
<span className="font-semibold text-sm">TOTAL_PROJECTS</span>
<Terminal className="h-4 w-4 text-primary" />
</div>
</div>
@@ -49,7 +49,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOP_FRONTEND</span>
<span className="font-semibold text-sm">TOP_FRONTEND</span>
<Cpu className="h-4 w-4 text-primary" />
</div>
</div>
@@ -66,7 +66,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOP_BACKEND</span>
<span className="font-semibold text-sm">TOP_BACKEND</span>
<Terminal className="h-4 w-4 text-primary" />
</div>
</div>
@@ -83,7 +83,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOP_ORM</span>
<span className="font-semibold text-sm">TOP_ORM</span>
<Download className="h-4 w-4 text-primary" />
</div>
</div>
@@ -100,7 +100,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOP_API</span>
<span className="font-semibold text-sm">TOP_API</span>
<TrendingUp className="h-4 w-4 text-primary" />
</div>
</div>
@@ -117,7 +117,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">AUTH_ADOPTION</span>
<span className="font-semibold text-sm">AUTH_ADOPTION</span>
<Users className="h-4 w-4 text-primary" />
</div>
</div>
@@ -134,7 +134,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">TOP_PKG_MGR</span>
<span className="font-semibold text-sm">TOP_PKG_MGR</span>
<Terminal className="h-4 w-4 text-primary" />
</div>
</div>
@@ -151,7 +151,7 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className=" font-semibold text-sm">AVG_DAILY</span>
<span className="font-semibold text-sm">AVG_DAILY</span>
<TrendingUp className="h-4 w-4 text-primary" />
</div>
</div>

View File

@@ -78,7 +78,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
POPULAR_STACK_COMBINATIONS.BAR
</span>
</div>
@@ -95,7 +95,7 @@ export function StackConfigurationCharts({
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
@@ -109,7 +109,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
FRONTEND_FRAMEWORKS.BAR
</span>
</div>
@@ -126,7 +126,7 @@ export function StackConfigurationCharts({
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
@@ -168,7 +168,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
BACKEND_FRAMEWORKS.BAR
</span>
</div>
@@ -219,7 +219,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
DATABASE_DISTRIBUTION.BAR
</span>
</div>
@@ -269,7 +269,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
ORM_DISTRIBUTION.BAR
</span>
</div>
@@ -314,7 +314,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
DATABASE_HOSTING.BAR
</span>
</div>
@@ -363,7 +363,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">API_LAYER.PIE</span>
<span className="font-semibold text-sm">API_LAYER.PIE</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
API layer technology distribution
@@ -406,7 +406,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">AUTH_ADOPTION.PIE</span>
<span className="font-semibold text-sm">AUTH_ADOPTION.PIE</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Authentication implementation rate
@@ -450,7 +450,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
RUNTIME_DISTRIBUTION.PIE
</span>
</div>
@@ -500,7 +500,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">PROJECT_TYPES.PIE</span>
<span className="font-semibold text-sm">PROJECT_TYPES.PIE</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Full-stack vs Frontend-only vs Backend-only projects
@@ -552,7 +552,7 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
DATABASE_ORM_COMBINATIONS.BAR
</span>
</div>
@@ -569,7 +569,7 @@ export function StackConfigurationCharts({
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />

View File

@@ -53,7 +53,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
PROJECT_TIMELINE.CHART
</span>
</div>
@@ -95,7 +95,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
MONTHLY_TRENDS.CHART
</span>
</div>
@@ -115,7 +115,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
@@ -129,7 +129,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
PLATFORM_DISTRIBUTION.PIE
</span>
</div>
@@ -185,7 +185,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className=" font-semibold text-sm">
<span className="font-semibold text-sm">
HOURLY_DISTRIBUTION.BAR
</span>
</div>
@@ -205,7 +205,7 @@ export function TimelineCharts({ data }: TimelineChartsProps) {
tickLine={false}
tickMargin={10}
axisLine={false}
className=" text-xs"
className="text-xs"
/>
<YAxis hide />
<ChartTooltip

View File

@@ -30,7 +30,7 @@ export default function ShowcaseItem({
<div className="border-border border-b px-3 py-2">
<div className="flex items-center gap-2">
<File className="h-3 w-3 text-primary" />
<span className=" font-semibold text-foreground text-xs">
<span className="font-semibold text-foreground text-xs">
{projectId}.PROJECT
</span>
<div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
@@ -60,9 +60,7 @@ export default function ShowcaseItem({
<div className="mb-4">
<div className="mb-2 flex items-center gap-2">
<span className=" text-muted-foreground text-xs">
DEPENDENCIES:
</span>
<span className="text-muted-foreground text-xs">DEPENDENCIES:</span>
</div>
<div className="flex flex-wrap gap-1">
{tags.map((tag) => (
@@ -107,12 +105,12 @@ export default function ShowcaseItem({
<div className="border-border border-t pt-2">
<div className="flex items-center gap-2 text-xs">
<span className="text-primary">$</span>
<span className=" text-muted-foreground">
<span className="text-muted-foreground">
echo &quot;Status: READY&quot;
</span>
<div className="ml-auto flex items-center gap-1">
<div className="h-1 w-1 animate-pulse rounded-full bg-green-400" />
<span className=" text-green-400 text-xs">ONLINE</span>
<span className="text-green-400 text-xs">ONLINE</span>
</div>
</div>
</div>

View File

@@ -1,106 +1,86 @@
"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";
const showcaseProjects = [
{
title: "DocSurf",
description:
"AI-powered writing platform with smart text suggestions, real-time autocomplete, and document management",
imageUrl: "https://docsurf.ai/opengraph.jpg",
liveUrl: "https://docsurf.ai/?ref=better-t-etter-t-stack",
tags: [
"TanStack Start",
"Convex",
"Better Auth",
"Biome",
"Husky",
"Turborepo",
"pnpm",
],
},
{
title: "Look Crafted",
description: "✨ Transform Your Selfies into Stunning Headshots with AI",
imageUrl: "https://www.lookcrafted.com/opengraph-image.png",
liveUrl: "http://lookcrafted.com",
tags: [
"oRPC",
"Next.js",
"Hono",
"Bun",
"Neon",
"Drizzle",
"Better Auth",
"Biome",
"Husky",
"Turborepo",
],
},
{
title: "Screenshothis",
description: "Your All-in-One Screenshot Solution",
imageUrl:
"https://api.screenshothis.com/v1/screenshots/take?api_key=ss_live_NQJgRXqHcKPwnoMTuQmgiwLIGbVfihjpMyQhgsaMyNBHTyesvrxpYNXmdgcnxipc&url=https%3A%2F%2Fscreenshothis.com%2F&width=1200&height=630&device_scale_factor=0.75&block_ads=true&block_cookie_banners=true&block_trackers=true&prefers_color_scheme=light&prefers_reduced_motion=reduce&is_cached=true&cache_key=cfb06bf3616b1d03bdf455628a3830120e2080dd",
liveUrl:
"https://screenshothis.com?utm_source=better-t-stack&utm_medium=showcase&utm_campaign=referer",
tags: [
"oRPC",
"TanStack Start",
"Hono",
"pnpm",
"PostgreSQL",
"Drizzle",
"Better Auth",
"Biome",
"Husky",
"Turborepo",
],
},
{
title: "gl1.chat",
description:
"An ai platform focused on speed, reliability and advanced workflows powered by trpc, drizzle, vite, elysia, tanstack router",
imageUrl: "https://gl1.chat/social-share-image.png",
liveUrl: "https://gl1.chat/?ref=better-t-stack",
tags: ["tRPC", "Drizzle", "Elysia", "Vite", "TanStack Router"],
},
{
title: "Transmogged",
description:
"Turn your video game characters into different styles worth showing off. Create profile pictures that impress you and your friends.",
imageUrl: "https://images.transmogged.com/transmogged-home.png",
liveUrl: "https://transmogged.com",
tags: [
"TanStack Router",
"Better Auth",
"Biome",
"bun",
"PostgreSQL",
"Drizzle",
"tRPC",
"Hono",
],
},
{
title: "Formcn",
description:
"Easily build single- and multi-step forms with auto-generated client- and server-side code.",
imageUrl: "https://formcn.dev/opengraph-image.jpg",
liveUrl: "https://formcn.dev",
tags: [
"Next.js",
"React 19",
"shadcn components",
"React-hook-form",
"Typescript",
],
},
];
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">
@@ -113,23 +93,41 @@ export default function ShowcasePage() {
</span>
</div>
<div className="h-px flex-1 bg-border" />
<span className=" text-muted-foreground text-xs">
<span className="text-muted-foreground text-xs">
[{showcaseProjects.length} PROJECTS FOUND]
</span>
</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.title} {...project} index={index} />
))}
</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">
<span className="text-muted-foreground">
Want to showcase your project? Submit via GitHub issues
</span>
</div>

View File

@@ -76,7 +76,7 @@ export const baseOptions: BaseLayoutProps = {
title: (
<>
{logo}
<span className="font-medium font-mono text-md tracking-tighter ">
<span className="font-medium font-mono text-md tracking-tighter">
Better T Stack
</span>
</>

View File

@@ -7,7 +7,6 @@ import { Toaster } from "@/components/ui/sonner";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL || "");
export default function Providers({ children }: { children: React.ReactNode }) {
console.log("CONVEX_URL", process.env.NEXT_PUBLIC_CONVEX_URL);
return (
<>
<ConvexProvider client={convex}>

View File

@@ -346,16 +346,49 @@ export const TECH_OPTIONS: Record<
],
webDeploy: [
{
id: "workers",
name: "Cloudflare Workers",
description: "Deploy to Cloudflare Workers",
id: "wrangler",
name: "Wrangler",
description: "Deploy to Cloudflare Workers using Wrangler",
icon: `${ICON_BASE_URL}/workers.svg`,
color: "from-orange-400 to-orange-600",
},
{
id: "alchemy",
name: "Alchemy",
description: "Deploy to Cloudflare Workers using Alchemy",
icon: `${ICON_BASE_URL}/alchemy.png`,
color: "from-purple-400 to-purple-600",
className: "scale-150"
},
{
id: "none",
name: "No Deployment",
description: "Skip deployment configuration",
name: "None",
description: "Skip deployment setup",
icon: "",
color: "from-gray-400 to-gray-600",
default: true,
},
],
serverDeploy: [
{
id: "wrangler",
name: "Wrangler",
description: "Deploy to Cloudflare Workers using Wrangler",
icon: `${ICON_BASE_URL}/workers.svg`,
color: "from-orange-400 to-orange-600",
},
{
id: "alchemy",
name: "Alchemy",
description: "Deploy to Cloudflare Workers using Alchemy",
icon: `${ICON_BASE_URL}/alchemy.png`,
color: "from-purple-400 to-purple-600",
className: "scale-150"
},
{
id: "none",
name: "None",
description: "Skip deployment setup",
icon: "",
color: "from-gray-400 to-gray-600",
default: true,
@@ -670,6 +703,7 @@ export type StackState = {
install: string;
api: string;
webDeploy: string;
serverDeploy: string;
};
export const DEFAULT_STACK: StackState = {
@@ -689,6 +723,7 @@ export const DEFAULT_STACK: StackState = {
install: "true",
api: "trpc",
webDeploy: "none",
serverDeploy: "none",
};
export const isStackDefault = <K extends keyof StackState>(

View File

@@ -54,6 +54,9 @@ export const stackParsers = {
webDeploy: parseAsStringEnum<StackState["webDeploy"]>(
getValidIds("webDeploy"),
).withDefault(DEFAULT_STACK.webDeploy),
serverDeploy: parseAsStringEnum<StackState["serverDeploy"]>(
getValidIds("serverDeploy"),
).withDefault(DEFAULT_STACK.serverDeploy),
};
export const stackUrlKeys: UrlKeys<typeof stackParsers> = {
@@ -73,6 +76,7 @@ export const stackUrlKeys: UrlKeys<typeof stackParsers> = {
git: "git",
install: "i",
webDeploy: "wd",
serverDeploy: "sd",
};
export const stackQueryStatesOptions = {

View File

@@ -8,6 +8,7 @@ export type TechCategory =
| "orm"
| "dbSetup"
| "webDeploy"
| "serverDeploy"
| "auth"
| "packageManager"
| "addons"