first commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { loadContent } from '@/lib/content/load';
|
||||
import { loadPost } from '@/lib/content/posts-store';
|
||||
import { BlogArticle } from '@/components/blog/BlogArticle';
|
||||
|
||||
// Live content: bodies and card meta both come from the CMS-merged tree, so
|
||||
// admin edits show immediately. (No generateStaticParams — render on demand.)
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
export function generateMetadata({ params }: { params: Params }) {
|
||||
const { en } = loadContent();
|
||||
const post = en.blog.items.find((p) => p.slug === params.slug);
|
||||
if (!post) return {};
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
openGraph: { title: post.title, description: post.excerpt, type: 'article' },
|
||||
alternates: {
|
||||
canonical: `/blog/${post.slug}`,
|
||||
languages: {
|
||||
'fa-IR': `/blog/${post.slug}`,
|
||||
'en-US': `/blog/${post.slug}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function BlogPostPage({ params }: { params: Params }) {
|
||||
const content = loadPost(params.slug);
|
||||
const { en: enContent, fa: faContent } = loadContent();
|
||||
const en = enContent.blog.items.find((p) => p.slug === params.slug);
|
||||
const fa = faContent.blog.items.find((p) => p.slug === params.slug);
|
||||
if (!content || !en || !fa) notFound();
|
||||
|
||||
return (
|
||||
<BlogArticle
|
||||
content={content}
|
||||
meta={{
|
||||
en: { title: en.title, category: en.category, readTime: en.readTime },
|
||||
fa: { title: fa.title, category: fa.category, readTime: fa.readTime },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { LocaleProvider } from '@/lib/i18n/locale-context';
|
||||
import { loadContent } from '@/lib/content/load';
|
||||
import { Navbar } from '@/components/nav/Navbar';
|
||||
import { CustomCursor } from '@/components/ui/CustomCursor';
|
||||
|
||||
/**
|
||||
* Public site shell. Reads the live content tree (dict defaults merged with
|
||||
* any admin overrides) on every request so edits made in the panel appear
|
||||
* immediately, then feeds it to the client-side LocaleProvider.
|
||||
*/
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function SiteLayout({ children }: { children: React.ReactNode }) {
|
||||
const content = loadContent();
|
||||
|
||||
return (
|
||||
<LocaleProvider content={content}>
|
||||
{/* Ambient backdrop */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 -z-10 bg-radial-aurora"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 -z-10 bg-grid-faint bg-grid opacity-40"
|
||||
style={{
|
||||
maskImage:
|
||||
'radial-gradient(ellipse at center, black 30%, transparent 75%)',
|
||||
WebkitMaskImage:
|
||||
'radial-gradient(ellipse at center, black 30%, transparent 75%)',
|
||||
}}
|
||||
/>
|
||||
<CustomCursor />
|
||||
<Navbar />
|
||||
<main>{children}</main>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Hero } from '@/components/hero/Hero';
|
||||
import { Services } from '@/components/sections/Services';
|
||||
import { DataFlow } from '@/components/sections/DataFlow';
|
||||
import { Stack } from '@/components/sections/Stack';
|
||||
import { Expertise } from '@/components/sections/Expertise';
|
||||
import { Portfolio } from '@/components/sections/Portfolio';
|
||||
import { Blog } from '@/components/sections/Blog';
|
||||
import { Contact } from '@/components/sections/Contact';
|
||||
import { Footer } from '@/components/sections/Footer';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Services />
|
||||
<DataFlow />
|
||||
<Stack />
|
||||
<Expertise />
|
||||
<Portfolio />
|
||||
<Blog />
|
||||
<Contact />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { dict, SERVICE_IDS, type ServiceId } from '@/lib/i18n/dictionaries';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
export function generateStaticParams() {
|
||||
return SERVICE_IDS.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export function generateMetadata({ params }: { params: Params }) {
|
||||
const id = params.slug as ServiceId;
|
||||
const fa = dict.fa.services.items.find((s) => s.id === id);
|
||||
const en = dict.en.services.items.find((s) => s.id === id);
|
||||
if (!en) return {};
|
||||
return {
|
||||
title: en.title,
|
||||
description: en.description,
|
||||
openGraph: { title: en.title, description: en.description },
|
||||
alternates: { canonical: `/services/${id}`, languages: { 'fa-IR': `/services/${id}`, 'en-US': `/services/${id}` } },
|
||||
other: { 'fa-title': fa?.title ?? '' },
|
||||
};
|
||||
}
|
||||
|
||||
export default function ServiceDetailPage({ params }: { params: Params }) {
|
||||
const id = params.slug as ServiceId;
|
||||
if (!SERVICE_IDS.includes(id)) notFound();
|
||||
|
||||
const en = dict.en.services.items.find((s) => s.id === id)!;
|
||||
const fa = dict.fa.services.items.find((s) => s.id === id)!;
|
||||
|
||||
return (
|
||||
<article className="relative px-5 py-32 sm:px-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Link
|
||||
href="/#services"
|
||||
className="label-mono inline-flex items-center gap-2 text-slate-400 hover:text-electric"
|
||||
>
|
||||
← {dict.en.nav.services}
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-6 font-display text-[clamp(2rem,4.5vw,3.4rem)] font-extrabold leading-tight text-white">
|
||||
{en.title}
|
||||
</h1>
|
||||
<p
|
||||
dir="rtl"
|
||||
className="mt-2 font-fa text-[clamp(1.1rem,2vw,1.5rem)] text-slate-400"
|
||||
>
|
||||
{fa.title}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-2">
|
||||
{en.tags.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded-full border border-electric/30 bg-electric/5 px-3 py-1 font-mono text-xs text-electric"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-10 text-[1.05rem] leading-relaxed text-slate-300">
|
||||
{en.description}
|
||||
</p>
|
||||
<p
|
||||
dir="rtl"
|
||||
className="mt-6 font-fa text-[1rem] leading-loose text-slate-400"
|
||||
>
|
||||
{fa.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-12 flex flex-wrap gap-3">
|
||||
<Link href="/#contact" className="btn-primary">
|
||||
Book a consultation
|
||||
</Link>
|
||||
<Link href="/#services" className="btn-ghost">
|
||||
All services
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user