457 lines
17 KiB
TypeScript
457 lines
17 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import Layout from "@/components/Layout";
|
|
import LoadingScreen from "@/components/LoadingScreen";
|
|
import { useAethexToast } from "@/hooks/use-aethex-toast";
|
|
import BlogHero from "@/components/blog/BlogHero";
|
|
import BlogTrendingRail from "@/components/blog/BlogTrendingRail";
|
|
import BlogCategoryChips from "@/components/blog/BlogCategoryChips";
|
|
import BlogPostGrid from "@/components/blog/BlogPostGrid";
|
|
import BlogNewsletterSection from "@/components/blog/BlogNewsletterSection";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ArrowRight, Layers, ListFilter, Newspaper } from "lucide-react";
|
|
import type { BlogCategory, BlogPost } from "@/components/blog/types";
|
|
|
|
const buildSlug = (post: BlogPost): string => post.slug || post.id?.toString() || "article";
|
|
|
|
const normalizeCategory = (value?: string | null) =>
|
|
(value || "general")
|
|
.toString()
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-");
|
|
|
|
const Blog = () => {
|
|
const toast = useAethexToast();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [posts, setPosts] = useState<BlogPost[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [selectedCategory, setSelectedCategory] = useState("all");
|
|
|
|
const staticPosts = useMemo<BlogPost[]>(
|
|
() => [
|
|
{
|
|
id: "static-0",
|
|
slug: "shipping-aethex-multiverse-events",
|
|
title: "Shipping AeThex Multiverse Events: Behind the Scenes",
|
|
excerpt:
|
|
"How the AeThex LiveOps squad orchestrated cross-platform events with zero downtime.",
|
|
author: "Sofia Alvarez",
|
|
date: "January 18, 2025",
|
|
readTime: "8 min read",
|
|
category: "Company News",
|
|
likes: 254,
|
|
comments: 44,
|
|
trending: true,
|
|
image:
|
|
"https://images.unsplash.com/photo-1529101091764-c3526daf38fe?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-1",
|
|
slug: "aethex-guilds-collaboration-toolkit",
|
|
title: "AeThex Guilds Collaboration Toolkit Launch",
|
|
excerpt:
|
|
"New creator guild features help communities prototype faster and ship together.",
|
|
author: "Priya Desai",
|
|
date: "January 16, 2025",
|
|
readTime: "7 min read",
|
|
category: "Product Updates",
|
|
likes: 311,
|
|
comments: 59,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1526402464885-402318f32905?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-2",
|
|
slug: "horizon-engine-2-0-preview",
|
|
title: "Horizon Engine 2.0 Preview: Performance Notes",
|
|
excerpt:
|
|
"Peek into the renderer upgrades, tooling, and ergonomics coming to Horizon Engine 2.0.",
|
|
author: "Liam Hart",
|
|
date: "January 14, 2025",
|
|
readTime: "9 min read",
|
|
category: "Technology",
|
|
likes: 368,
|
|
comments: 71,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-3",
|
|
slug: "community-creators-spotlight-january",
|
|
title: "Community Creators Spotlight — January Edition",
|
|
excerpt:
|
|
"Celebrating five AeThex creators pushing storytelling, accessibility, and esports forward.",
|
|
author: "AeThex Community Team",
|
|
date: "January 13, 2025",
|
|
readTime: "6 min read",
|
|
category: "Community",
|
|
likes: 427,
|
|
comments: 95,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1522199710521-72d69614c702?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-4",
|
|
slug: "building-scalable-game-architecture",
|
|
title: "Building Scalable Game Architecture with Microservices",
|
|
excerpt:
|
|
"Learn how to design game backends that can handle millions of concurrent players using modern microservices patterns.",
|
|
author: "Marcus Rodriguez",
|
|
date: "December 12, 2024",
|
|
readTime: "6 min read",
|
|
category: "Technology",
|
|
likes: 89,
|
|
comments: 15,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-5",
|
|
slug: "advanced-unity-optimization-techniques",
|
|
title: "Advanced Unity Optimization Techniques",
|
|
excerpt:
|
|
"Performance optimization strategies that can boost your Unity game's frame rate by up to 300%.",
|
|
author: "Alex Thompson",
|
|
date: "December 10, 2024",
|
|
readTime: "12 min read",
|
|
category: "Tutorials",
|
|
likes: 156,
|
|
comments: 34,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1527443224154-dcc0707b462b?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-6",
|
|
slug: "aethex-labs-neural-network-compression",
|
|
title: "AeThex Labs: Breakthrough in Neural Network Compression",
|
|
excerpt:
|
|
"Our research team achieves 90% model size reduction while maintaining accuracy for mobile game AI.",
|
|
author: "Dr. Aisha Patel",
|
|
date: "December 8, 2024",
|
|
readTime: "5 min read",
|
|
category: "Research",
|
|
likes: 203,
|
|
comments: 41,
|
|
trending: true,
|
|
image:
|
|
"https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-7",
|
|
slug: "introducing-aethex-cloud-gaming-platform",
|
|
title: "Introducing AeThex Cloud Gaming Platform",
|
|
excerpt:
|
|
"Launch games instantly across any device with our new cloud gaming infrastructure and global CDN.",
|
|
author: "AeThex Team",
|
|
date: "December 5, 2024",
|
|
readTime: "4 min read",
|
|
category: "Company News",
|
|
likes: 278,
|
|
comments: 52,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1511512578047-dfb367046420?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-8",
|
|
slug: "real-time-ray-tracing-in-web-games",
|
|
title: "Real-time Ray Tracing in Web Games",
|
|
excerpt:
|
|
"Tutorial: Implementing hardware-accelerated ray tracing in browser-based games using WebGPU.",
|
|
author: "Jordan Kim",
|
|
date: "December 3, 2024",
|
|
readTime: "15 min read",
|
|
category: "Tutorials",
|
|
likes: 94,
|
|
comments: 18,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1489515217757-5fd1be406fef?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
{
|
|
id: "static-9",
|
|
slug: "the-evolution-of-game-ai",
|
|
title: "The Evolution of Game AI: From Scripts to Neural Networks",
|
|
excerpt:
|
|
"A comprehensive look at how artificial intelligence in games has evolved and where it's heading next.",
|
|
author: "Dr. Michael Chen",
|
|
date: "December 1, 2024",
|
|
readTime: "10 min read",
|
|
category: "Technology",
|
|
likes: 167,
|
|
comments: 29,
|
|
trending: false,
|
|
image:
|
|
"https://images.unsplash.com/photo-1523580846011-d3a5bc25702b?auto=format&fit=crop&w=1200&q=80",
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const res = await fetch("/api/blog?limit=50");
|
|
let data: any = [];
|
|
try {
|
|
if (res.ok) {
|
|
data = await res.json();
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to parse blog API response, falling back to Supabase", error);
|
|
}
|
|
|
|
if ((!Array.isArray(data) || !data.length) && import.meta.env.VITE_SUPABASE_URL && import.meta.env.VITE_SUPABASE_ANON_KEY) {
|
|
try {
|
|
const sbUrl = import.meta.env.VITE_SUPABASE_URL.replace(/\/$/, "");
|
|
const url = `${sbUrl}/rest/v1/blog_posts?select=slug,title,excerpt,author,date,read_time,category,image,likes,comments,published_at&order=published_at.desc&limit=50`;
|
|
const fallbackRes = await fetch(url, {
|
|
headers: {
|
|
apikey: import.meta.env.VITE_SUPABASE_ANON_KEY,
|
|
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_ANON_KEY}`,
|
|
},
|
|
});
|
|
if (fallbackRes.ok) {
|
|
data = await fallbackRes.json();
|
|
}
|
|
} catch (error) {
|
|
console.warn("Supabase fallback failed", error);
|
|
}
|
|
}
|
|
|
|
if (!cancelled && Array.isArray(data)) {
|
|
const mapped: BlogPost[] = data.map((record: any) => ({
|
|
id: record.id ?? record.slug,
|
|
slug: record.slug,
|
|
title: record.title,
|
|
excerpt: record.excerpt ?? record.summary ?? null,
|
|
author: record.author ?? "AeThex Team",
|
|
date: record.date ?? record.published_at,
|
|
readTime: record.read_time ?? record.readTime ?? null,
|
|
category: record.category ?? "General",
|
|
image: record.image ?? null,
|
|
likes: typeof record.likes === "number" ? record.likes : null,
|
|
comments: typeof record.comments === "number" ? record.comments : null,
|
|
trending: Boolean(record.trending) || (typeof record.likes === "number" && record.likes > 250),
|
|
body: record.body_html ?? record.body ?? null,
|
|
}));
|
|
setPosts(mapped);
|
|
}
|
|
} catch (error) {
|
|
console.warn("Blog fetch failed", error);
|
|
toast.system("Loaded curated AeThex articles");
|
|
} finally {
|
|
if (!cancelled) {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [toast]);
|
|
|
|
const dataset = posts.length ? posts : staticPosts;
|
|
|
|
const filteredPosts = useMemo(() => {
|
|
const query = searchQuery.trim().toLowerCase();
|
|
return dataset.filter((post) => {
|
|
const matchesCategory =
|
|
selectedCategory === "all" || normalizeCategory(post.category) === selectedCategory;
|
|
if (!matchesCategory) return false;
|
|
|
|
if (!query) return true;
|
|
const haystack = [post.title, post.excerpt, post.author]
|
|
.filter(Boolean)
|
|
.map((value) => value!.toLowerCase())
|
|
.join(" ");
|
|
return haystack.includes(query);
|
|
});
|
|
}, [dataset, selectedCategory, searchQuery]);
|
|
|
|
const featuredPost = useMemo(() => {
|
|
if (!filteredPosts.length) {
|
|
return dataset.find((post) => post.trending) ?? dataset[0] ?? null;
|
|
}
|
|
return filteredPosts.find((post) => post.trending) ?? filteredPosts[0] ?? null;
|
|
}, [dataset, filteredPosts]);
|
|
|
|
const displayedPosts = useMemo(() => {
|
|
if (!featuredPost) return filteredPosts;
|
|
return filteredPosts.filter((post) => buildSlug(post) !== buildSlug(featuredPost));
|
|
}, [filteredPosts, featuredPost]);
|
|
|
|
const trendingPosts = useMemo(() => {
|
|
const sorted = [...dataset]
|
|
.filter((post) => post.trending || (post.likes ?? 0) >= 200)
|
|
.sort((a, b) => (b.likes ?? 0) - (a.likes ?? 0));
|
|
return sorted.slice(0, 3);
|
|
}, [dataset]);
|
|
|
|
const categories: BlogCategory[] = useMemo(() => {
|
|
const counts = new Map<string, BlogCategory>();
|
|
dataset.forEach((post) => {
|
|
const id = normalizeCategory(post.category);
|
|
const name = post.category || "General";
|
|
counts.set(id, {
|
|
id,
|
|
name,
|
|
count: (counts.get(id)?.count ?? 0) + 1,
|
|
});
|
|
});
|
|
|
|
const ordered = [
|
|
{ id: "all", name: "All posts", count: dataset.length },
|
|
...Array.from(counts.values()).sort((a, b) => b.count - a.count),
|
|
];
|
|
|
|
return ordered;
|
|
}, [dataset]);
|
|
|
|
const insights = useMemo(
|
|
() => [
|
|
{
|
|
label: "Teams publishing",
|
|
value: new Set(dataset.map((post) => (post.author || "AeThex Team").split(" ")[0])).size,
|
|
helper: "Active contributors this month",
|
|
icon: <Layers className="h-4 w-4" />,
|
|
},
|
|
{
|
|
label: "Focus areas",
|
|
value: new Set(dataset.map((post) => post.category || "General")).size,
|
|
helper: "Distinct categories covered",
|
|
icon: <ListFilter className="h-4 w-4" />,
|
|
},
|
|
{
|
|
label: "Stories published",
|
|
value: dataset.length,
|
|
helper: "All-time AeThex blog posts",
|
|
icon: <Newspaper className="h-4 w-4" />,
|
|
},
|
|
],
|
|
[dataset],
|
|
);
|
|
|
|
if (isLoading) {
|
|
return <LoadingScreen message="Loading AeThex blog" showProgress />;
|
|
}
|
|
|
|
const handleResetFilters = () => {
|
|
setSelectedCategory("all");
|
|
setSearchQuery("");
|
|
};
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="bg-slate-950 text-foreground">
|
|
<BlogHero
|
|
featured={featuredPost}
|
|
totalCount={dataset.length}
|
|
search={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
onViewAll={handleResetFilters}
|
|
/>
|
|
|
|
<section className="border-b border-border/30 bg-background/60 py-12">
|
|
<div className="container mx-auto px-4">
|
|
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="space-y-2">
|
|
<p className="text-xs uppercase tracking-[0.4em] text-muted-foreground">
|
|
Filter by track
|
|
</p>
|
|
<h2 className="text-2xl font-semibold text-white">Navigate the AeThex knowledge graph</h2>
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={handleResetFilters} className="self-start lg:self-auto">
|
|
Reset filters
|
|
</Button>
|
|
</div>
|
|
<div className="mt-6">
|
|
<BlogCategoryChips
|
|
categories={categories}
|
|
selected={selectedCategory}
|
|
onSelect={setSelectedCategory}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<BlogTrendingRail posts={trendingPosts} />
|
|
|
|
<section className="border-b border-border/30 bg-background/80 py-16">
|
|
<div className="container mx-auto grid gap-6 px-4 md:grid-cols-3">
|
|
{insights.map((insight) => (
|
|
<Card
|
|
key={insight.label}
|
|
className="border-border/40 bg-background/60 backdrop-blur transition hover:border-aethex-400/50"
|
|
>
|
|
<CardContent className="flex items-center gap-4 p-6">
|
|
<span className="flex h-12 w-12 items-center justify-center rounded-full border border-border/30 bg-background/70 text-aethex-200">
|
|
{insight.icon}
|
|
</span>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">{insight.label}</p>
|
|
<p className="text-2xl font-semibold text-white">{insight.value}</p>
|
|
<p className="text-xs text-muted-foreground">{insight.helper}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="py-20">
|
|
<div className="container mx-auto space-y-12 px-4">
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
<div className="space-y-2">
|
|
<p className="text-xs uppercase tracking-[0.4em] text-muted-foreground">Latest updates</p>
|
|
<h2 className="text-3xl font-semibold text-white">Fresh from the AeThex ship room</h2>
|
|
</div>
|
|
<Button asChild variant="outline" className="self-start border-border/60 text-sm">
|
|
<Link to="/changelog">
|
|
View changelog
|
|
<ArrowRight className="ml-2 h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<BlogPostGrid posts={displayedPosts} />
|
|
</div>
|
|
</section>
|
|
|
|
<BlogNewsletterSection />
|
|
|
|
<section className="bg-background/70 py-16">
|
|
<div className="container mx-auto px-4">
|
|
<div className="rounded-2xl border border-border/40 bg-background/80 p-8">
|
|
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
|
<div className="space-y-2">
|
|
<p className="text-xs uppercase tracking-[0.4em] text-muted-foreground">Explore more</p>
|
|
<h3 className="text-2xl font-semibold text-white">Dive into AeThex documentation</h3>
|
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
|
Looking for implementation guides, deployment recipes, or program onboarding materials?
|
|
Visit our documentation hub for developer tutorials, platform references, and community playbooks.
|
|
</p>
|
|
</div>
|
|
<Button asChild className="bg-gradient-to-r from-aethex-500 to-neon-blue">
|
|
<Link to="/docs">Open documentation hub</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default Blog;
|