From 223798e5a3ceac4cbba569028fd385d11a9970af Mon Sep 17 00:00:00 2001 From: Ken Matsui <26405363+ken-matsui@users.noreply.github.com> Date: Sun, 3 May 2026 16:57:02 -0400 Subject: [PATCH] Switch to astro --- .github/dependabot.yml | 5 - .github/workflows/{next.yml => astro.yml} | 19 +- .github/workflows/vercel-preview.yml | 34 - .github/workflows/vercel.yml | 32 - .gitignore | 15 +- README.md | 79 +- app/_components/external-link-button.tsx | 71 - app/_components/feature-card.tsx | 56 - app/_components/footer.tsx | 89 - app/_components/gradient-icon.tsx | 52 - app/_components/header.tsx | 86 - app/_components/loading.tsx | 9 - app/_components/logo.tsx | 13 - app/_components/search.tsx | 64 - app/_components/section-header.tsx | 31 - app/_components/stat-item.tsx | 34 - app/_lib/constants.ts | 1 - app/_lib/hasuraClient.ts | 19 - app/globals.css | 28 - app/hero.ts | 2 - app/layout.tsx | 30 - app/loading.ts | 3 - app/not-found.tsx | 14 - .../[group]/[name]/[version]/loading.ts | 3 - .../[group]/[name]/[version]/loading.tsx | 5 - .../[group]/[name]/[version]/page.tsx | 44 - .../[name]/_components/package-details.tsx | 224 - app/packages/[group]/[name]/loading.ts | 3 - app/packages/[group]/[name]/loading.tsx | 5 - app/packages/[group]/[name]/page.tsx | 47 - app/page.tsx | 227 - app/providers.tsx | 14 - app/search/_components/pagination.tsx | 42 - app/search/loading.ts | 3 - app/search/loading.tsx | 5 - app/search/page.tsx | 199 - astro.config.ts | 13 + biome.json | 14 +- codegen.ts | 6 +- graphql/getAllPackages.gql | 16 + graphql/getPackageByNameAndVersion.gql | 21 - graphql/getPackagesByName.gql | 16 - graphql/searchPackages.gql | 15 - next-env.d.ts | 6 - next-sitemap.config.js | 7 - next.config.ts | 15 - package.json | 54 +- postcss.config.mjs | 5 - proxy.ts | 80 - public/_headers | 6 + public/header-search.js | 12 + public/manifest.json | 2 +- scripts/verify-no-inline-scripts.mjs | 46 + src/components/BrandIcon.astro | 27 + src/components/FeatureCard.astro | 36 + src/components/Footer.astro | 72 + src/components/GradientIcon.astro | 52 + src/components/Header.astro | 62 + src/components/Icon.astro | 58 + src/components/Logo.astro | 10 + src/components/package/InstallSnippet.astro | 22 + src/components/package/PackageCard.astro | 34 + .../package/PackageCardContent.astro | 73 + .../package/PackageDetailView.astro | 70 + src/components/package/PackageMetaGrid.astro | 142 + src/components/package/ReadmeRenderer.astro | 34 + src/components/ui/Badge.astro | 22 + src/components/ui/Button.astro | 55 + src/components/ui/Card.astro | 42 + src/components/ui/CodeBlock.astro | 15 + src/components/ui/ExternalLink.astro | 35 + src/components/ui/Pagination.astro | 62 + src/env.d.ts | 1 + src/layouts/BaseLayout.astro | 51 + src/lib/classes.ts | 39 + src/lib/constants.ts | 36 + src/lib/format.ts | 36 + src/lib/hasuraClient.ts | 16 + src/lib/markdown.ts | 29 + src/lib/packages.ts | 353 ++ src/lib/pagination.ts | 208 + src/lib/search.ts | 96 + src/lib/types.ts | 22 + src/pages/404.astro | 12 + src/pages/index.astro | 173 + src/pages/packages.json.ts | 14 + src/pages/packages/[group]/[name].astro | 27 + .../packages/[group]/[name]/[version].astro | 28 + src/pages/robots.txt.ts | 20 + src/pages/search.astro | 82 + src/scripts/search.ts | 170 + src/styles/global.css | 114 + tsconfig.json | 20 +- wrangler.jsonc | 9 + yarn.lock | 5521 +++++++++-------- 95 files changed, 5461 insertions(+), 4480 deletions(-) rename .github/workflows/{next.yml => astro.yml} (56%) delete mode 100644 .github/workflows/vercel-preview.yml delete mode 100644 .github/workflows/vercel.yml delete mode 100644 app/_components/external-link-button.tsx delete mode 100644 app/_components/feature-card.tsx delete mode 100644 app/_components/footer.tsx delete mode 100644 app/_components/gradient-icon.tsx delete mode 100644 app/_components/header.tsx delete mode 100644 app/_components/loading.tsx delete mode 100644 app/_components/logo.tsx delete mode 100644 app/_components/search.tsx delete mode 100644 app/_components/section-header.tsx delete mode 100644 app/_components/stat-item.tsx delete mode 100644 app/_lib/constants.ts delete mode 100644 app/_lib/hasuraClient.ts delete mode 100644 app/globals.css delete mode 100644 app/hero.ts delete mode 100644 app/layout.tsx delete mode 100644 app/loading.ts delete mode 100644 app/not-found.tsx delete mode 100644 app/packages/[group]/[name]/[version]/loading.ts delete mode 100644 app/packages/[group]/[name]/[version]/loading.tsx delete mode 100644 app/packages/[group]/[name]/[version]/page.tsx delete mode 100644 app/packages/[group]/[name]/_components/package-details.tsx delete mode 100644 app/packages/[group]/[name]/loading.ts delete mode 100644 app/packages/[group]/[name]/loading.tsx delete mode 100644 app/packages/[group]/[name]/page.tsx delete mode 100644 app/page.tsx delete mode 100644 app/providers.tsx delete mode 100644 app/search/_components/pagination.tsx delete mode 100644 app/search/loading.ts delete mode 100644 app/search/loading.tsx delete mode 100644 app/search/page.tsx create mode 100644 astro.config.ts create mode 100644 graphql/getAllPackages.gql delete mode 100644 graphql/getPackageByNameAndVersion.gql delete mode 100644 graphql/getPackagesByName.gql delete mode 100644 graphql/searchPackages.gql delete mode 100644 next-env.d.ts delete mode 100644 next-sitemap.config.js delete mode 100644 next.config.ts delete mode 100644 postcss.config.mjs delete mode 100644 proxy.ts create mode 100644 public/_headers create mode 100644 public/header-search.js create mode 100644 scripts/verify-no-inline-scripts.mjs create mode 100644 src/components/BrandIcon.astro create mode 100644 src/components/FeatureCard.astro create mode 100644 src/components/Footer.astro create mode 100644 src/components/GradientIcon.astro create mode 100644 src/components/Header.astro create mode 100644 src/components/Icon.astro create mode 100644 src/components/Logo.astro create mode 100644 src/components/package/InstallSnippet.astro create mode 100644 src/components/package/PackageCard.astro create mode 100644 src/components/package/PackageCardContent.astro create mode 100644 src/components/package/PackageDetailView.astro create mode 100644 src/components/package/PackageMetaGrid.astro create mode 100644 src/components/package/ReadmeRenderer.astro create mode 100644 src/components/ui/Badge.astro create mode 100644 src/components/ui/Button.astro create mode 100644 src/components/ui/Card.astro create mode 100644 src/components/ui/CodeBlock.astro create mode 100644 src/components/ui/ExternalLink.astro create mode 100644 src/components/ui/Pagination.astro create mode 100644 src/env.d.ts create mode 100644 src/layouts/BaseLayout.astro create mode 100644 src/lib/classes.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/hasuraClient.ts create mode 100644 src/lib/markdown.ts create mode 100644 src/lib/packages.ts create mode 100644 src/lib/pagination.ts create mode 100644 src/lib/search.ts create mode 100644 src/lib/types.ts create mode 100644 src/pages/404.astro create mode 100644 src/pages/index.astro create mode 100644 src/pages/packages.json.ts create mode 100644 src/pages/packages/[group]/[name].astro create mode 100644 src/pages/packages/[group]/[name]/[version].astro create mode 100644 src/pages/robots.txt.ts create mode 100644 src/pages/search.astro create mode 100644 src/scripts/search.ts create mode 100644 src/styles/global.css create mode 100644 wrangler.jsonc diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1fdaf121..4d051ba6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,8 +14,3 @@ updates: update-types: - "minor" - "patch" - fontawesome: - patterns: - - "@fortawesome/fontawesome-svg-core" - - "@fortawesome/free-brands-svg-icons" - - "@fortawesome/free-solid-svg-icons" diff --git a/.github/workflows/next.yml b/.github/workflows/astro.yml similarity index 56% rename from .github/workflows/next.yml rename to .github/workflows/astro.yml index a45a8475..4774b3ba 100644 --- a/.github/workflows/next.yml +++ b/.github/workflows/astro.yml @@ -1,4 +1,4 @@ -name: Next.js +name: Astro on: push: @@ -6,7 +6,7 @@ on: pull_request: jobs: - lint: + build: runs-on: ubuntu-latest steps: @@ -17,23 +17,10 @@ jobs: node-version-file: package.json - name: Install dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Lint run: yarn lint - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install dependencies - run: yarn install - - name: Build run: yarn build diff --git a/.github/workflows/vercel-preview.yml b/.github/workflows/vercel-preview.yml deleted file mode 100644 index db7aa974..00000000 --- a/.github/workflows/vercel-preview.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Vercel Preview - -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - -on: - push: - branches-ignore: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - if: github.actor != 'dependabot[bot]' - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - - - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml deleted file mode 100644 index cd9919e1..00000000 --- a/.github/workflows/vercel.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Vercel Production - -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version-file: package.json - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - - - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.gitignore b/.gitignore index 9146e012..d3fa3520 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +*.patch + # dependencies /node_modules /.pnp @@ -11,12 +13,12 @@ package-lock.json # testing /coverage -# next.js -/.next/ -/out/ +# astro +/.astro/ # production /build +/dist # misc .DS_Store @@ -31,13 +33,16 @@ yarn-error.log* # local env files .env*.local -# vercel +# deployment state .vercel +.wrangler + +# local Claude Code settings +.claude/ # typescript *.tsbuildinfo -public/robots.txt public/sitemap*.xml graphql.schema.json diff --git a/README.md b/README.md index 7a3fba92..426f54a6 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,85 @@ A package registry website for Cabin, a package manager and build system for C++. -### Contributing +### Architecture -#### Install Node.js dependencies +This site is a fully static Astro build. Package data is fetched from +`https://cabin.hasura.app/v1/graphql` at build time, package detail pages are +pre-rendered, and `/packages.json` is generated for client-side search. + +The output in `dist/` can be served by Cloudflare Pages, Cloudflare Workers +Static Assets, or any static file host. No Next.js, Vercel runtime, SSR adapter, +API routes, or server functions are required. + +### Development + +Install Node.js dependencies: + +```bash +yarn install +``` + +Start the local Astro dev server: ```bash -$ yarn install +yarn dev ``` -#### Start endpoint +`yarn dev` regenerates GraphQL types from Hasura before starting Astro, so a +fresh checkout works without a separate `yarn generate` step. Astro serves the +site at [`localhost:4321`](http://localhost:4321) by default. + +### Build and preview ```bash -$ yarn dev +yarn lint +yarn typecheck +yarn build +yarn preview ``` -Now you can visit [`localhost:3000`](http://localhost:3000) from your browser. +`yarn build` regenerates GraphQL types, runs Astro type checking, fetches package +data from Hasura, verifies that generated HTML has no inline scripts, and writes +the static site to `dist/`. + +Biome is used for TypeScript, JavaScript, CSS, and config files. Astro component +files are excluded from Biome because this setup relies on Astro's own parser and +type checker for `.astro`; run `yarn typecheck` directly, or rely on `yarn build`, +which runs `astro check` before building. + +### Cloudflare deployment + +`wrangler.jsonc` is configured for Workers Static Assets with `./dist` as the +asset directory. Build before deploying: + +```bash +yarn build +yarn wrangler deploy +``` + +No deploy workflow is included because Cloudflare account and project secrets +vary by environment. + +### Static search + +`/search` is a static page. In the browser it reads `q`, `page`, and `perPage` +from the URL, fetches `/packages.json`, filters package names with +case-insensitive substring matching, and renders pagination links by updating +the query string. The browser does not call Hasura. + +### Package detail routes + +Each package gets two statically generated detail routes: + +- `/packages//` renders the latest version. +- `/packages///` renders that exact version. + +Both are pre-rendered at build time from the same Hasura package data and share +their markup through `src/components/package/PackageDetailView.astro`. + +### Known limitation + +Package detail routes use `/packages//`, matching the previous site +and Cabin's current two-segment package naming. Packages with names that do not +fit exactly one slash are included in `/packages.json` but do not get a generated +detail page; the search UI renders them as non-clickable cards. diff --git a/app/_components/external-link-button.tsx b/app/_components/external-link-button.tsx deleted file mode 100644 index 5b2349b5..00000000 --- a/app/_components/external-link-button.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Button, Link } from "@heroui/react"; - -interface ExternalLinkButtonProps { - href: string; - icon: IconDefinition; - children: React.ReactNode; - variant?: - | "light" - | "flat" - | "bordered" - | "ghost" - | "shadow" - | "solid" - | "faded"; - color?: - | "default" - | "primary" - | "secondary" - | "success" - | "warning" - | "danger"; - size?: "sm" | "md" | "lg"; - className?: string; - isIconOnly?: boolean; - fullWidth?: boolean; - iconClassName?: string; - animate?: boolean; -} - -export function ExternalLinkButton({ - href, - icon, - children, - variant = "flat", - color = "default", - size = "sm", - className = "", - isIconOnly = false, - fullWidth = false, - iconClassName = "", - animate = false, -}: ExternalLinkButtonProps) { - const buttonClass = `${fullWidth ? "w-full justify-start" : ""} ${className}`; - const iconClasses = `text-sm ${animate ? "animate-pulse" : ""} ${iconClassName}`; - - return ( - - ); -} diff --git a/app/_components/feature-card.tsx b/app/_components/feature-card.tsx deleted file mode 100644 index 98d410b2..00000000 --- a/app/_components/feature-card.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { Card, CardBody, CardHeader } from "@heroui/react"; -import { GradientIcon } from "./gradient-icon"; - -interface FeatureCardProps { - icon: IconDefinition; - title: string; - description: string; - colorScheme?: "primary" | "blue" | "purple" | "green" | "orange" | "red"; - className?: string; -} - -const cardColorVariants = { - primary: - "bg-gradient-to-br from-primary/5 to-secondary/10 border-primary/20", - blue: "bg-gradient-to-br from-primary/5 to-primary/10 border-primary/20", - purple: "bg-gradient-to-br from-secondary/5 to-secondary/10 border-secondary/20", - green: "bg-gradient-to-br from-success/5 to-success/10 border-success/20", - orange: "bg-gradient-to-br from-warning/5 to-warning/10 border-warning/20", - red: "bg-gradient-to-br from-danger/5 to-danger/10 border-danger/20", -}; - -const iconColorMapping = { - primary: "primary" as const, - blue: "blue" as const, - purple: "purple" as const, - green: "green" as const, - orange: "orange" as const, - red: "red" as const, -}; - -export function FeatureCard({ - icon, - title, - description, - colorScheme = "primary", - className = "", -}: FeatureCardProps) { - return ( - - -
- -

{title}

-
-
- -

{description}

-
-
- ); -} diff --git a/app/_components/footer.tsx b/app/_components/footer.tsx deleted file mode 100644 index e8a0b983..00000000 --- a/app/_components/footer.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import { - faBookOpen, - faHeart, - faHouseChimneyWindow, -} from "@fortawesome/free-solid-svg-icons"; -import { Divider, Link } from "@heroui/react"; -import { ExternalLinkButton } from "./external-link-button"; -import { GradientIcon } from "./gradient-icon"; - -export function Footer() { - return ( -
-
-
- {/* Logo and tagline */} -
-
- -

- Cabin -

-
-

- Modern, intuitive, and lightning-fast package - manager for C++ developers. -

-
- - {/* Mobile navigation - only shown on small screens */} -
- - Documentation - - - GitHub - - - Sponsor - -
- - {/* Divider */} -
- -
- - {/* Copyright and links */} -
-

- Built by{" "} - - Ken Matsui - -

-

- © 2018-{new Date().getFullYear()} Cabin. All rights - reserved. -

-
-
-
- - {/* Background decoration */} -
-
- ); -} diff --git a/app/_components/gradient-icon.tsx b/app/_components/gradient-icon.tsx deleted file mode 100644 index 864b1a84..00000000 --- a/app/_components/gradient-icon.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -interface GradientIconProps { - icon: IconDefinition; - size?: "sm" | "md" | "lg" | "xl"; - colorScheme?: "primary" | "blue" | "purple" | "green" | "orange" | "red"; - className?: string; - iconClassName?: string; -} - -const sizeClasses = { - sm: "w-8 h-8", - md: "w-10 h-10", - lg: "w-12 h-12", - xl: "w-16 h-16", -}; - -const iconSizeClasses = { - sm: "text-sm", - md: "text-sm", - lg: "text-lg", - xl: "text-xl", -}; - -const colorVariants = { - primary: "bg-gradient-to-br from-primary-500 to-secondary-500", - blue: "bg-gradient-to-br from-primary-500 to-primary-600", - purple: "bg-gradient-to-br from-secondary-500 to-secondary-600", - green: "bg-gradient-to-br from-success-500 to-success-600", - orange: "bg-gradient-to-br from-warning-500 to-warning-600", - red: "bg-gradient-to-br from-danger-500 to-danger-600", -}; - -export function GradientIcon({ - icon, - size = "md", - colorScheme = "primary", - className = "", - iconClassName = "", -}: GradientIconProps) { - return ( -
- -
- ); -} diff --git a/app/_components/header.tsx b/app/_components/header.tsx deleted file mode 100644 index 142d0cb1..00000000 --- a/app/_components/header.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import { faBookOpen, faHeart } from "@fortawesome/free-solid-svg-icons"; -import { Navbar, NavbarBrand, NavbarContent, NavbarItem } from "@heroui/react"; -import NextLink from "next/link"; -import { ExternalLinkButton } from "./external-link-button"; - -import { Logo } from "./logo"; -import { SearchButton } from "./search"; - -export function Header() { - return ( - - {/* Mobile layout */} -
- - - -
- -
-
- - {/* Desktop layout */} -
- - - - - - - - - - - - - {/* Right side navigation */} - - {/* Docs link */} - - - Docs - - - - {/* GitHub link */} - - - GitHub Repository - - - - {/* Sponsor link */} - - - Sponsor - - - -
-
- ); -} diff --git a/app/_components/loading.tsx b/app/_components/loading.tsx deleted file mode 100644 index b61ba3f7..00000000 --- a/app/_components/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Spinner } from "@heroui/react"; - -export function Loading() { - return ( -
- -
- ); -} diff --git a/app/_components/logo.tsx b/app/_components/logo.tsx deleted file mode 100644 index ae93bb4f..00000000 --- a/app/_components/logo.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { faHouseChimneyWindow } from "@fortawesome/free-solid-svg-icons"; -import { GradientIcon } from "./gradient-icon"; - -export const Logo = () => ( -
- -
-

- Cabin -

-
-
-); diff --git a/app/_components/search.tsx b/app/_components/search.tsx deleted file mode 100644 index 7a9c7eb7..00000000 --- a/app/_components/search.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Input, Spinner } from "@heroui/react"; -import { useRouter } from "next/navigation"; -import { useState, useTransition } from "react"; - -export function SearchButton() { - const [value, setValue] = useState(""); - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "Enter" && value.trim()) { - startTransition(() => { - router.push(`/search?q=${encodeURIComponent(value.trim())}`); - }); - } - }; - - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - if (value.trim()) { - startTransition(() => { - router.push(`/search?q=${encodeURIComponent(value.trim())}`); - }); - } - }; - - return ( -
- - ) : ( - - ) - } - value={value} - onValueChange={setValue} - onKeyDown={handleKeyDown} - isDisabled={isPending} - /> - - ); -} diff --git a/app/_components/section-header.tsx b/app/_components/section-header.tsx deleted file mode 100644 index a2546fb1..00000000 --- a/app/_components/section-header.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -interface SectionHeaderProps { - icon: IconDefinition; - title: string; - iconColor?: string; - size?: "sm" | "md" | "lg"; - className?: string; -} - -const sizeClasses = { - sm: "text-lg", - md: "text-xl", - lg: "text-2xl", -}; - -export function SectionHeader({ - icon, - title, - iconColor = "text-primary", - size = "md", - className = "", -}: SectionHeaderProps) { - return ( -
- -

{title}

-
- ); -} diff --git a/app/_components/stat-item.tsx b/app/_components/stat-item.tsx deleted file mode 100644 index 511578a9..00000000 --- a/app/_components/stat-item.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Chip } from "@heroui/react"; -import type { ReactNode } from "react"; - -interface StatItemProps { - icon: IconDefinition; - label: string; - value: ReactNode; - className?: string; -} - -export function StatItem({ - icon, - label, - value, - className = "", -}: StatItemProps) { - return ( -
-
- - {label} -
- {typeof value === "string" || typeof value === "number" ? ( - - {value} - - ) : ( - value - )} -
- ); -} diff --git a/app/_lib/constants.ts b/app/_lib/constants.ts deleted file mode 100644 index 6edae3c2..00000000 --- a/app/_lib/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PER_PAGE = 20; diff --git a/app/_lib/hasuraClient.ts b/app/_lib/hasuraClient.ts deleted file mode 100644 index 4a373828..00000000 --- a/app/_lib/hasuraClient.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GraphQLClient } from "graphql-request"; -import { getSdk } from "~/graphql"; - -const HASURA_GRAPHQL_URL = "https://cabin.hasura.app/v1/graphql"; - -export const getHasuraClient = (token: string | null = null) => { - const headers = - token !== null - ? { - authorization: `Bearer ${token}`, - } - : undefined; - const client = new GraphQLClient(HASURA_GRAPHQL_URL, { - headers, - }); - return getSdk(client); -}; - -export type HasuraClient = ReturnType; diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index daeb3021..00000000 --- a/app/globals.css +++ /dev/null @@ -1,28 +0,0 @@ -/* biome-ignore-all lint/suspicious/noUnknownAtRules: reason */ -/* biome-ignore-all lint/complexity/noImportantStyles: reason */ - -@import "tailwindcss"; -@plugin "./hero.ts"; -@source "../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"; -@custom-variant dark (&:is(.dark *)); - -html { - font-size: 16px; - padding: 0px !important; - overflow-x: hidden; - scroll-padding-top: 64px; -} - -body { - min-height: 100vh; - position: relative; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} diff --git a/app/hero.ts b/app/hero.ts deleted file mode 100644 index 9cd4dc4e..00000000 --- a/app/hero.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { heroui } from "@heroui/react"; -export default heroui(); diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index b03c2110..00000000 --- a/app/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Analytics } from "@vercel/analytics/react"; -import { SpeedInsights } from "@vercel/speed-insights/next"; - -import { Footer } from "./_components/footer"; -import { Header } from "./_components/header"; -import { Providers } from "./providers"; - -import "./globals.css"; - -export default function Layout({ - children, -}: { - children: React.ReactNode; -}): React.ReactElement { - return ( - - - -
- {children} -
- - - - - - ); -} diff --git a/app/loading.ts b/app/loading.ts deleted file mode 100644 index d6c03aae..00000000 --- a/app/loading.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Loading } from "./_components/loading"; - -export default Loading; diff --git a/app/not-found.tsx b/app/not-found.tsx deleted file mode 100644 index 9d0eb629..00000000 --- a/app/not-found.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "404: Not Found", -}; - -export default function Custom404(): React.ReactElement { - return ( -
-

404

- This page could not be found. -
- ); -} diff --git a/app/packages/[group]/[name]/[version]/loading.ts b/app/packages/[group]/[name]/[version]/loading.ts deleted file mode 100644 index b8969798..00000000 --- a/app/packages/[group]/[name]/[version]/loading.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Loading } from "~/app/_components/loading"; - -export default Loading; diff --git a/app/packages/[group]/[name]/[version]/loading.tsx b/app/packages/[group]/[name]/[version]/loading.tsx deleted file mode 100644 index 6c23b10d..00000000 --- a/app/packages/[group]/[name]/[version]/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Loading } from "../../../../_components/loading"; - -export default function PackageVersionLoading() { - return ; -} diff --git a/app/packages/[group]/[name]/[version]/page.tsx b/app/packages/[group]/[name]/[version]/page.tsx deleted file mode 100644 index e7e4c3f2..00000000 --- a/app/packages/[group]/[name]/[version]/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { Metadata, ResolvingMetadata } from "next"; -import { notFound } from "next/navigation"; -import { getHasuraClient } from "~/app/_lib/hasuraClient"; -import { PackageDetails } from "../_components/package-details"; - -export const revalidate = 86400; // 1 day - -type Params = Promise<{ - group: string; - name: string; - version: string; -}>; - -export async function generateMetadata( - props: { - params: Params; - }, - _parent: ResolvingMetadata, -): Promise { - const params = await props.params; - return { - title: `${params.group}/${params.name} (v${params.version})`, - }; -} - -export default async function Page(props: { params: Params }) { - const params = await props.params; - - const hasuraClient = getHasuraClient(); - const data = await hasuraClient.getPackageByNameAndVersion({ - name: `${params.group}/${params.name}`, - version: params.version, - }); - if (!data || data.packages.length === 0) { - return notFound(); - } - - return ( - - ); -} diff --git a/app/packages/[group]/[name]/_components/package-details.tsx b/app/packages/[group]/[name]/_components/package-details.tsx deleted file mode 100644 index e8461114..00000000 --- a/app/packages/[group]/[name]/_components/package-details.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { - faBookOpen, - faBox, - faCalendarAlt, - faCode, - faCodeBranch, - faCube, - faDownload, - faGavel, - faHome, - faTag, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Card, CardBody, CardHeader, Chip, Code, Link } from "@heroui/react"; -import ReactMarkdown from "react-markdown"; -import { format } from "timeago.js"; -import { ExternalLinkButton } from "~/app/_components/external-link-button"; -import { GradientIcon } from "~/app/_components/gradient-icon"; -import { SectionHeader } from "~/app/_components/section-header"; -import { StatItem } from "~/app/_components/stat-item"; -import type { GetPackageByNameAndVersionQuery } from "~/graphql"; - -type PackageMetadata = { - dependencies?: unknown[]; - package: { - homepage?: string; - documentation?: string; - repository?: string; - }; -}; - -export function PackageDetails({ - pack, - numVersion, -}: { - pack: GetPackageByNameAndVersionQuery["packages"][0]; - numVersion: number; -}) { - const metadata = pack.metadata as PackageMetadata; - return ( -
- {/* Hero Section */} -
-
-
-
- -
-

- {pack.name} -

-
- - - v{pack.version} - -
-
-
-
- - } - > - C++{String(pack.edition).slice(-2)} - -
-
- {pack.description && ( -

- {pack.description} -

- )} -
-
- -
- {/* Main Content */} -
- {/* Installation */} - - - - - -

- Add the following line to your{" "} - - cabin.toml - {" "} - file: -

- {`"${pack.name}" = "${pack.version}"`} -
-
- - {/* README */} - - - - - - {pack.readme ? ( -
- - {pack.readme} - -
- ) : ( -
- -

- No README available for this package -

-
- )} -
-
-
- - {/* Sidebar */} -
- {/* Package Stats */} - - -

- Package Info -

-
- - - - - - {pack.license} - - } - /> - -
- - {/* Links */} - - -

Links

-
- - {metadata.package.homepage && ( - - Homepage - - )} - {metadata.package.documentation && ( - - Documentation - - )} - {metadata.package.repository && ( - - Repository - - )} - -
-
-
-
- ); -} diff --git a/app/packages/[group]/[name]/loading.ts b/app/packages/[group]/[name]/loading.ts deleted file mode 100644 index b8969798..00000000 --- a/app/packages/[group]/[name]/loading.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Loading } from "~/app/_components/loading"; - -export default Loading; diff --git a/app/packages/[group]/[name]/loading.tsx b/app/packages/[group]/[name]/loading.tsx deleted file mode 100644 index edb68c28..00000000 --- a/app/packages/[group]/[name]/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Loading } from "../../../_components/loading"; - -export default function PackageLoading() { - return ; -} diff --git a/app/packages/[group]/[name]/page.tsx b/app/packages/[group]/[name]/page.tsx deleted file mode 100644 index ae683036..00000000 --- a/app/packages/[group]/[name]/page.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { Metadata, ResolvingMetadata } from "next"; -import { notFound } from "next/navigation"; -import { getHasuraClient } from "~/app/_lib/hasuraClient"; -import { PackageDetails } from "./_components/package-details"; - -export const revalidate = 86400; // 1 day - -type Params = Promise<{ - group: string; - name: string; -}>; - -export async function generateMetadata( - props: { - params: Params; - }, - _parent: ResolvingMetadata, -): Promise { - const params = await props.params; - return { - title: `${params.group}/${params.name} (latest)`, - }; -} - -export default async function Page(props: { params: Params }) { - const params = await props.params; - - const hasuraClient = getHasuraClient(); - const data = await hasuraClient.getPackagesByName({ - name: `${params.group}/${params.name}`, - }); - if (!data || data.packages.length === 0) { - return notFound(); - } - - data.packages.sort((a, b) => { - const semver = require("semver"); - return semver.rcompare(a.version, b.version); - }); - - return ( - - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 13b839df..00000000 --- a/app/page.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { - faArrowRight, - faBolt, - faBox, - faCode, - faCog, - faDownload, - faHouseChimneyWindow, - faRocket, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Button, Link } from "@heroui/react"; -import type { Metadata } from "next"; -import Image from "next/image"; -import { FeatureCard } from "./_components/feature-card"; - -export const revalidate = 86400; // 1 day - -export const metadata: Metadata = { - title: "Cabin - C++ package manager and build system", - description: - "Modern, intuitive, and lightning-fast package manager and build system for C++ developers.", -}; - -export default function Home() { - return ( -
- {/* Hero Section */} -
-
-
- {/* Content */} -
-
-

- - Effortlessly - {" "} - build and share your{" "} - - C++ packages - -

-

- Modern, intuitive, and lightning-fast - package manager and build system for C++ - developers. -

-
- - {/* CTA Buttons */} -
- - -
-
- - {/* Demo */} -
-
- Demo of Cabin package manager -
- {/* Floating badge */} -
- -
-
-
-
- - {/* Background decoration */} -
-
- - {/* Features Section */} -
-
-
-

- Why Choose{" "} - - Cabin - - ? -

-

- Built for modern C++ development with developer - experience as the top priority. -

-
- -
- - - - - - - - - - - -
-
-
- - {/* CTA Section */} -
-
-

- Ready to build something amazing? -

-

- Join the growing community of developers building better - C++ applications with Cabin. -

-
- - -
-
-
-
- ); -} diff --git a/app/providers.tsx b/app/providers.tsx deleted file mode 100644 index 8caad436..00000000 --- a/app/providers.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import { HeroUIProvider } from "@heroui/react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; - -export function Providers({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} diff --git a/app/search/_components/pagination.tsx b/app/search/_components/pagination.tsx deleted file mode 100644 index e929a969..00000000 --- a/app/search/_components/pagination.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { Pagination as NextUIPagination } from "@heroui/react"; -import { useRouter } from "next/navigation"; -import { useTransition } from "react"; - -type Props = { - query: string; - page: number; - numPages: number; - perPage: number; -}; - -export function Pagination({ query, page, numPages, perPage }: Props) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); - - const handlePageChange = (page: number) => { - startTransition(() => { - router.push( - `/search?q=${encodeURIComponent(query)}&page=${page}&perPage=${perPage}`, - ); - // Scroll to top smoothly after navigation - setTimeout(() => { - window.scrollTo({ top: 0, behavior: "smooth" }); - }, 100); - }); - }; - - return ( - - ); -} diff --git a/app/search/loading.ts b/app/search/loading.ts deleted file mode 100644 index b8969798..00000000 --- a/app/search/loading.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Loading } from "~/app/_components/loading"; - -export default Loading; diff --git a/app/search/loading.tsx b/app/search/loading.tsx deleted file mode 100644 index 1afd1a7d..00000000 --- a/app/search/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Loading } from "../_components/loading"; - -export default function SearchLoading() { - return ; -} diff --git a/app/search/page.tsx b/app/search/page.tsx deleted file mode 100644 index 2055bc31..00000000 --- a/app/search/page.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { - faBox, - faCube, - faSearch, - faTag, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Card, CardBody, CardHeader, Chip, Divider } from "@heroui/react"; -import type { Metadata, ResolvingMetadata } from "next"; -import NextLink from "next/link"; -import { format } from "timeago.js"; -import { GradientIcon } from "../_components/gradient-icon"; -import { PER_PAGE } from "../_lib/constants"; -import { getHasuraClient } from "../_lib/hasuraClient"; -import { Pagination } from "./_components/pagination"; - -export const revalidate = 86400; // 1 day - -type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; - -export async function generateMetadata( - props: { - searchParams: SearchParams; - }, - _parent: ResolvingMetadata, -): Promise { - const searchParams = await props.searchParams; - - if (!searchParams || !searchParams.q || searchParams.q === "") { - return { - title: "All packages", - }; - } - - return { - title: `Search results for "${searchParams.q}"`, - }; -} - -export default async function Page(props: { searchParams: SearchParams }) { - const searchParams = await props.searchParams; - - const query = searchParams?.q ? String(searchParams.q) : ""; - const page = Number(searchParams?.page ?? 1); - const perPage = Number(searchParams?.perPage ?? PER_PAGE); - - const hasuraClient = getHasuraClient(); - const data = await hasuraClient.searchPackages({ - name: `%${query}%`, - limit: perPage, - offset: (page - 1) * perPage, - }); - - const totalCount = data?.packages_aggregate?.aggregate?.count ?? 0; - const currentLast = page * perPage; - const currentPos = { - first: currentLast - (perPage - 1), - last: currentLast > totalCount ? totalCount : currentLast, - }; - const numPages = Math.ceil(totalCount / perPage); - - // Empty state - if (!data || data.packages.length === 0) { - return ( -
- {/* Header */} -
-
- -

- {query - ? `Search results for "${query}"` - : "All packages"} -

-
-
- - {/* Empty state */} -
- -

- No packages found -

-

- {query - ? `We couldn't find any packages matching "${query}". Try a different search term.` - : "No packages are available at the moment."} -

-
-
- ); - } - - return ( -
- {/* Header */} -
-
- -
-

- {query - ? `Search results for "${query}"` - : "All packages"} -

-

- Displaying{" "} - - {currentPos.first}-{currentPos.last} - {" "} - of{" "} - - {totalCount} - {" "} - packages -

-
-
-
- - {/* Results */} -
- {data.packages.map((pkg) => ( - - -
-
-
- -
-
-

- {pkg.name} -

-
- - - v{pkg.version} - -
-
-
-
- - C++{String(pkg.edition).slice(-2)} - - - {format(pkg.published_at as string)} - -
-
-
- {pkg.description && ( - <> - - -

- {pkg.description} -

-
- - )} -
- ))} -
- - {/* Pagination */} - {numPages > 1 && ( -
- -
- )} -
- ); -} diff --git a/astro.config.ts b/astro.config.ts new file mode 100644 index 00000000..c493e846 --- /dev/null +++ b/astro.config.ts @@ -0,0 +1,13 @@ +import sitemap from "@astrojs/sitemap"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "astro/config"; +import { SITE_URL } from "./src/lib/constants"; + +export default defineConfig({ + site: SITE_URL, + output: "static", + integrations: [sitemap()], + vite: { + plugins: [tailwindcss()], + }, +}); diff --git a/biome.json b/biome.json index 30276dd5..8dc4273c 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,17 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json", "files": { - "includes": ["**", "!.next/**/*", "!graphql/**/*"] + "includes": [ + "**", + "!.claude/**/*", + "!.next/**/*", + "!.astro/**/*", + "!dist/**/*", + "!graphql/**/*", + "!graphql.schema.json", + "!node_modules/**/*", + "!src/**/*.astro" + ] }, "linter": { "enabled": true, diff --git a/codegen.ts b/codegen.ts index 4d2abd18..cdef71fb 100644 --- a/codegen.ts +++ b/codegen.ts @@ -1,7 +1,8 @@ import type { CodegenConfig } from "@graphql-codegen/cli"; +import { HASURA_ENDPOINT } from "./src/lib/constants"; const config: CodegenConfig = { - schema: "https://cabin.hasura.app/v1/graphql", + schema: HASURA_ENDPOINT, documents: ["./graphql/**/*.gql"], overwrite: true, generates: { @@ -13,7 +14,8 @@ const config: CodegenConfig = { ], config: { skipTypename: false, - withHooks: true, + useTypeImports: true, + withHooks: false, withHOC: false, withComponent: false, }, diff --git a/graphql/getAllPackages.gql b/graphql/getAllPackages.gql new file mode 100644 index 00000000..c0a3255c --- /dev/null +++ b/graphql/getAllPackages.gql @@ -0,0 +1,16 @@ +query getAllPackages($limit: Int!, $offset: Int!) @cached(ttl: 600) { + packages(limit: $limit, offset: $offset, order_by: {name: asc, version: asc, id: asc}) { + authors + description + edition + id + license + metadata + name + published_at + readme + repository + sha256sum + version + } +} diff --git a/graphql/getPackageByNameAndVersion.gql b/graphql/getPackageByNameAndVersion.gql deleted file mode 100644 index 0727fd53..00000000 --- a/graphql/getPackageByNameAndVersion.gql +++ /dev/null @@ -1,21 +0,0 @@ -query getPackageByNameAndVersion($name: String!, $version: String!) @cached(ttl: 600) { - packages(where: {name: {_eq: $name}, version: {_eq: $version}}) { - authors - description - edition - id - license - metadata - name - published_at - readme - repository - sha256sum - version - } - packages_aggregate(where: {name: {_eq: $name}}) { - aggregate { - count - } - } -} diff --git a/graphql/getPackagesByName.gql b/graphql/getPackagesByName.gql deleted file mode 100644 index 0e3c772e..00000000 --- a/graphql/getPackagesByName.gql +++ /dev/null @@ -1,16 +0,0 @@ -query getPackagesByName($name: String!) @cached(ttl: 600) { - packages(where: {name: {_eq: $name}}) { - authors - description - edition - id - license - metadata - name - published_at - readme - repository - sha256sum - version - } -} diff --git a/graphql/searchPackages.gql b/graphql/searchPackages.gql deleted file mode 100644 index 3c134f29..00000000 --- a/graphql/searchPackages.gql +++ /dev/null @@ -1,15 +0,0 @@ -query searchPackages($name: String!, $limit: Int!, $offset: Int!) @cached(ttl: 600) { - packages(where: {name: {_ilike: $name}}, limit: $limit, offset: $offset, distinct_on: name) { - description - edition - id - name - published_at - version - } - packages_aggregate(where: {name: {_ilike: $name}}) { - aggregate { - count(distinct: true) - } - } -} diff --git a/next-env.d.ts b/next-env.d.ts deleted file mode 100644 index 9edff1c7..00000000 --- a/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next-sitemap.config.js b/next-sitemap.config.js deleted file mode 100644 index a52e4f1b..00000000 --- a/next-sitemap.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next-sitemap').IConfig} */ - -export default { - siteUrl: "https://cabinpkg.com", - generateRobotsTxt: true, // (optional) - // ...other options -}; diff --git a/next.config.ts b/next.config.ts deleted file mode 100644 index ba71ae8d..00000000 --- a/next.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - images: { - remotePatterns: [ - { - protocol: "https", - hostname: "github.com", - pathname: "**", - }, - ], - }, -}; - -export default nextConfig; diff --git a/package.json b/package.json index 59ef0e75..60575c2b 100644 --- a/package.json +++ b/package.json @@ -7,58 +7,40 @@ }, "type": "module", "scripts": { - "dev": "next dev", - "prebuild": "yarn generate", - "build": "next build --webpack", - "postbuild": "next-sitemap", - "start": "next start", + "dev": "yarn generate && astro dev", + "build": "yarn generate && yarn typecheck && astro build && yarn verify:csp", + "preview": "astro preview", "lint": "biome check .", + "typecheck": "astro check", + "verify:csp": "node scripts/verify-no-inline-scripts.mjs", "fmt": "biome format . --write", "generate": "graphql-codegen --config codegen.ts" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^7.2.0", - "@fortawesome/free-brands-svg-icons": "^7.2.0", - "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@fortawesome/react-fontawesome": "^3.3.1", - "@heroui/react": "2.8.9", - "@heroui/theme": "^2.4.19", - "@vercel/analytics": "^2.0.1", - "@vercel/speed-insights": "^2.0.0", - "framer-motion": "12.38.0", + "@lucide/astro": "^0.577.0", "graphql": "^16.13.2", - "next": "^16.2.4", - "next-themes": "^0.4.6", - "react": "19.2.5", - "react-dom": "19.2.5", - "react-markdown": "^10.1.0", - "semver": "^7.7.4", - "timeago.js": "^4.0.2" + "graphql-request": "^7.4.0", + "graphql-tag": "^2.0.0", + "markdown-it": "^14.1.0", + "semver": "^7.7.4" }, "devDependencies": { + "@astrojs/check": "^0.9.0", + "@astrojs/sitemap": "^3.2.1", "@biomejs/biome": "2.4.14", "@graphql-codegen/cli": "^7.0.0", "@graphql-codegen/introspection": "^6.0.0", "@graphql-codegen/typescript-graphql-request": "^7.0.1", "@graphql-codegen/typescript-operations": "^6.0.0", - "@tailwindcss/postcss": "^4.2.4", "@tailwindcss/typography": "^0.5.19", - "@tsconfig/next": "^2.0.6", + "@tailwindcss/vite": "^4.2.4", "@types/node": "25.6.0", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "graphql-request": "^7.4.0", - "graphql-tag": "^2.0.0", - "next-sitemap": "^4.2.3", - "postcss": "^8.5.13", - "tailwind-variants": "^3.2.2", + "@types/markdown-it": "^14.1.2", + "@types/semver": "^7.7.1", + "astro": "^5.0.0", "tailwindcss": "4.2.4", - "typescript": "^6.0.3" - }, - "resolutions": { - "react": "19.2.5", - "react-dom": "19.2.5", - "react-textarea-autosize": "^8.5.3" + "typescript": "^6.0.3", + "wrangler": "^4.2.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 78253b6d..00000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index 9769ce4e..00000000 --- a/proxy.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; - -const IS_DEV = process.env.NODE_ENV === "development"; - -export function proxy(request: NextRequest) { - const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); - - // TODO: unsafe-inline is needed for NextUI? - // script-src 'self' 'nonce-${nonce}' 'strict-dynamic' static.cloudflareinsights.com ${ - // IS_DEV ? "'unsafe-eval'" : "" - // }; - // style-src 'self' 'nonce-${nonce}'; - - const cspHeader = ` - default-src 'self'; - script-src 'self' 'unsafe-inline' static.cloudflareinsights.com ${ - IS_DEV ? "va.vercel-scripts.com 'unsafe-eval'" : "" - }; - style-src 'self' 'unsafe-inline'; - connect-src 'self' vitals.vercel-insights.com; - img-src 'self' github.com release-assets.githubusercontent.com blob: data:; - font-src 'self'; - object-src 'none'; - base-uri 'self'; - form-action 'self'; - frame-ancestors 'none'; - block-all-mixed-content; - upgrade-insecure-requests; -`; - // Replace newline characters and spaces - const contentSecurityPolicyHeaderValue = cspHeader - .replace(/\s{2,}/g, " ") - .trim(); - - const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-nonce", nonce); - - requestHeaders.set( - "Content-Security-Policy", - contentSecurityPolicyHeaderValue, - ); - - const response = NextResponse.next({ - request: { - headers: requestHeaders, - }, - }); - response.headers.set( - "Content-Security-Policy", - contentSecurityPolicyHeaderValue, - ); - response.headers.set("X-Frame-Options", "deny"); - response.headers.set("X-Content-Type-Options", "nosniff"); - response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); - response.headers.set( - "Permissions-Policy", - "camera=(), microphone=(), geolocation=()", - ); - - return response; -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - { - source: "/((?!api|_next/static|_next/image|favicon.ico).*)", - missing: [ - { type: "header", key: "next-router-prefetch" }, - { type: "header", key: "purpose", value: "prefetch" }, - ], - }, - ], -}; diff --git a/public/_headers b/public/_headers new file mode 100644 index 00000000..f5348cec --- /dev/null +++ b/public/_headers @@ -0,0 +1,6 @@ +/* + Content-Security-Policy: default-src 'self'; script-src 'self' static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' github.com release-assets.githubusercontent.com blob: data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content; upgrade-insecure-requests + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() diff --git a/public/header-search.js b/public/header-search.js new file mode 100644 index 00000000..24db638a --- /dev/null +++ b/public/header-search.js @@ -0,0 +1,12 @@ +const siteSearchInput = document.getElementById("site-search"); + +if ( + siteSearchInput instanceof HTMLInputElement && + window.location.pathname.replace(/\/$/, "") === + siteSearchInput.dataset.searchPath +) { + const query = new URLSearchParams(window.location.search).get("q"); + if (query !== null) { + siteSearchInput.value = query; + } +} diff --git a/public/manifest.json b/public/manifest.json index 53f313a1..359b78d0 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,5 +11,5 @@ "start_url": "./index.html", "display": "standalone", "theme_color": "#000000", - "background_color": "#ffffff" + "background_color": "#05070d" } diff --git a/scripts/verify-no-inline-scripts.mjs b/scripts/verify-no-inline-scripts.mjs new file mode 100644 index 00000000..5f184924 --- /dev/null +++ b/scripts/verify-no-inline-scripts.mjs @@ -0,0 +1,46 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; + +const distDirectory = path.resolve("dist"); +const inlineScripts = []; + +for (const filePath of await findHtmlFiles(distDirectory)) { + const html = await readFile(filePath, "utf8"); + const scriptTags = html.matchAll(/]*)>/gi); + + for (const match of scriptTags) { + const attributes = match[1] ?? ""; + if (!/\ssrc\s*=/i.test(attributes)) { + inlineScripts.push({ + filePath, + tag: match[0], + }); + } + } +} + +if (inlineScripts.length > 0) { + console.error("Inline +
diff --git a/src/components/Icon.astro b/src/components/Icon.astro new file mode 100644 index 00000000..dbf9e733 --- /dev/null +++ b/src/components/Icon.astro @@ -0,0 +1,58 @@ +--- +import { + ArrowRight, + Bolt, + BookOpen, + Box, + Calendar, + Code, + Cuboid, + Download, + ExternalLink, + Gavel, + GitBranch, + Heart, + House, + Rocket, + Search, + Tag, + Wrench, +} from "@lucide/astro"; + +const icons = { + "arrow-right": ArrowRight, + bolt: Bolt, + book: BookOpen, + box: Box, + branch: GitBranch, + calendar: Calendar, + code: Code, + cube: Cuboid, + download: Download, + external: ExternalLink, + gavel: Gavel, + heart: Heart, + home: House, + rocket: Rocket, + search: Search, + tag: Tag, + wrench: Wrench, +}; + +export type IconName = keyof typeof icons; + +interface Props { + name: IconName; + class?: string; + ariaLabel?: string; +} + +const { name, class: className = "h-5 w-5", ariaLabel } = Astro.props; +const Component = icons[name]; +--- + + diff --git a/src/components/Logo.astro b/src/components/Logo.astro new file mode 100644 index 00000000..588219e4 --- /dev/null +++ b/src/components/Logo.astro @@ -0,0 +1,10 @@ +--- +import GradientIcon from "./GradientIcon.astro"; +--- + + + + + diff --git a/src/components/package/InstallSnippet.astro b/src/components/package/InstallSnippet.astro new file mode 100644 index 00000000..9cdda855 --- /dev/null +++ b/src/components/package/InstallSnippet.astro @@ -0,0 +1,22 @@ +--- +import type { PackageRecord } from "../../lib/packages"; +import Icon from "../Icon.astro"; +import Card from "../ui/Card.astro"; +import CodeBlock from "../ui/CodeBlock.astro"; + +interface Props { + package: PackageRecord; +} + +const { package: pack } = Astro.props; +const installSnippet = `"${pack.name}" = "${pack.version}"`; +--- + + +
+ +

Installation

+
+

Add the following line to your cabin.toml file:

+ +
diff --git a/src/components/package/PackageCard.astro b/src/components/package/PackageCard.astro new file mode 100644 index 00000000..de98acca --- /dev/null +++ b/src/components/package/PackageCard.astro @@ -0,0 +1,34 @@ +--- +import type { PackageListItem } from "../../lib/types"; +import PackageCardContent from "./PackageCardContent.astro"; + +interface Props { + package?: PackageListItem; + templateId?: string; + templateLinkable?: boolean; +} + +const { + package: item, + templateId, + templateLinkable = true, +} = Astro.props; +const templatePackage: PackageListItem = item ?? { + name: "", + version: "", + description: "", + edition: "", + published_at: "", + href: templateLinkable ? "#" : null, +}; +--- + +{ + templateId ? ( + + ) : ( + + ) +} diff --git a/src/components/package/PackageCardContent.astro b/src/components/package/PackageCardContent.astro new file mode 100644 index 00000000..4f14a245 --- /dev/null +++ b/src/components/package/PackageCardContent.astro @@ -0,0 +1,73 @@ +--- +import { formatEdition, formatRelativeTime } from "../../lib/format"; +import type { PackageListItem } from "../../lib/types"; +import Icon from "../Icon.astro"; +import Badge from "../ui/Badge.astro"; +import Card from "../ui/Card.astro"; + +interface Props { + package?: PackageListItem; +} + +const { package: item } = Astro.props; + +const packageName = item?.name ?? ""; +const version = item?.version ?? ""; +const description = item?.description ?? ""; +const edition = item ? formatEdition(item.edition) : ""; +const href = item?.href ?? null; +const isLinkable = Boolean(href); +const publishedAt = item?.published_at ?? ""; +const publishedLabel = publishedAt ? formatRelativeTime(publishedAt) : ""; +--- + + +
+
+
+ +
+
+

+ {packageName} +

+

+ v{version} +

+
+
+ +
+ + {edition} + +

+ {publishedLabel} +

+
+
+ +

+ {description} +

+
diff --git a/src/components/package/PackageDetailView.astro b/src/components/package/PackageDetailView.astro new file mode 100644 index 00000000..1db116c3 --- /dev/null +++ b/src/components/package/PackageDetailView.astro @@ -0,0 +1,70 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import { formatEdition } from "../../lib/format"; +import type { PackageRecord } from "../../lib/packages"; +import GradientIcon from "../GradientIcon.astro"; +import Icon from "../Icon.astro"; +import Badge from "../ui/Badge.astro"; +import InstallSnippet from "./InstallSnippet.astro"; +import PackageMetaGrid from "./PackageMetaGrid.astro"; +import ReadmeRenderer from "./ReadmeRenderer.astro"; + +interface Props { + pack: PackageRecord; + versionCount: number; + title: string; + description: string; + canonicalPath: string; +} + +const { pack, versionCount, title, description, canonicalPath } = Astro.props; +--- + + +
+
+
+
+ +
+

+ {pack.name} +

+

+ + v{pack.version} +

+
+
+
+ + + {formatEdition(pack.edition)} + +
+
+ + { + pack.description && ( +

+ {pack.description} +

+ ) + } +
+
+ +
+
+ + +
+ + +
+
diff --git a/src/components/package/PackageMetaGrid.astro b/src/components/package/PackageMetaGrid.astro new file mode 100644 index 00000000..37e3e028 --- /dev/null +++ b/src/components/package/PackageMetaGrid.astro @@ -0,0 +1,142 @@ +--- +import { + formatEdition, + formatRelativeTime, + stringifyValue, +} from "../../lib/format"; +import { + normalizePackageMetadata, + type PackageRecord, +} from "../../lib/packages"; +import Icon from "../Icon.astro"; +import Badge from "../ui/Badge.astro"; +import Card from "../ui/Card.astro"; +import ExternalLink from "../ui/ExternalLink.astro"; + +interface Props { + package: PackageRecord; + versionCount: number; +} + +const { package: pack, versionCount } = Astro.props; +const metadata = normalizePackageMetadata(pack.metadata); +const publishedAt = stringifyValue(pack.published_at); +const license = stringifyValue(pack.license); +const licenseHref = license + ? `https://choosealicense.com/licenses/${license.toLowerCase()}/` + : undefined; +--- + + diff --git a/src/components/package/ReadmeRenderer.astro b/src/components/package/ReadmeRenderer.astro new file mode 100644 index 00000000..9eca1f9b --- /dev/null +++ b/src/components/package/ReadmeRenderer.astro @@ -0,0 +1,34 @@ +--- +import { renderReadmeMarkdown } from "../../lib/markdown"; +import Icon from "../Icon.astro"; +import Card from "../ui/Card.astro"; + +interface Props { + readme: string | null | undefined; +} + +const { readme } = Astro.props; +const readmeHtml = readme ? renderReadmeMarkdown(readme) : ""; +--- + + +
+ +

README

+
+ { + readme ? ( +
+ ) : ( +
+
+ +
+

No README available for this package

+
+ ) + } + diff --git a/src/components/ui/Badge.astro b/src/components/ui/Badge.astro new file mode 100644 index 00000000..3668b48f --- /dev/null +++ b/src/components/ui/Badge.astro @@ -0,0 +1,22 @@ +--- +import { badgeToneClasses, joinClasses } from "../../lib/classes"; + +interface Props { + tone?: keyof typeof badgeToneClasses; + class?: string; + [key: string]: unknown; +} + +const { tone = "default", class: className = "", ...attrs } = Astro.props; +--- + + + + diff --git a/src/components/ui/Button.astro b/src/components/ui/Button.astro new file mode 100644 index 00000000..a552ae2d --- /dev/null +++ b/src/components/ui/Button.astro @@ -0,0 +1,55 @@ +--- +import { + buttonBaseClass, + buttonVariantClasses, + focusRingClass, + iconButtonClass, + joinClasses, +} from "../../lib/classes"; + +interface Props { + href?: string; + external?: boolean; + variant?: keyof typeof buttonVariantClasses; + class?: string; + iconOnly?: boolean; + ariaLabel?: string; + type?: "button" | "submit" | "reset"; +} + +const { + href, + external = false, + variant = "primary", + class: className = "", + iconOnly = false, + ariaLabel, + type = "button", +} = Astro.props; + +const classes = joinClasses( + buttonBaseClass, + buttonVariantClasses[variant], + focusRingClass, + iconOnly && iconButtonClass, + className, +); +--- + +{ + href ? ( + + + + ) : ( + + ) +} diff --git a/src/components/ui/Card.astro b/src/components/ui/Card.astro new file mode 100644 index 00000000..8b14475d --- /dev/null +++ b/src/components/ui/Card.astro @@ -0,0 +1,42 @@ +--- +import { + focusRingClass, + interactiveSurfaceClass, + joinClasses, + surfaceClass, +} from "../../lib/classes"; + +interface Props { + href?: string | null; + interactive?: boolean; + class?: string; + ariaLabel?: string; +} + +const { + href, + interactive = Boolean(href), + class: className = "", + ariaLabel, +} = Astro.props; + +const classes = joinClasses( + "rounded-3xl", + surfaceClass, + interactive && interactiveSurfaceClass, + href && focusRingClass, + className, +); +--- + +{ + href ? ( + + + + ) : ( +
+ +
+ ) +} diff --git a/src/components/ui/CodeBlock.astro b/src/components/ui/CodeBlock.astro new file mode 100644 index 00000000..6f0bb6e7 --- /dev/null +++ b/src/components/ui/CodeBlock.astro @@ -0,0 +1,15 @@ +--- +interface Props { + code: string; + class?: string; +} + +const { code, class: className = "" } = Astro.props; +--- + +
{code}
diff --git a/src/components/ui/ExternalLink.astro b/src/components/ui/ExternalLink.astro new file mode 100644 index 00000000..7efe4ae6 --- /dev/null +++ b/src/components/ui/ExternalLink.astro @@ -0,0 +1,35 @@ +--- +import Icon from "../Icon.astro"; +import type { IconName } from "../Icon.astro"; +import Button from "./Button.astro"; + +interface Props { + href: string; + icon?: IconName; + label?: string; + class?: string; + iconOnly?: boolean; + variant?: "primary" | "secondary" | "ghost"; +} + +const { + href, + icon = "external", + label, + class: className = "", + iconOnly = false, + variant = "ghost", +} = Astro.props; +--- + + diff --git a/src/components/ui/Pagination.astro b/src/components/ui/Pagination.astro new file mode 100644 index 00000000..598c075c --- /dev/null +++ b/src/components/ui/Pagination.astro @@ -0,0 +1,62 @@ +--- +import { getPaginationItems } from "../../lib/pagination"; + +interface Props { + query: string; + page: number; + perPage: number; + totalPages: number; + id?: string; + class?: string; +} + +const { + query, + page, + perPage, + totalPages, + id, + class: className = "", +} = Astro.props; + +const paginationItems = getPaginationItems({ + query, + currentPage: page, + perPage, + totalPages, +}); +--- + +{ + totalPages > 1 && ( + + ) +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..f964fe0c --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 00000000..0ee11777 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,51 @@ +--- +import Footer from "../components/Footer.astro"; +import Header from "../components/Header.astro"; +import { SITE_DESCRIPTION, SITE_NAME, SITE_URL } from "../lib/constants"; +import "../styles/global.css"; + +interface Props { + title?: string; + description?: string; + canonicalPath?: string; + contentClass?: string; +} + +const { + title = `${SITE_NAME} - C++ package manager and build system`, + description = SITE_DESCRIPTION, + canonicalPath, + contentClass = "", +} = Astro.props; + +const pageTitle = title.includes(SITE_NAME) ? title : `${title} | ${SITE_NAME}`; +const canonicalUrl = new URL(canonicalPath ?? Astro.url.pathname, SITE_URL); +--- + + + + + + + + + + + + + + + + + + + {pageTitle} + + +
+
+ +
+