Skip to content

Commit 97e6e5b

Browse files
committed
feat: complete frontend build
- Root layout, fonts, and global styles - UI primitives: Button, Badge, Input, Textarea, Card - Navbar and Footer with brand assets - Home page: Hero, Products, Latest Releases, Hackathon strip - About page - Products list and individual product pages - Blog/Updates list and individual post pages - Team page - Careers page with application form - Contact page with contact form - 404 page - Admin dashboard: all CRUD pages, TipTap editor, image upload
1 parent eb27a11 commit 97e6e5b

54 files changed

Lines changed: 4688 additions & 448 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 136 additions & 190 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 136 additions & 190 deletions
Large diffs are not rendered by default.

src/app/(public)/about/page.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,98 @@
1+
import Link from "next/link";
2+
import Button from "@/components/ui/Button";
3+
4+
const companyFacts = [
5+
{ label: "Founded", value: "March 2026" },
6+
{ label: "Registered", value: "RC 9426867, Nigeria" },
7+
{ label: "Location", value: "Lagos, Nigeria" },
8+
{ label: "Team", value: "3 founders, all full-stack engineers" },
9+
];
10+
111
export default function AboutPage() {
2-
return null;
12+
return (
13+
<main className="bg-white">
14+
<section className="py-24 md:py-32">
15+
<div className="mx-auto max-w-5xl px-6">
16+
<div className="max-w-4xl space-y-6">
17+
<h1 className="font-mono text-4xl font-bold leading-[1.1] text-[#121F38] md:text-[56px]">
18+
About CodedDevs
19+
</h1>
20+
<p className="max-w-3xl font-sans text-lg leading-[1.75] text-[#2C3A52]">
21+
We build AI-first software products for African markets — from
22+
first principles.
23+
</p>
24+
</div>
25+
</div>
26+
</section>
27+
28+
<section className="py-16 md:py-24">
29+
<div className="mx-auto grid max-w-5xl gap-10 px-6 md:grid-cols-[280px_1fr]">
30+
<h2 className="font-mono text-3xl font-bold leading-[1.2] text-[#121F38] md:text-[40px]">
31+
What We&apos;re Building
32+
</h2>
33+
<div className="space-y-6 font-sans text-lg leading-[1.75] text-[#2C3A52]">
34+
<p>
35+
CodedDevs Technology LTD is an African startup building AI
36+
products and open-source tools that help businesses grow, scale,
37+
and operate more efficiently.
38+
</p>
39+
<p>
40+
We develop a portfolio of products across social commerce,
41+
e-commerce, and AI-driven tools — designed to enable seamless
42+
transactions, stronger digital presence, and smarter business
43+
growth.
44+
</p>
45+
</div>
46+
</div>
47+
</section>
48+
49+
<section className="py-16 md:py-24">
50+
<div className="mx-auto grid max-w-5xl gap-10 px-6 md:grid-cols-[280px_1fr]">
51+
<h2 className="font-mono text-3xl font-bold leading-[1.2] text-[#121F38] md:text-[40px]">
52+
How We Build
53+
</h2>
54+
<div className="space-y-6 font-sans text-lg leading-[1.75] text-[#2C3A52]">
55+
<p>
56+
Our approach is AI-first and deeply focused on the African
57+
market. We solve real-world challenges unique to the region — not
58+
by adapting tools built elsewhere, but by engineering from first
59+
principles.
60+
</p>
61+
<p>
62+
We build in the open. Our products are open-source because we
63+
believe the best software for Africa should be built with Africa.
64+
</p>
65+
</div>
66+
</div>
67+
</section>
68+
69+
<section className="py-16 md:py-24">
70+
<div className="mx-auto max-w-5xl px-6">
71+
<div className="grid gap-6 md:grid-cols-4">
72+
{companyFacts.map((fact) => (
73+
<div key={fact.label} className="space-y-2">
74+
<p className="font-sans text-xs font-medium uppercase text-[#6B7896]">
75+
{fact.label}
76+
</p>
77+
<p className="font-sans text-base leading-[1.7] text-[#121F38]">
78+
{fact.value}
79+
</p>
80+
</div>
81+
))}
82+
</div>
83+
</div>
84+
</section>
85+
86+
<section className="bg-[#F4F5F8] py-16 md:py-20">
87+
<div className="mx-auto flex max-w-5xl flex-col gap-6 px-6 md:flex-row md:items-center md:justify-between">
88+
<h2 className="font-mono text-2xl font-semibold leading-[1.3] text-[#121F38]">
89+
Meet the team behind CodedDevs
90+
</h2>
91+
<Button asChild>
92+
<Link href="/team">Meet the Team</Link>
93+
</Button>
94+
</div>
95+
</section>
96+
</main>
97+
);
398
}
Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,133 @@
1-
export default function BlogPostPage() {
2-
return null;
1+
import type { Metadata } from "next";
2+
import Image from "next/image";
3+
import { notFound } from "next/navigation";
4+
import { and, desc, eq } from "drizzle-orm";
5+
import PostContent, {
6+
type TiptapJson,
7+
} from "@/components/blog/PostContent";
8+
import Badge from "@/components/ui/Badge";
9+
import { blogPosts, db } from "@/db";
10+
11+
type UpdatePageProps = {
12+
params: {
13+
slug: string;
14+
};
15+
};
16+
17+
function formatDate(date: Date | null) {
18+
if (!date) {
19+
return "Unscheduled";
20+
}
21+
22+
return new Intl.DateTimeFormat("en", {
23+
month: "long",
24+
day: "numeric",
25+
year: "numeric",
26+
}).format(date);
27+
}
28+
29+
function isTiptapJson(value: unknown): value is TiptapJson {
30+
return typeof value === "object" && value !== null && !Array.isArray(value);
31+
}
32+
33+
async function getPublishedPostBySlug(slug: string) {
34+
try {
35+
const [post] = await db
36+
.select()
37+
.from(blogPosts)
38+
.where(and(eq(blogPosts.slug, slug), eq(blogPosts.is_published, true)))
39+
.limit(1);
40+
41+
return post ?? null;
42+
} catch (error) {
43+
console.error("Failed to fetch update", error);
44+
return null;
45+
}
46+
}
47+
48+
export async function generateStaticParams() {
49+
try {
50+
const postSlugs = await db
51+
.select({ slug: blogPosts.slug })
52+
.from(blogPosts)
53+
.where(eq(blogPosts.is_published, true))
54+
.orderBy(desc(blogPosts.published_at));
55+
56+
return postSlugs;
57+
} catch (error) {
58+
console.error("Failed to generate update params", error);
59+
return [];
60+
}
61+
}
62+
63+
export async function generateMetadata({
64+
params,
65+
}: UpdatePageProps): Promise<Metadata> {
66+
const post = await getPublishedPostBySlug(params.slug);
67+
68+
if (!post) {
69+
return {
70+
title: "Update \u2014 CodedDevs Updates",
71+
};
72+
}
73+
74+
return {
75+
title: `${post.title} \u2014 CodedDevs Updates`,
76+
description: post.excerpt,
77+
};
78+
}
79+
80+
export default async function UpdatePage({ params }: UpdatePageProps) {
81+
const post = await getPublishedPostBySlug(params.slug);
82+
83+
if (!post) {
84+
notFound();
85+
}
86+
87+
const content = isTiptapJson(post.content)
88+
? post.content
89+
: { type: "doc", content: [] };
90+
91+
return (
92+
<main className="bg-white">
93+
<article>
94+
<header className="py-24 md:py-32">
95+
<div className="mx-auto max-w-5xl px-6">
96+
<div className="max-w-4xl space-y-6">
97+
<Badge>{post.category}</Badge>
98+
<h1 className="font-mono text-4xl font-bold leading-[1.1] text-[#121F38] md:text-[56px]">
99+
{post.title}
100+
</h1>
101+
<p className="font-sans text-sm leading-[1.6] text-[#6B7896]">
102+
{post.author} - {formatDate(post.published_at)}
103+
</p>
104+
</div>
105+
</div>
106+
</header>
107+
108+
{post.cover_url ? (
109+
<section className="pb-16">
110+
<div className="mx-auto max-w-5xl px-6">
111+
<div className="relative aspect-[16/9] overflow-hidden rounded-lg bg-[#F4F5F8]">
112+
<Image
113+
src={post.cover_url}
114+
alt={post.title}
115+
fill
116+
sizes="(min-width: 1024px) 1024px, 100vw"
117+
className="object-cover"
118+
priority
119+
/>
120+
</div>
121+
</div>
122+
</section>
123+
) : null}
124+
125+
<section className="pb-24 md:pb-32">
126+
<div className="mx-auto max-w-5xl px-6">
127+
<PostContent content={content} />
128+
</div>
129+
</section>
130+
</article>
131+
</main>
132+
);
3133
}

src/app/(public)/blog/page.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,61 @@
1-
export default function BlogPage() {
2-
return null;
1+
import { desc, eq } from "drizzle-orm";
2+
import UpdatesList, {
3+
type UpdateListPost,
4+
} from "@/components/blog/UpdatesList";
5+
import { blogPosts, db } from "@/db";
6+
7+
export const dynamic = "force-dynamic";
8+
9+
async function getPublishedPosts(): Promise<UpdateListPost[]> {
10+
try {
11+
const posts = await db
12+
.select({
13+
id: blogPosts.id,
14+
slug: blogPosts.slug,
15+
title: blogPosts.title,
16+
excerpt: blogPosts.excerpt,
17+
author: blogPosts.author,
18+
category: blogPosts.category,
19+
published_at: blogPosts.published_at,
20+
})
21+
.from(blogPosts)
22+
.where(eq(blogPosts.is_published, true))
23+
.orderBy(desc(blogPosts.published_at));
24+
25+
return posts.map((post) => ({
26+
...post,
27+
published_at: post.published_at?.toISOString() ?? null,
28+
}));
29+
} catch (error) {
30+
console.error("Failed to fetch updates", error);
31+
return [];
32+
}
33+
}
34+
35+
export default async function UpdatesPage() {
36+
const posts = await getPublishedPosts();
37+
38+
return (
39+
<main className="bg-white">
40+
<section className="py-24 md:py-32">
41+
<div className="mx-auto max-w-5xl px-6">
42+
<div className="max-w-3xl space-y-6">
43+
<h1 className="font-mono text-4xl font-bold leading-[1.1] text-[#121F38] md:text-[56px]">
44+
Updates
45+
</h1>
46+
<p className="font-sans text-lg leading-[1.75] text-[#2C3A52]">
47+
Product updates, announcements, and stories from the CodedDevs
48+
team.
49+
</p>
50+
</div>
51+
</div>
52+
</section>
53+
54+
<section className="pb-24 md:pb-32">
55+
<div className="mx-auto max-w-5xl px-6">
56+
<UpdatesList posts={posts} />
57+
</div>
58+
</section>
59+
</main>
60+
);
361
}

0 commit comments

Comments
 (0)