feat(web): llms.txt, convex live stats, switch to vercel and improved ui (#460)

This commit is contained in:
Aman Varshney
2025-08-03 02:42:15 +05:30
committed by GitHub
parent 004cc01a0c
commit fef7f6b5e2
38 changed files with 2352 additions and 2676 deletions

View File

@@ -1202,11 +1202,12 @@ const StackBuilder = () => {
<TechIcon
icon={tech.icon}
name={tech.name}
className={
className={cn(
tech.icon.startsWith("/icon/")
? "h-3 w-3"
: "h-3 w-3 text-xs"
}
: "h-3 w-3 text-xs",
tech.className,
)}
/>
)}
{tech.name}

View File

@@ -161,7 +161,7 @@ export default function Testimonials() {
}}
>
<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="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.id}]</span>
@@ -211,7 +211,7 @@ export default function Testimonials() {
<div className="w-full min-w-0 overflow-hidden">
<div style={{ width: "100%", minWidth: 0, maxWidth: "100%" }}>
<Suspense fallback={<TweetSkeleton />}>
<Tweet id={tweetId} />
<Tweet id={tweetId} components={components} />
</Suspense>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,31 @@
"use client";
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useNpmDownloadCounter } from "@erquhart/convex-oss-stats/react";
import NumberFlow, { continuous } from "@number-flow/react";
import { useQuery } from "convex/react";
import {
BarChart3,
Check,
ChevronDown,
ChevronRight,
Copy,
Github,
Package,
Star,
Terminal,
TrendingUp,
Users,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import discordLogo from "@/public/icon/discord.svg";
import analyticsData from "@/public/analytics-minimal.json";
import Footer from "./_components/footer";
import PackageIcon from "./_components/icons";
import NpmPackage from "./_components/npm-package";
@@ -19,8 +33,6 @@ import SponsorsSection from "./_components/sponsors-section";
import Testimonials from "./_components/testimonials";
export default function HomePage() {
const [stars, setStars] = useState<number | null>(null);
const [isLoadingStars, setIsLoadingStars] = useState(true);
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun");
@@ -30,36 +42,24 @@ export default function HomePage() {
bun: "bun create better-t-stack@latest",
};
useEffect(() => {
async function fetchStars() {
try {
const response = await fetch(
"https://api.github.com/repos/amanvarshney01/create-better-t-stack",
);
if (response.ok) {
const data = await response.json();
setStars(data.stargazers_count);
} else {
console.error("Failed to fetch GitHub stars");
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
} finally {
setIsLoadingStars(false);
}
}
fetchStars();
}, []);
const copyCommand = (command: string, packageManager: string) => {
navigator.clipboard.writeText(command);
setCopiedCommand(packageManager);
setTimeout(() => setCopiedCommand(null), 2000);
};
const githubRepo = useQuery(api.stats.getGithubRepo, {
name: "AmanVarshney01/create-better-t-stack",
});
const npmPackages = useQuery(api.stats.getNpmPackages, {
names: ["create-better-t-stack"],
});
const liveNpmDownloadCount = useNpmDownloadCounter(npmPackages);
return (
<div className="mx-auto min-h-svh max-w-[1280px]">
<main className="mx-auto px-4 pt-16">
<main className="mx-auto px-4 pt-12">
<div className="mb-8 flex items-center justify-center">
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4 md:gap-6">
<pre className="ascii-art text-primary text-xs leading-tight sm:text-sm">
@@ -114,115 +114,281 @@ export default function HomePage() {
<NpmPackage />
</div>
<div className=" mb-8 rounded border border-border p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm">QUICK_START</span>
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
<div className="flex h-full flex-col justify-between rounded border border-border p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm">CLI_COMMAND</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center gap-2 rounded border border-border px-3 py-1.5 text-xs transition-colors hover:bg-muted/10"
>
<PackageIcon pm={selectedPM} className="h-3 w-3" />
<span>{selectedPM.toUpperCase()}</span>
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{(["bun", "pnpm", "npm"] as const).map((pm) => (
<DropdownMenuItem
key={pm}
onClick={() => setSelectedPM(pm)}
className={cn(
"flex items-center gap-2",
selectedPM === pm && "bg-accent text-background",
)}
>
<PackageIcon pm={pm} className="h-3 w-3" />
<span>{pm.toUpperCase()}</span>
{selectedPM === pm && (
<Check className="ml-auto h-3 w-3 text-background" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center rounded border border-border p-0.5">
{(["bun", "pnpm", "npm"] as const).map((pm) => (
<div className="space-y-3">
<div className="flex items-center justify-between rounded border border-border p-3">
<div className="flex items-center gap-2 font-mono text-sm">
<span className="text-primary">$</span>
<span className="text-foreground">
{commands[selectedPM]}
</span>
</div>
<button
type="button"
key={pm}
onClick={() => setSelectedPM(pm)}
className={cn(
"flex items-center gap-1.5 rounded px-2 py-1 text-xs transition-colors duration-150",
selectedPM === pm
? "bg-primary/20 text-primary"
: "text-muted-foreground hover:text-foreground",
)}
onClick={() => copyCommand(commands[selectedPM], selectedPM)}
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs hover:bg-muted/10"
>
<PackageIcon pm={pm} className="h-3 w-3" />
{pm.toUpperCase()}
{copiedCommand === selectedPM ? (
<Check className="h-3 w-3 text-primary" />
) : (
<Copy className="h-3 w-3" />
)}
{copiedCommand === selectedPM ? "COPIED!" : "COPY"}
</button>
))}
</div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between rounded border border-border p-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className=" text-foreground">{commands[selectedPM]}</span>
<Link href="/new">
<div className="group flex h-full cursor-pointer flex-col justify-between rounded border border-border p-4 transition-colors hover:bg-muted/10">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<ChevronRight className="h-4 w-4 text-primary transition-transform group-hover:translate-x-1" />
<span className="font-semibold text-sm">STACK_BUILDER</span>
</div>
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
INTERACTIVE
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between rounded border border-border p-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-primary"></span>
<span className="text-foreground">
Interactive configuration wizard
</span>
</div>
<div className="rounded border border-border bg-muted/30 px-2 py-1 text-xs">
START
</div>
</div>
</div>
<button
type="button"
onClick={() => copyCommand(commands[selectedPM], selectedPM)}
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs hover:bg-muted/50"
>
{copiedCommand === selectedPM ? (
<Check className="h-3 w-3 text-primary" />
) : (
<Copy className="h-3 w-3" />
)}
{copiedCommand === selectedPM ? "COPIED!" : "COPY"}
</button>
</div>
</div>
</Link>
</div>
<div className="mb-8 grid grid-cols-1 gap-4 sm:mb-12 sm:grid-cols-2 lg:grid-cols-3">
<Link href="/new">
<div className="group cursor-pointer rounded border border-border p-4 transition-colors focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2 hover:bg-muted/50">
<div className="flex items-center gap-2">
<ChevronRight className="h-4 w-4 text-primary transition-transform group-hover:translate-x-1" />
<Link href="/analytics">
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
<div className="mb-3 flex items-center gap-2">
<Terminal className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm sm:text-base">
STACK_BUILDER.SH
CLI_ANALYTICS.JSON
</span>
</div>
<p className="mt-2 text-muted-foreground text-xs sm:text-sm">
[EXEC] Interactive configuration wizard
</p>
</div>
</Link>
<Link
href="https://github.com/amanvarshney01/create-better-t-stack"
target="_blank"
rel="noopener noreferrer"
>
<div className="group cursor-pointer rounded border border-border p-4 transition-colors focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2 hover:bg-muted/50">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Github className="h-4 w-4 flex-shrink-0 text-primary" />
<span className="truncate font-semibold text-sm sm:text-base">
GITHUB_REPO.GIT
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<BarChart3 className="h-3 w-3" />
Total Projects
</span>
<NumberFlow
value={analyticsData.totalProjects}
className="font-bold font-mono text-lg text-primary tabular-nums"
transformTiming={{
duration: 1000,
easing: "ease-out",
}}
trend={1}
willChange
isolate
/>
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<TrendingUp className="h-3 w-3" />
Avg/Day
</span>
<span className="font-mono text-foreground text-sm">
{analyticsData.avgProjectsPerDay}
</span>
</div>
{stars !== null && !isLoadingStars && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border border-border bg-muted/30 px-2 py-1 text-xs">
<Star className="h-3 w-3 text-accent" />
<span className="tabular-nums">{stars}</span>
<div className="border-border/50 border-t pt-3">
<div className="flex items-center justify-between text-xs">
<span className="font-mono text-muted-foreground">
Last Updated
</span>
<span className="font-mono text-accent">
{analyticsData.lastUpdated ||
new Date().toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</span>
</div>
)}
</div>
</div>
<p className="mt-2 text-muted-foreground text-xs sm:text-sm">
[LINK] Star the repository on GitHub
</p>
</div>
</Link>
<Link
href="https://discord.gg/ZYsbjpDaM5"
href="https://github.com/AmanVarshney01/create-better-t-stack"
target="_blank"
rel="noopener noreferrer"
className="sm:col-span-2 lg:col-span-1"
>
<div className="group cursor-pointer rounded border border-border p-4 transition-colors focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2 hover:bg-muted/50">
<div className="flex items-center gap-2">
<Image
src={discordLogo}
alt="discord"
className="h-4 w-4 flex-shrink-0 invert-0 dark:invert"
/>
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
<div className="mb-3 flex items-center gap-2">
<Github className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm sm:text-base">
DISCORD_CHAT.IRC
GITHUB_REPO.GIT
</span>
</div>
<p className="mt-2 text-muted-foreground text-xs sm:text-sm">
[JOIN] Connect to developer community
</p>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<Star className="h-3 w-3" />
Stars
</span>
{githubRepo?.starCount !== undefined ? (
<NumberFlow
value={githubRepo.starCount}
className="font-bold font-mono text-lg text-primary tabular-nums"
transformTiming={{
duration: 800,
easing: "ease-out",
}}
trend={1}
willChange
isolate
/>
) : (
<div className="h-6 w-16 animate-pulse rounded bg-muted/50 font-bold font-mono text-lg text-primary tabular-nums" />
)}
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<Users className="h-3 w-3" />
Contributors
</span>
<span className="font-mono text-foreground text-sm">
{githubRepo?.contributorCount || "—"}
</span>
</div>
<div className="border-border/50 border-t pt-3">
<div className="flex items-center justify-between text-xs">
<span className="font-mono text-muted-foreground">
Repository
</span>
<span className="font-mono text-accent">
AmanVarshney01/create-better-t-stack
</span>
</div>
</div>
</div>
</div>
</Link>
<Link
href="https://www.npmjs.com/package/create-better-t-stack"
target="_blank"
rel="noopener noreferrer"
>
<div className="cursor-pointer rounded border border-border p-4 transition-colors hover:bg-muted/10">
<div className="mb-3 flex items-center gap-2">
<Terminal className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm sm:text-base">
NPM_PACKAGE.JS
</span>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<Package className="h-3 w-3" />
Downloads
</span>
{liveNpmDownloadCount?.count !== undefined ? (
<NumberFlow
value={liveNpmDownloadCount.count}
className="font-bold font-mono text-lg text-primary tabular-nums"
transformTiming={{
duration: liveNpmDownloadCount.intervalMs || 1000,
easing: "linear",
}}
trend={1}
willChange
plugins={[continuous]}
isolate
/>
) : (
<div className="h-6 w-20 animate-pulse rounded bg-muted/50 font-bold font-mono text-lg text-primary tabular-nums" />
)}
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1 font-mono text-muted-foreground text-xs uppercase tracking-wide">
<TrendingUp className="h-3 w-3" />
Avg/Day
</span>
<span className="font-mono text-foreground text-sm">
{npmPackages?.dayOfWeekAverages
? Math.round(
npmPackages.dayOfWeekAverages.reduce(
(a, b) => a + b,
0,
) / 7,
)
: "—"}
</span>
</div>
<div className="border-border/50 border-t pt-3">
<div className="flex items-center justify-between text-xs">
<span className="font-mono text-muted-foreground">
Package
</span>
<span className="font-mono text-accent">
create-better-t-stack
</span>
</div>
</div>
</div>
</div>
</Link>
</div>

View File

@@ -0,0 +1,25 @@
import { generateOGImage } from "fumadocs-ui/og";
import { notFound } from "next/navigation";
import { source } from "@/lib/source";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return generateOGImage({
title: page.data.title,
description: page.data.description,
site: "Better T Stack",
});
}
export function generateStaticParams() {
return source.generateParams().map((page) => ({
...page,
slug: [...page.slug, "image.png"],
}));
}

View File

@@ -7,6 +7,7 @@ import {
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
import { source } from "@/lib/source";
export default async function Page(props: {
@@ -20,6 +21,13 @@ export default async function Page(props: {
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<div className="flex flex-row items-center gap-2 border-b pt-2 pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/amanvarshney01/create-better-t-stack/blob/dev/apps/docs/content/docs/${page.path}`}
/>
</div>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
@@ -33,15 +41,24 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: {
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
const { slug = [] } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const image = ["/docs-og", ...slug, "image.png"].join("/");
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: image,
},
twitter: {
card: "summary_large_image",
images: image,
},
};
}

View File

@@ -96,6 +96,8 @@
}
@theme inline {
--font-sans: var(--font-geist);
--font-mono: var(--font-geist-mono);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
@@ -133,9 +135,6 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: Montserrat, sans-serif;
--font-mono: Fira Code, monospace;
--font-serif: Georgia, serif;
--radius: 0.35rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
@@ -198,9 +197,6 @@
--sidebar-ring: #8839ef;
--destructive-foreground: oklch(1 0 0);
--radius: 0.35rem;
--font-sans: Montserrat, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Fira Code, monospace;
--shadow-color: hsl(240 30% 25%);
--shadow-opacity: 0.12;
--shadow-blur: 6px;
@@ -266,9 +262,6 @@
--sidebar-ring: #cba6f7;
--destructive-foreground: #11111b;
--radius: 0.35rem;
--font-sans: Montserrat, sans-serif;
--font-serif: Georgia, serif;
--font-mono: Fira Code, monospace;
--shadow-color: hsl(240 30% 5%);
--shadow-opacity: 0.25;
--shadow-blur: 8px;
@@ -351,12 +344,6 @@
transform: translateY(-1px);
}
/* .terminal-matrix-bg {
background: linear-gradient(90deg, transparent 98%, var(--border) 100%),
linear-gradient(0deg, transparent 98%, var(--border) 100%);
background-size: 20px 20px;
} */
.ascii-art {
font-family: "Courier New", monospace;
line-height: 1;
@@ -407,3 +394,139 @@
.file-load-animation {
animation: file-load 0.3s ease-out forwards;
}
/* Chart styling to match terminal aesthetic */
.recharts-wrapper {
font-family: var(--font-mono);
}
.recharts-cartesian-axis-tick-value {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
}
.recharts-cartesian-axis-line {
stroke: hsl(var(--border));
stroke-width: 1;
}
.recharts-cartesian-grid-horizontal line,
.recharts-cartesian-grid-vertical line {
stroke: hsl(var(--border));
stroke-width: 1;
stroke-dasharray: 3 3;
opacity: 0.5;
}
.recharts-tooltip-wrapper {
background: hsl(var(--popover));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
}
.recharts-tooltip-item {
color: hsl(var(--foreground));
}
.recharts-legend-item-text {
font-family: var(--font-mono);
font-size: 0.75rem;
color: hsl(var(--foreground));
}
.recharts-pie-label {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.recharts-bar-rectangle {
transition: opacity 0.2s ease;
}
.recharts-bar-rectangle:hover {
opacity: 0.8;
}
.recharts-pie-sector {
transition: opacity 0.2s ease;
}
.recharts-pie-sector:hover {
opacity: 0.8;
}
.recharts-area-curve {
transition: opacity 0.2s ease;
}
.recharts-area-curve:hover {
opacity: 0.8;
}
/* Chart container styling */
.chart-container {
border-radius: var(--radius);
overflow: hidden;
}
/* Terminal-style chart headers */
.chart-header {
border-bottom: 1px solid hsl(var(--border));
padding: 0.75rem 1rem;
background: hsl(var(--card));
}
.chart-header-title {
font-family: var(--font-mono);
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.chart-header-description {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-top: 0.25rem;
}
/* Chart hover effects */
.chart-card {
transition: all 0.2s ease;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
overflow: hidden;
}
.chart-card:hover {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.1);
}
/* Chart loading state */
.chart-loading {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
font-size: 0.875rem;
}
/* Chart error state */
.chart-error {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
color: hsl(var(--destructive));
font-family: var(--font-mono);
font-size: 0.875rem;
}

View File

@@ -76,7 +76,7 @@ export const baseOptions: BaseLayoutProps = {
title: (
<>
{logo}
<span className="font-medium [.uwu_&]:hidden [header_&]:text-[15px]">
<span className="font-medium font-mono text-md tracking-tighter ">
Better T Stack
</span>
</>

View File

@@ -2,19 +2,27 @@ export const dynamic = "force-static";
import { RootProvider } from "fumadocs-ui/provider";
import type { Metadata, Viewport } from "next";
import { Poppins } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Geist, Geist_Mono } from "next/font/google";
import type { ReactNode } from "react";
import { Toaster } from "@/components/ui/sonner";
import SearchDialog from "@/components/search";
import { cn } from "@/lib/utils";
import "./global.css";
import Providers from "@/components/providers";
const poppins = Poppins({
const geist = Geist({
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
weight: ["400", "500", "600", "700"],
variable: "--font-geist",
});
const geistMono = Geist_Mono({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-geist-mono",
});
const ogImage =
"https://api.screenshothis.com/v1/screenshots/take?api_key=ss_live_NQJgRXqHcKPwnoMTuQmgiwLIGbVfihjpMyQhgsaMyNBHTyesvrxpYNXmdgcnxipc&url=https%3A%2F%2Fbetter-t-stack.dev%2F&width=1200&height=630&block_ads=true&block_cookie_banners=true&block_trackers=true&device_scale_factor=0.75&prefers_color_scheme=dark&is_cached=true";
"https://api.screenshothis.com/v1/screenshots/take?api_key=ss_live_NQJgRXqHcKPwnoMTuQmgiwLIGbVfihjpMyQhgsaMyNBHTyesvrxpYNXmdgcnxipc&url=https%3A%2F%2Fbetter-t-stack.dev%2F&width=1200&height=630&block_ads=true&block_cookie_banners=true&block_trackers=true&device_scale_factor=0.70&prefers_color_scheme=dark&is_cached=true&cache_key=bts";
export const metadata: Metadata = {
title: "Better-T Stack",
@@ -99,10 +107,15 @@ export const viewport: Viewport = {
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={poppins.className} suppressHydrationWarning>
<html
lang="en"
className={cn(geist.variable, geistMono.variable, "font-sans")}
suppressHydrationWarning
>
<body>
<RootProvider
search={{
SearchDialog,
options: {
type: "static",
},
@@ -112,8 +125,7 @@ export default function Layout({ children }: { children: ReactNode }) {
defaultTheme: "system",
}}
>
<NuqsAdapter>{children}</NuqsAdapter>
<Toaster />
<Providers>{children}</Providers>
</RootProvider>
</body>
</html>

View File

@@ -0,0 +1,12 @@
import { getLLMText } from "@/lib/get-llm-text";
import { source } from "@/lib/source";
// cached forever
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join("\n\n"));
}

View File

@@ -0,0 +1,21 @@
import { notFound } from "next/navigation";
import { type NextRequest, NextResponse } from "next/server";
import { getLLMText } from "@/lib/get-llm-text";
import { source } from "@/lib/source";
export const revalidate = false;
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> },
) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
return new NextResponse(await getLLMText(page));
}
export function generateStaticParams() {
return source.generateParams();
}

View File

@@ -0,0 +1,184 @@
"use client";
import { cva } from "class-variance-authority";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "fumadocs-ui/components/ui/popover";
import { useCopyButton } from "fumadocs-ui/utils/use-copy-button";
import {
Check,
ChevronDown,
Copy,
ExternalLinkIcon,
MessageCircleIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { cn } from "../../../lib/cn";
const cache = new Map<string, string>();
export function LLMCopyButton({
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl,
}: {
markdownUrl: string;
}) {
const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached);
setLoading(true);
try {
await navigator.clipboard.write([
new ClipboardItem({
"text/plain": fetch(markdownUrl).then(async (res) => {
const content = await res.text();
cache.set(markdownUrl, content);
return content;
}),
}),
]);
} finally {
setLoading(false);
}
});
return (
<button
type="button"
disabled={isLoading}
className={cn(
buttonVariants({
color: "secondary",
size: "sm",
className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground",
}),
)}
onClick={onClick}
>
{checked ? <Check /> : <Copy />}
Copy Markdown
</button>
);
}
const optionVariants = cva(
"inline-flex items-center gap-2 rounded-lg p-2 text-sm hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-4",
);
export function ViewOptions({
markdownUrl,
githubUrl,
}: {
/**
* A URL to the raw Markdown/MDX content of page
*/
markdownUrl: string;
/**
* Source file URL on GitHub
*/
githubUrl: string;
}) {
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window !== "undefined"
? new URL(markdownUrl, window.location.origin)
: "loading";
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [
{
title: "Open in GitHub",
href: githubUrl,
icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
},
{
title: "Open in ChatGPT",
href: `https://chatgpt.com/?${new URLSearchParams({
hints: "search",
q,
})}`,
icon: (
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
},
{
title: "Open in Claude",
href: `https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic</title>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
),
},
{
title: "Open in T3 Chat",
href: `https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: <MessageCircleIcon />,
},
];
}, [githubUrl, markdownUrl]);
return (
<Popover>
<PopoverTrigger
className={cn(
buttonVariants({
color: "secondary",
size: "sm",
className: "gap-2",
}),
)}
>
Open
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
</PopoverTrigger>
<PopoverContent className="flex flex-col overflow-auto">
{items.map((item) => (
<a
key={item.href}
href={item.href}
rel="noreferrer noopener"
target="_blank"
className={cn(optionVariants())}
>
{item.icon}
{item.title}
<ExternalLinkIcon className="ms-auto size-3.5 text-fd-muted-foreground" />
</a>
))}
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,18 @@
"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Toaster } from "@/components/ui/sonner";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL || "");
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<>
<ConvexProvider client={convex}>
<NuqsAdapter>{children}</NuqsAdapter>
</ConvexProvider>
<Toaster />
</>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import { create } from "@orama/orama";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from "fumadocs-ui/components/dialog/search";
import { customSearchItems, filterCustomItems } from "@/lib/search-config";
function initOrama() {
return create({
schema: {
_: "string",
},
language: "english",
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { search, setSearch, query } = useDocsSearch({
type: "static",
initOrama,
});
const filteredCustomItems = filterCustomItems(
customSearchItems,
search || "",
);
const combinedResults =
query.data === "empty" || !query.data
? null
: [
...query.data,
...filteredCustomItems.map((item, index) => ({
id: `custom-${index}`,
title: item.title,
url: item.url,
content: item.content,
type: "page" as const,
})),
];
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={combinedResults} />
</SearchDialogContent>
</SearchDialog>
);
}

View File

@@ -53,7 +53,13 @@ export function SpecialSponsorBanner() {
<div className="">
<div className="flex flex-col gap-2">
{specialSponsors.map((sponsor) => (
<div key={sponsor.sponsor.login} className="flex items-center gap-3">
<a
key={sponsor.sponsor.login}
className="flex items-center gap-3"
href={sponsor.sponsor.websiteUrl}
target="_blank"
rel="noopener noreferrer"
>
<Image
src={sponsor.sponsor.customLogoUrl || sponsor.sponsor.avatarUrl}
alt={sponsor.sponsor.name || sponsor.sponsor.login}
@@ -67,7 +73,7 @@ export function SpecialSponsorBanner() {
{sponsor.sponsor.name || sponsor.sponsor.login}
</h4>
</div>
</div>
</a>
))}
</div>
</div>

View File

@@ -0,0 +1,257 @@
"use client";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 font-medium text-sm data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-muted-foreground text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -0,0 +1,26 @@
import type { InferPageType } from "fumadocs-core/source";
import { remarkInclude } from "fumadocs-mdx/config";
import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkMdx from "remark-mdx";
import type { source } from "@/lib/source";
const processor = remark()
.use(remarkMdx)
// needed for Fumadocs MDX
.use(remarkInclude)
.use(remarkGfm);
export async function getLLMText(page: InferPageType<typeof source>) {
const processed = await processor.process({
path: page.data._file.absolutePath,
value: page.data.content,
});
return `# ${page.data.title}
URL: ${page.url}
${page.data.description}
${processed.value}`;
}

View File

@@ -0,0 +1,66 @@
export interface CustomSearchItem {
title: string;
url: string;
content: string;
tags: string[];
}
export const customSearchItems: CustomSearchItem[] = [
{
title: "Analytics",
url: "/analytics",
content: "Analytics",
tags: ["analytics", "insights", "statistics", "data", "metrics"],
},
{
title: "Showcase",
url: "/showcase",
content: "Showcase",
tags: ["showcase", "projects", "examples", "demos", "portfolio"],
},
{
title: "Builder",
url: "/new",
content: "Builder",
tags: ["builder", "create", "new", "project", "setup"],
},
{
title: "GitHub Repository",
url: "https://github.com/AmanVarshney01/create-better-t-stack",
content: "GitHub",
tags: ["github", "source", "code", "repository", "contribute", "star"],
},
{
title: "NPM Package",
url: "https://www.npmjs.com/package/create-better-t-stack",
content: "NPM",
tags: ["npm", "package", "install", "cli", "tool"],
},
{
title: "X (Twitter)",
url: "https://x.com/amanvarshney01",
content: "X",
tags: ["twitter", "x", "social", "updates", "announcements", "follow"],
},
{
title: "Discord Community",
url: "https://discord.gg/ZYsbjpDaM5",
content: "Discord",
tags: ["discord", "community", "chat", "help", "support", "discussions"],
},
];
export function filterCustomItems(
items: CustomSearchItem[],
searchQuery: string,
): CustomSearchItem[] {
if (!searchQuery) return items;
const searchLower = searchQuery.toLowerCase();
return items.filter(
(item) =>
item.title.toLowerCase().includes(searchLower) ||
item.content.toLowerCase().includes(searchLower) ||
item.tags.some((tag) => tag.toLowerCase().includes(searchLower)),
);
}