mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): llms.txt, convex live stats, switch to vercel and improved ui (#460)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
25
apps/web/src/app/docs-og/[...slug]/route.tsx
Normal file
25
apps/web/src/app/docs-og/[...slug]/route.tsx
Normal 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"],
|
||||
}));
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
apps/web/src/app/llms-full.txt/route.ts
Normal file
12
apps/web/src/app/llms-full.txt/route.ts
Normal 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"));
|
||||
}
|
||||
21
apps/web/src/app/llms.mdx/[[...slug]]/route.ts
Normal file
21
apps/web/src/app/llms.mdx/[[...slug]]/route.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user