Skip to content

Commit 7c1cc16

Browse files
Merge pull request #2 from coded-devs/feature/frontend
feat: complete frontend build
2 parents dbcdf97 + efa94c7 commit 7c1cc16

55 files changed

Lines changed: 4806 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: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,136 @@
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+
if (process.env.CI === "true") {
50+
return [];
51+
}
52+
53+
try {
54+
const postSlugs = await db
55+
.select({ slug: blogPosts.slug })
56+
.from(blogPosts)
57+
.where(eq(blogPosts.is_published, true))
58+
.orderBy(desc(blogPosts.published_at));
59+
60+
return postSlugs;
61+
} catch {
62+
return [];
63+
}
64+
}
65+
66+
export async function generateMetadata({
67+
params,
68+
}: UpdatePageProps): Promise<Metadata> {
69+
const post = await getPublishedPostBySlug(params.slug);
70+
71+
if (!post) {
72+
return {
73+
title: "Update \u2014 CodedDevs Updates",
74+
};
75+
}
76+
77+
return {
78+
title: `${post.title} \u2014 CodedDevs Updates`,
79+
description: post.excerpt,
80+
};
81+
}
82+
83+
export default async function UpdatePage({ params }: UpdatePageProps) {
84+
const post = await getPublishedPostBySlug(params.slug);
85+
86+
if (!post) {
87+
notFound();
88+
}
89+
90+
const content = isTiptapJson(post.content)
91+
? post.content
92+
: { type: "doc", content: [] };
93+
94+
return (
95+
<main className="bg-white">
96+
<article>
97+
<header className="py-24 md:py-32">
98+
<div className="mx-auto max-w-5xl px-6">
99+
<div className="max-w-4xl space-y-6">
100+
<Badge>{post.category}</Badge>
101+
<h1 className="font-mono text-4xl font-bold leading-[1.1] text-[#121F38] md:text-[56px]">
102+
{post.title}
103+
</h1>
104+
<p className="font-sans text-sm leading-[1.6] text-[#6B7896]">
105+
{post.author} - {formatDate(post.published_at)}
106+
</p>
107+
</div>
108+
</div>
109+
</header>
110+
111+
{post.cover_url ? (
112+
<section className="pb-16">
113+
<div className="mx-auto max-w-5xl px-6">
114+
<div className="relative aspect-[16/9] overflow-hidden rounded-lg bg-[#F4F5F8]">
115+
<Image
116+
src={post.cover_url}
117+
alt={post.title}
118+
fill
119+
sizes="(min-width: 1024px) 1024px, 100vw"
120+
className="object-cover"
121+
priority
122+
/>
123+
</div>
124+
</div>
125+
</section>
126+
) : null}
127+
128+
<section className="pb-24 md:pb-32">
129+
<div className="mx-auto max-w-5xl px-6">
130+
<PostContent content={content} />
131+
</div>
132+
</section>
133+
</article>
134+
</main>
135+
);
3136
}

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)