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

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"