Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ export const metadata: Metadata = {
description:
"A student-run nonprofit making tech education free and accessible for everyone, everywhere.",
alternates: { canonical: "/about/" },
openGraph: {
title: "About Us | CodeWithPurpose",
description:
"A student-run nonprofit making tech education free and accessible for everyone, everywhere.",
url: "/about/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "About Us | CodeWithPurpose",
description:
"A student-run nonprofit making tech education free and accessible for everyone, everywhere.",
},
};

const founders: TeamMember[] = [
Expand Down
11 changes: 11 additions & 0 deletions src/app/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export const metadata: Metadata = {
title: "Contact",
description: "Get in touch with the CodeWithPurpose team.",
alternates: { canonical: "/contact/" },
openGraph: {
title: "Contact | CodeWithPurpose",
description: "Get in touch with the CodeWithPurpose team.",
url: "/contact/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Contact | CodeWithPurpose",
description: "Get in touch with the CodeWithPurpose team.",
},
};

export default function ContactPage() {
Expand Down
13 changes: 13 additions & 0 deletions src/app/courses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ export const metadata: Metadata = {
description:
"Free courses and interactive lessons for students worldwide. Python, Vibecoding, Machine Learning, Financial Literacy, and Health in Tech.",
alternates: { canonical: "/courses/" },
openGraph: {
title: "Free Coding Courses | CodeWithPurpose",
description:
"Free courses and interactive lessons for students worldwide. Python, Vibecoding, Machine Learning, Financial Literacy, and Health in Tech.",
url: "/courses/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Free Coding Courses | CodeWithPurpose",
description:
"Free courses and interactive lessons for students worldwide. Python, Vibecoding, Machine Learning, Financial Literacy, and Health in Tech.",
},
};

interface Course {
Expand Down
13 changes: 13 additions & 0 deletions src/app/donate/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export const metadata: Metadata = {
description:
"Support CodeWithPurpose. Every dollar helps a student start their coding journey today.",
alternates: { canonical: "/donate/" },
openGraph: {
title: "Donate | CodeWithPurpose",
description:
"Support CodeWithPurpose. Every dollar helps a student start their coding journey today.",
url: "/donate/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Donate | CodeWithPurpose",
description:
"Support CodeWithPurpose. Every dollar helps a student start their coding journey today.",
},
};

const impactAreas = [
Expand Down
13 changes: 13 additions & 0 deletions src/app/impact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ export const metadata: Metadata = {
description:
"5,000+ students across 150+ countries, and the stories behind the numbers.",
alternates: { canonical: "/impact/" },
openGraph: {
title: "Impact & Stories | CodeWithPurpose",
description:
"5,000+ students across 150+ countries, and the stories behind the numbers.",
url: "/impact/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Impact & Stories | CodeWithPurpose",
description:
"5,000+ students across 150+ countries, and the stories behind the numbers.",
},
};

const stats = [
Expand Down
13 changes: 13 additions & 0 deletions src/app/join/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ export const metadata: Metadata = {
description:
"Volunteer with CodeWithPurpose. Teach, mentor, and help bring free coding education to students worldwide.",
alternates: { canonical: "/join/" },
openGraph: {
title: "Join Us | CodeWithPurpose",
description:
"Volunteer with CodeWithPurpose. Teach, mentor, and help bring free coding education to students worldwide.",
url: "/join/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Join Us | CodeWithPurpose",
description:
"Volunteer with CodeWithPurpose. Teach, mentor, and help bring free coding education to students worldwide.",
},
};

const roles = [
Expand Down
39 changes: 39 additions & 0 deletions src/app/media/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Metadata } from "next";
import { MediaLibrary } from "@/components/media/MediaLibrary";
import { PageHero, PageSection } from "@/components/PageHero";
import { PageShell } from "@/components/PageShell";
import { getMediaItems } from "@/lib/media";

export const metadata: Metadata = {
title: "Media",
description:
"Short lessons, student stories, and behind-the-scenes videos from CodeWithPurpose.",
alternates: { canonical: "/media/" },
openGraph: {
title: "Media | CodeWithPurpose",
description:
"Short lessons, student stories, and behind-the-scenes videos from CodeWithPurpose.",
url: "/media/",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Media | CodeWithPurpose",
description:
"Short lessons, student stories, and behind-the-scenes videos from CodeWithPurpose.",
},
};

export default function MediaPage() {
return (
<PageShell>
<PageHero
title="What we&apos;re making, in motion"
description="Short lessons, student stories, and the work happening behind free coding education. Watch on the site or follow the original post to join the conversation."
/>
<PageSection>
<MediaLibrary items={getMediaItems()} />
</PageSection>
</PageShell>
);
}
2 changes: 2 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GlobalReachSection } from "@/components/GlobalReachSection";
import { SecuritySection } from "@/components/SecuritySection";
import { QuoteSection } from "@/components/QuoteSection";
import { HowItWorksSection } from "@/components/HowItWorksSection";
import { MediaSection } from "@/components/media/MediaSection";
import { FaqSection } from "@/components/FaqSection";
import { FinalCtaSection } from "@/components/FinalCtaSection";
import { SiteFooter } from "@/components/SiteFooter";
Expand Down Expand Up @@ -40,6 +41,7 @@ export default function Home() {
</div>
<QuoteSection />
<HowItWorksSection />
<MediaSection />
<FaqSection />
<FinalCtaSection />
</main>
Expand Down
2 changes: 2 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
"/impact",
"/contact",
"/donate",
"/media",
"/learn/ml",
"/learn/vibecoding",
"/learn/python",
Expand All @@ -30,6 +31,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
"/learn/roblox",
"/learn/github",
"/learn/html-css",
"/learn/computer-vision",
].map((path) => ({
url: abs(path),
lastModified: new Date(),
Expand Down
6 changes: 5 additions & 1 deletion src/components/PageHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ export function PageHero({
return (
<section className="relative overflow-hidden border-b-[0.5px] border-[var(--home-hairline)] bg-[var(--home-page)] pt-12 pb-14 md:pt-20 md:pb-24">
<div aria-hidden="true" className="cwp-hero-bg absolute inset-0" />
<div className="relative mx-auto grid w-full max-w-[85rem] items-center gap-10 px-5 md:px-10 lg:grid-cols-2">
<div
className={`relative mx-auto grid w-full max-w-[85rem] items-center gap-10 px-5 md:px-10 ${
image ? "lg:grid-cols-2" : "lg:grid-cols-1"
}`}
>
<Reveal>
<h1 className="home-display text-[2rem] leading-[1.05] tracking-[-0.02em] md:text-[2.75rem] lg:text-[3.25rem]">
{title}
Expand Down
6 changes: 6 additions & 0 deletions src/components/SiteFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LEARN_ML_HREF,
LEARN_PYTHON_HREF,
LEARN_VIBECODING_HREF,
MEDIA_HREF,
TOOLKIT_HREF,
X_HREF,
} from "@/lib/links";
Expand Down Expand Up @@ -106,6 +107,11 @@ export function SiteFooter() {
Learning Toolkit
</a>
</li>
<li>
<a href={MEDIA_HREF} className="home-footer-link">
Media
</a>
</li>
<li>
<a href={ABOUT_HREF} className="home-footer-link">
Our Story
Expand Down
111 changes: 111 additions & 0 deletions src/components/media/MediaCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { InstagramIcon } from "@/components/icons";
import type { MediaItem } from "@/lib/media";
import { getYouTubeEmbedUrl } from "@/lib/media";

interface MediaCardProps {
item: MediaItem;
}

export function MediaCard({ item }: MediaCardProps) {
if (item.platform === "youtube") {
if (!item.videoId) {
throw new Error(`YouTube item ${item.id} is missing its videoId.`);
}

return (
<article className="home-card overflow-hidden rounded-[20px]">
<div className="aspect-video bg-[var(--home-grey-450)]">
<iframe
src={getYouTubeEmbedUrl(item.videoId)}
title={`${item.title} on YouTube`}
loading="lazy"
allow="encrypted-media; picture-in-picture"
allowFullScreen
className="h-full w-full border-0"
/>
</div>
<div className="p-6 md:p-7">
<p className="text-[11px] uppercase tracking-[0.12em] text-[var(--home-ink-quiet)]">
YouTube
</p>
<h2 className="mt-2 text-xl leading-tight md:text-2xl">{item.title}</h2>
<p className="mt-3 text-[15px] leading-[1.55] text-[var(--home-ink-soft)]">
{item.description}
</p>
</div>
</article>
);
}

return (
<article className="home-card home-lift overflow-hidden rounded-[20px]">
<a
href={item.url}
target="_blank"
rel="noreferrer"
aria-label={`${item.title} — watch on Instagram`}
className="group block"
>
<div className="relative aspect-video overflow-hidden bg-[linear-gradient(135deg,#f4d7d2,#e1d7f3_52%,#d7eedb)]">
{item.thumbnail ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.thumbnail}
alt=""
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.025]"
/>
) : (
<div className="flex h-full flex-col items-center justify-center gap-3 text-[var(--home-moss)]">
<InstagramIcon className="h-12 w-12" />
<span className="text-sm font-medium">Watch on Instagram</span>
</div>
)}
<span className="absolute bottom-4 left-4 inline-flex items-center gap-2 rounded-full bg-[var(--home-ink)] px-3 py-1.5 text-xs font-medium text-white shadow-[var(--home-shadow-sm)]">
<InstagramIcon className="h-3.5 w-3.5" />
Instagram
</span>
</div>
<div className="p-6 md:p-7">
<h2 className="text-xl leading-tight md:text-2xl">{item.title}</h2>
<p className="mt-3 text-[15px] leading-[1.55] text-[var(--home-ink-soft)]">
{item.description}
</p>
<span className="home-arrow-link mt-5">
Watch on Instagram <span className="home-arrow">→</span>
</span>
</div>
</a>
</article>
);
}

export function MediaGrid({
items,
emptyTitle = "New videos are on the way",
emptyDescription = "We're collecting the lessons, workshops, and small moments that make CodeWithPurpose what it is. Check back soon for the first set.",
}: {
items: readonly MediaItem[];
emptyTitle?: string;
emptyDescription?: string;
}) {
if (items.length === 0) {
return (
<div className="home-card rounded-[20px] bg-[var(--home-grey-450)] p-8 text-center md:p-12">
<h2 className="home-serif text-[1.75rem] md:text-[2.25rem]">{emptyTitle}</h2>
<p className="mx-auto mt-3 max-w-xl text-[15px] leading-[1.6] text-[var(--home-ink-soft)]">
{emptyDescription}
</p>
</div>
);
}

return (
<div className="grid gap-6 md:grid-cols-2">
{items.map((item) => (
<MediaCard key={item.id} item={item} />
))}
</div>
);
}
Loading
Loading