fixed nav, added sanity and guides pages
CI / test (push) Successful in 5s

This commit is contained in:
2026-03-12 07:10:37 -04:00
parent f8265063d5
commit fb52521495
14 changed files with 13284 additions and 179 deletions
+151
View File
@@ -0,0 +1,151 @@
import { postBySlugQuery, postSlugsQuery, urlFor } from '@/lib/sanity'
import { sanityFetch } from '@/lib/sanityFetch'
import { PortableTextRenderer } from '@/components/PortableTextRenderer'
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
export const revalidate = 60
type Params = { slug: string }
type Post = {
_id: string
title: string
slug: { current: string }
publishedAt: string
excerpt: string | null
mainImage: { asset: object; alt?: string } | null
body: unknown[]
seoTitle: string | null
seoDescription: string | null
author: { name: string; image?: object; bio?: string } | null
}
export async function generateStaticParams() {
try {
const slugs: { slug: string }[] = await sanityFetch<{ slug: string }[]>(postSlugsQuery)
return slugs.map((s) => ({ slug: s.slug }))
} catch (err) {
console.error('[Guides] generateStaticParams fetch failed:', err)
return []
}
}
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { slug } = await params
let post: Post | null = null
try {
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
} catch (err) {
console.error('[Guides] generateMetadata fetch failed:', err)
}
if (!post) return {}
return {
title: post.seoTitle ?? post.title,
description: post.seoDescription ?? post.excerpt ?? undefined,
openGraph: {
title: post.seoTitle ?? post.title,
description: post.seoDescription ?? post.excerpt ?? undefined,
images: post.mainImage?.asset
? [{ url: urlFor(post.mainImage).width(1200).height(630).url() }]
: [],
},
}
}
function formatDate(iso: string) {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
export default async function PostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params
let post: Post | null = null
try {
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
} catch (err) {
console.error('[Guides] PostPage fetch failed:', err)
}
if (!post) notFound()
return (
<main className="mx-auto max-w-3xl px-4 py-10">
{/* Breadcrumb */}
<nav className="mb-6 text-xs text-zinc-500">
<Link href="/guides" className="hover:text-zinc-300">
Guides
</Link>
</nav>
{/* Header */}
<header className="mb-8">
<div className="mb-3 flex items-center gap-2 text-[0.7rem] text-zinc-500">
{post.author?.name && <span>{post.author.name}</span>}
{post.author?.name && <span>·</span>}
<time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>
</div>
<h1 className="text-3xl font-semibold leading-tight tracking-tight text-zinc-100 md:text-4xl">
{post.title}
</h1>
{post.excerpt && (
<p className="mt-4 text-base text-zinc-400 leading-relaxed">
{post.excerpt}
</p>
)}
</header>
{/* Hero image */}
{post.mainImage?.asset && (
<div className="relative mb-10 h-64 w-full overflow-hidden rounded-xl md:h-80">
<Image
src={urlFor(post.mainImage).width(1200).height(630).url()}
alt={post.mainImage.alt ?? post.title}
fill
priority
className="object-cover"
sizes="(max-width: 768px) 100vw, 768px"
/>
</div>
)}
{/* Body */}
{post.body && (
<article className="border-b border-zinc-800 pb-10">
<PortableTextRenderer value={post.body} />
</article>
)}
{/* CTA footer */}
<div className="mt-10 rounded-xl border border-zinc-800 bg-zinc-950/60 p-6 text-center">
<p className="text-sm font-medium text-zinc-200">
Ready to put this into practice?
</p>
<p className="mt-1 text-xs text-zinc-500">
Use the Battl Builder to price out your parts and track your total.
</p>
<Link
href="/builder"
className="mt-4 inline-block rounded-md bg-amber-400 px-5 py-2.5 text-sm font-semibold text-black hover:bg-amber-300 transition-colors"
>
Start Your Build
</Link>
</div>
{/* Back link */}
<div className="mt-8 text-center">
<Link href="/guides" className="text-xs text-zinc-500 hover:text-zinc-300">
Back to all guides
</Link>
</div>
</main>
)
}
+119
View File
@@ -0,0 +1,119 @@
import { postsQuery, urlFor } from '@/lib/sanity'
import { sanityFetch } from '@/lib/sanityFetch'
import Link from 'next/link'
import Image from 'next/image'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Build Guides',
description: 'AR-15 build guides, part breakdowns, and buying advice from Battl Builders.',
}
export const revalidate = 60 // ISR — regenerate every 60s
type Post = {
_id: string
title: string
slug: { current: string }
publishedAt: string
excerpt: string | null
mainImage: { asset: object; alt?: string } | null
author: { name: string; image?: object } | null
}
function formatDate(iso: string) {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
export default async function GuidesPage() {
let posts: Post[] = []
try {
posts = await sanityFetch<Post[]>(postsQuery)
} catch (err) {
console.error('[Guides] Sanity fetch failed:', err)
}
return (
<main className="mx-auto max-w-4xl px-4 py-10">
<header className="mb-10">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500">
Battl Builders
</p>
<h1 className="mt-2 text-3xl font-semibold tracking-tight">
Build Guides
</h1>
<p className="mt-3 text-sm text-zinc-400 max-w-xl">
Part breakdowns, buying advice, and build walkthroughs to help you
make smarter decisions before you spend a dollar.
</p>
</header>
{posts.length === 0 ? (
<p className="text-sm text-zinc-500">No guides published yet check back soon.</p>
) : (
<div className="space-y-8">
{posts.map((post) => (
<article
key={post._id}
className="group flex flex-col gap-4 rounded-xl border border-zinc-800 bg-zinc-950/60 p-5 transition-colors hover:border-zinc-700 sm:flex-row"
>
{/* Thumbnail */}
{post.mainImage?.asset && (
<div className="relative h-44 w-full shrink-0 overflow-hidden rounded-lg sm:h-32 sm:w-48">
<Image
src={urlFor(post.mainImage).width(400).height(256).url()}
alt={post.mainImage.alt ?? post.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 640px) 100vw, 192px"
/>
</div>
)}
{/* Text */}
<div className="flex flex-col justify-center gap-2">
<div className="flex items-center gap-2 text-[0.7rem] text-zinc-500">
{post.author?.name && <span>{post.author.name}</span>}
{post.author?.name && <span>·</span>}
<time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>
</div>
<h2 className="text-lg font-semibold leading-snug text-zinc-100 group-hover:text-amber-300 transition-colors">
<Link href={`/guides/${post.slug.current}`} className="stretched-link">
{post.title}
</Link>
</h2>
{post.excerpt && (
<p className="text-sm text-zinc-400 leading-relaxed line-clamp-2">
{post.excerpt}
</p>
)}
<Link
href={`/guides/${post.slug.current}`}
className="mt-1 self-start text-xs font-medium text-amber-400 hover:text-amber-300"
>
Read guide
</Link>
</div>
</article>
))}
</div>
)}
<div className="mt-12 rounded-lg border border-zinc-800 bg-zinc-950/60 p-5 text-center">
<p className="text-sm text-zinc-400">
Ready to start building?{' '}
<Link href="/builder" className="font-semibold text-amber-400 hover:text-amber-300">
Open the Builder
</Link>
</p>
</div>
</main>
)
}
+2 -2
View File
@@ -2,7 +2,7 @@
import type { ReactNode } from "react";
import { Suspense } from "react";
import { BuilderNav } from "@/components/BuilderNav";
import { BuilderNavConditional } from "@/components/BuilderNavConditional";
import { TopNav } from "@/components/TopNav";
import { Footer } from "@/components/Footer";
@@ -11,7 +11,7 @@ export default function BuilderLayout({ children }: { children: ReactNode }) {
<div className="min-h-screen bg-white text-zinc-900 dark:bg-neutral-950 dark:text-zinc-50">
<TopNav />
<Suspense fallback={<div className="h-[52px]" />}>
<BuilderNav />
<BuilderNavConditional />
</Suspense>
<main className="min-h-screen">{children}</main>
<Footer />
+1 -4
View File
@@ -13,7 +13,7 @@ export default function NotFound() {
</h1>
<p className="mt-3 text-sm text-zinc-400">
This area of the site isnt publicly accessible yet.
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<div className="mt-6 flex justify-center gap-3">
@@ -32,9 +32,6 @@ export default function NotFound() {
</a> */}
</div>
<p className="mt-6 text-[11px] text-zinc-600">
Early access features are launching soon.
</p>
</div>
</main>
);