mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
1
apps/web/.gitignore
vendored
1
apps/web/.gitignore
vendored
@@ -15,6 +15,7 @@ public/analytics-minimal.json
|
||||
*.tsbuildinfo
|
||||
/.open-next/
|
||||
/.wrangler/
|
||||
.alchemy
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "Status: READY"
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TechCategory =
|
||||
| "orm"
|
||||
| "dbSetup"
|
||||
| "webDeploy"
|
||||
| "serverDeploy"
|
||||
| "auth"
|
||||
| "packageManager"
|
||||
| "addons"
|
||||
|
||||
Reference in New Issue
Block a user