Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Inter, Space_Grotesk } from 'next/font/google';
import type React from 'react';
import '@/app/globals.css';
import { LayoutShell } from '@/components/layout-shell';
import { catalogStatsFrom, getTemplatesData } from '@/lib/data';
import { jsonLdScript, organizationJsonLd, websiteJsonLd } from '@/lib/seo';

const siteUrl = 'https://create-awesome-python-app.vercel.app';
Expand Down Expand Up @@ -91,7 +92,10 @@ const spaceGrotesk = Space_Grotesk({
weight: ['400', '500', '600', '700'],
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const catalog = await getTemplatesData();
const stats = catalogStatsFrom(catalog);

return (
<html lang="en" className="dark">
<body className={`${inter.variable} ${spaceGrotesk.variable} font-sans`}>
Expand All @@ -105,7 +109,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}),
}}
/>
<LayoutShell>{children}</LayoutShell>
<LayoutShell stats={stats}>{children}</LayoutShell>
</body>
</html>
);
Expand Down
12 changes: 9 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import { TemplateCategories } from '@/components/template-categories';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { DISCORD_INVITE_URL } from '@/lib/community';
import { getTemplatesData } from '@/lib/data';
import { catalogStatsFrom, getTemplatesData } from '@/lib/data';

const PRIMARY_COMMAND = 'uvx create-awesome-python-app my-app';

export default async function Home() {
const { templates, categories } = await getTemplatesData();
const catalog = await getTemplatesData();
const { templates, categories } = catalog;
const stats = catalogStatsFrom(catalog);

const flagshipTemplate = templates.find((t) => t.slug === 'fastapi-starter');
const otherTemplates = templates.filter((t) => t.slug !== 'fastapi-starter');
Expand Down Expand Up @@ -72,7 +74,11 @@ export default async function Home() {
sideVisual={<AnimatedTerminal />}
/>

<StatsBar />
<StatsBar
templates={stats.templates}
extensions={stats.extensions}
categories={stats.categories}
/>

<SaasAiBanner />

Expand Down
25 changes: 20 additions & 5 deletions src/components/layout-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ import { PerformanceProvider } from '@/components/performance-provider';
import { SiteHeader } from '@/components/site-header';
import { ThemeProvider } from '@/components/theme-provider';

export function LayoutShell({ children }: { children: ReactNode }) {
type CatalogStats = {
templates: number;
extensions: number;
categories: number;
};

export function LayoutShell({
children,
stats = { templates: 5, extensions: 12, categories: 9 },
}: {
children: ReactNode;
stats?: CatalogStats;
}) {
const [open, setOpen] = useState(false);
return (
<PerformanceProvider>
Expand All @@ -26,19 +38,22 @@ export function LayoutShell({ children }: { children: ReactNode }) {
{children}
</main>
<footer className="w-full border-t mt-16">
{/* Stats row */}
{/* Stats row — counts from templates.json via getTemplatesData */}
<div className="border-b border-border/50">
<div className="container flex flex-wrap items-center justify-center gap-x-8 gap-y-2 py-4 text-xs text-muted-foreground">
<span>
<strong className="text-foreground font-display font-semibold">5</strong> templates
<strong className="text-foreground font-display font-semibold">{stats.templates}</strong>{' '}
templates
</span>
<span className="text-border">·</span>
<span>
<strong className="text-foreground font-display font-semibold">8</strong> extensions
<strong className="text-foreground font-display font-semibold">{stats.extensions}</strong>{' '}
extensions
</span>
<span className="text-border">·</span>
<span>
<strong className="text-foreground font-display font-semibold">9</strong> categories
<strong className="text-foreground font-display font-semibold">{stats.categories}</strong>{' '}
categories
</span>
<span className="text-border">·</span>
<span>MIT licensed</span>
Expand Down
21 changes: 18 additions & 3 deletions src/components/stats-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Stat {
suffix?: string;
}

const STATS: Stat[] = [
const DEFAULT_STATS: Stat[] = [
{ value: 5, label: 'Templates' },
{ value: 12, label: 'Extensions' },
{ value: 9, label: 'Categories' },
Expand Down Expand Up @@ -47,11 +47,26 @@ function Counter({ target, duration = 1200 }: { target: number; duration?: numbe
return <span ref={ref}>{count}</span>;
}

export function StatsBar() {
export function StatsBar({
templates,
extensions,
categories,
}: {
templates?: number;
extensions?: number;
categories?: number;
} = {}) {
const stats: Stat[] = [
{ value: templates ?? (DEFAULT_STATS[0].value as number), label: 'Templates' },
{ value: extensions ?? (DEFAULT_STATS[1].value as number), label: 'Extensions' },
{ value: categories ?? (DEFAULT_STATS[2].value as number), label: 'Categories' },
{ value: 'MIT', label: 'Licensed' },
];

return (
<div className="w-full border-y border-border/50 bg-muted/20">
<div className="container flex flex-wrap items-center justify-center gap-x-10 gap-y-4 py-6 md:gap-x-16">
{STATS.map((stat) => (
{stats.map((stat) => (
<div key={stat.label} className="flex flex-col items-center gap-0.5">
<span className="font-display text-2xl font-bold text-foreground md:text-3xl">
{typeof stat.value === 'number' ? <Counter target={stat.value} /> : stat.value}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import { type TemplatesData, templatesDataSchema } from './schemas';

const TEMPLATES_URL = 'https://raw.githubusercontent.com/Create-Python-App/cpa-templates/main/templates.json';

/** Catalog counts for footer / stats bar — single source of truth. */
export type CatalogStats = {
templates: number;
extensions: number;
categories: number;
};

export function catalogStatsFrom(data: TemplatesData): CatalogStats {
return {
templates: data.templates.length,
extensions: data.extensions.length,
categories: data.categories.length,
};
}

export async function getTemplatesData(): Promise<TemplatesData> {
try {
const response = await fetch(TEMPLATES_URL, { next: { revalidate: 3600 } }); // Revalidate every hour
Expand Down
Loading