From c3fcc3f0e91b0d99bbd756acf3b42a5650a640bc Mon Sep 17 00:00:00 2001 From: angelcodes95 Date: Sun, 21 Sep 2025 10:50:07 -0700 Subject: [PATCH 1/3] feat: create watch page to showcase past livestreams Add archive page with embedded YouTube videos, livestream data structure, and header navigation. Styled to match site theme. --- app/archive/page.tsx | 162 ++++++++++++++++++++++++++++++++++++++ app/components/Header.tsx | 3 + app/info/livestreams.ts | 32 ++++++++ 3 files changed, 197 insertions(+) create mode 100644 app/archive/page.tsx create mode 100644 app/info/livestreams.ts diff --git a/app/archive/page.tsx b/app/archive/page.tsx new file mode 100644 index 0000000..666de0e --- /dev/null +++ b/app/archive/page.tsx @@ -0,0 +1,162 @@ +"use client" +import styled from "styled-components" +import { livestreams } from "../info/livestreams" + +// Components // + +export default function Archive() { + return ( +
+ + Livestream Archive + + Watch past DEVx meetup livestreams featuring community talks and networking sessions. + + + {livestreams.map((stream, index) => ( + + {stream.date} + + + + + {stream.title} + {stream.description} + + + ))} + + + + See All Livestreams + + + +
+ ) +} + +const Main = styled.main` + padding-top: 5rem; + min-height: 100vh; + & ~ footer { + display: none; + } +` + +const ArchiveSection = styled.section` + background-color: transparent; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + margin-bottom: 3rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +` + +const Title = styled.h2` + font-size: 1.875rem; + font-weight: bold; + margin-bottom: 1rem; + text-align: center; + color: white; +` + +const ArchiveDescription = styled.p` + margin-top: 0.5rem; + font-size: 1.25rem; + text-align: center; + max-width: 60rem; + margin-left: auto; + margin-right: auto; + margin-bottom: 3rem; + color: #d1d5db; +` + +const LivestreamGrid = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + width: 100%; +` + +const LivestreamCard = styled.div` + display: flex; + flex-direction: column; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 0.5rem; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); +` + +const DateLabel = styled.div` + background-color: black; + color: white; + padding: 0.75rem 1rem; + font-weight: 600; + font-size: 1rem; +` + +const VideoContainer = styled.div` + width: 100%; + height: 400px; + position: relative; + + @media (min-width: 768px) { + height: 500px; + } +` + +const StreamInfo = styled.div` + padding: 1.5rem; +` + +const StreamTitle = styled.h3` + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 0.75rem; + color: white; +` + +const StreamDescription = styled.p` + font-size: 1rem; + line-height: 1.6; + color: #d1d5db; +` + +const ButtonSection = styled.div` + margin-top: 3rem; + display: flex; + justify-content: center; +` + +const ViewAllButton = styled.a` + background-color: white; + color: black; + padding: 15px 30px; + border-radius: 0.25rem; + text-decoration: none; + display: inline-block; + font-weight: 600; + font-size: 1.1rem; + transition: background-color 0.2s ease; + + &:hover { + background-color: #e5e5e5; + } +` diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 5789598..678e9a6 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -124,6 +124,9 @@ const NavLinks = () => { Event Calendar + + Archive + {/* Hide Events until the page design is ready and finalized */} {/* Events diff --git a/app/info/livestreams.ts b/app/info/livestreams.ts new file mode 100644 index 0000000..d1589af --- /dev/null +++ b/app/info/livestreams.ts @@ -0,0 +1,32 @@ +/* manually adding data for now, chose to display most recent four livestreams, will have a button that links to the livestream page on youtube at the bottom, can think about API integration and dynamic updating, figured one video addition and subtraction monthly is fine to start, -AV */ + +export const livestreams = [ + { + id: "VIEHeDUsjjA", + title: "DEVx Meetup - Latest Stream", + date: "2024-12-01", + description: + "AJ Caldwell - Capacitor for making Native apps with web tech / Rajni Gediya - Bluetooth Low Energy (BLE) fundamentals" + }, + { + id: "SabVW9xrcfE", + title: "DEVx Meetup - November 2024", + date: "2024-11-01", + description: + "Ezekiel Lopez - Vibe Coding with Claude Code from your Phone / Javier Pacheco - Azure Infrastructrure for the Modern Lakehouse" + }, + { + id: "AYiq9cScCV8", + title: "DEVx Meetup - October 2024", + date: "2024-10-01", + description: + "James Lawrence - Music Theory and Generating keys with Python / Jonathan Lewis - Getting Started with Github Actions" + }, + { + id: "o8L7ylMgDAc", + title: "DEVx Meetup - September 2024", + date: "2024-09-01", + description: + "AJ Caldwell - Asynchronous Svelte: Component level await at last / Jonathan Lewis - From Git to Game: DevOps for Modern Game Devs" + } +] From 1420d6135b620b2469f26826eade7ac00e932ec6 Mon Sep 17 00:00:00 2001 From: angelcodes95 Date: Fri, 26 Sep 2025 23:30:49 -0700 Subject: [PATCH 2/3] feat: restructure data and UI to showcase individual speaker talks Pivot from livestream-centric to speaker-centric model. Create talks.ts with flat structure where each talk is a separate entry with timestamps. Update UI to display one card per speaker. Rename archive to watch and add all 31 talk timestamps. --- app/archive/page.tsx | 162 -------------- app/components/Header.tsx | 2 +- app/info/livestreams.ts | 32 --- app/info/talks.ts | 272 +++++++++++++++++++++++ app/watch/page.tsx | 452 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 725 insertions(+), 195 deletions(-) delete mode 100644 app/archive/page.tsx delete mode 100644 app/info/livestreams.ts create mode 100644 app/info/talks.ts create mode 100644 app/watch/page.tsx diff --git a/app/archive/page.tsx b/app/archive/page.tsx deleted file mode 100644 index 666de0e..0000000 --- a/app/archive/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client" -import styled from "styled-components" -import { livestreams } from "../info/livestreams" - -// Components // - -export default function Archive() { - return ( -
- - Livestream Archive - - Watch past DEVx meetup livestreams featuring community talks and networking sessions. - - - {livestreams.map((stream, index) => ( - - {stream.date} - - - - - {stream.title} - {stream.description} - - - ))} - - - - See All Livestreams - - - -
- ) -} - -const Main = styled.main` - padding-top: 5rem; - min-height: 100vh; - & ~ footer { - display: none; - } -` - -const ArchiveSection = styled.section` - background-color: transparent; - padding: 2rem; - border-radius: 0.5rem; - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - margin-bottom: 3rem; - max-width: 1200px; - margin-left: auto; - margin-right: auto; -` - -const Title = styled.h2` - font-size: 1.875rem; - font-weight: bold; - margin-bottom: 1rem; - text-align: center; - color: white; -` - -const ArchiveDescription = styled.p` - margin-top: 0.5rem; - font-size: 1.25rem; - text-align: center; - max-width: 60rem; - margin-left: auto; - margin-right: auto; - margin-bottom: 3rem; - color: #d1d5db; -` - -const LivestreamGrid = styled.div` - display: flex; - flex-direction: column; - gap: 3rem; - width: 100%; -` - -const LivestreamCard = styled.div` - display: flex; - flex-direction: column; - background-color: rgba(255, 255, 255, 0.05); - border-radius: 0.5rem; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.1); -` - -const DateLabel = styled.div` - background-color: black; - color: white; - padding: 0.75rem 1rem; - font-weight: 600; - font-size: 1rem; -` - -const VideoContainer = styled.div` - width: 100%; - height: 400px; - position: relative; - - @media (min-width: 768px) { - height: 500px; - } -` - -const StreamInfo = styled.div` - padding: 1.5rem; -` - -const StreamTitle = styled.h3` - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 0.75rem; - color: white; -` - -const StreamDescription = styled.p` - font-size: 1rem; - line-height: 1.6; - color: #d1d5db; -` - -const ButtonSection = styled.div` - margin-top: 3rem; - display: flex; - justify-content: center; -` - -const ViewAllButton = styled.a` - background-color: white; - color: black; - padding: 15px 30px; - border-radius: 0.25rem; - text-decoration: none; - display: inline-block; - font-weight: 600; - font-size: 1.1rem; - transition: background-color 0.2s ease; - - &:hover { - background-color: #e5e5e5; - } -` diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 678e9a6..9d6b9c1 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -125,7 +125,7 @@ const NavLinks = () => {
- Archive + Watch {/* Hide Events until the page design is ready and finalized */} {/* diff --git a/app/info/livestreams.ts b/app/info/livestreams.ts deleted file mode 100644 index d1589af..0000000 --- a/app/info/livestreams.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* manually adding data for now, chose to display most recent four livestreams, will have a button that links to the livestream page on youtube at the bottom, can think about API integration and dynamic updating, figured one video addition and subtraction monthly is fine to start, -AV */ - -export const livestreams = [ - { - id: "VIEHeDUsjjA", - title: "DEVx Meetup - Latest Stream", - date: "2024-12-01", - description: - "AJ Caldwell - Capacitor for making Native apps with web tech / Rajni Gediya - Bluetooth Low Energy (BLE) fundamentals" - }, - { - id: "SabVW9xrcfE", - title: "DEVx Meetup - November 2024", - date: "2024-11-01", - description: - "Ezekiel Lopez - Vibe Coding with Claude Code from your Phone / Javier Pacheco - Azure Infrastructrure for the Modern Lakehouse" - }, - { - id: "AYiq9cScCV8", - title: "DEVx Meetup - October 2024", - date: "2024-10-01", - description: - "James Lawrence - Music Theory and Generating keys with Python / Jonathan Lewis - Getting Started with Github Actions" - }, - { - id: "o8L7ylMgDAc", - title: "DEVx Meetup - September 2024", - date: "2024-09-01", - description: - "AJ Caldwell - Asynchronous Svelte: Component level await at last / Jonathan Lewis - From Git to Game: DevOps for Modern Game Devs" - } -] diff --git a/app/info/talks.ts b/app/info/talks.ts new file mode 100644 index 0000000..1782676 --- /dev/null +++ b/app/info/talks.ts @@ -0,0 +1,272 @@ +/* +Manually adding data for now +TODO: API automation in future update -AV +TODO: Fill in missing startTime/endTime values by watching videos +*/ + +export const talks = [ + // August 2025 + { + videoId: "VIEHeDUsjjA", + speaker: "AJ Caldwell", + title: "Capacitor for Making Native Apps with Web Tech", + date: "2025-08-24", + year: 2025, + startTime: "10m53s", + endTime: "27m14s" + }, + { + videoId: "VIEHeDUsjjA", + speaker: "Rajni Gediya", + title: "Bluetooth Low Energy (BLE) Fundamentals", + date: "2025-08-24", + year: 2025, + startTime: "29m54s", + endTime: "64m7s" + }, + // July 2025 + { + videoId: "SabVW9xrcfE", + speaker: "Ezekiel Lopez", + title: "Vibe Coding with Claude Code from your Phone", + date: "2025-07-27", + year: 2025, + startTime: "48s", + endTime: "17m3s" + }, + { + videoId: "SabVW9xrcfE", + speaker: "Javier Pacheco", + title: "Azure Infrastructure for the Modern Lakehouse", + date: "2025-07-27", + year: 2025, + startTime: "19m18s", + endTime: "37m5s" + }, + // June 2025 + { + videoId: "AYiq9cScCV8", + speaker: "James Lawrence", + title: "Music Theory and Generating Keys with Python", + date: "2025-06-29", + year: 2025, + startTime: "5m40s", + endTime: "22m8s" + }, + { + videoId: "AYiq9cScCV8", + speaker: "Jonathan Lewis", + title: "Getting Started with GitHub Actions", + date: "2025-06-29", + year: 2025, + startTime: "24m50s", + endTime: "74m8s" + }, + // May 2025 + { + videoId: "o8L7ylMgDAc", + speaker: "AJ Caldwell", + title: "Asynchronous Svelte: Component Level Await at Last", + date: "2025-06-01", + year: 2025, + startTime: "7m42s", + endTime: "16m41s" + }, + { + videoId: "o8L7ylMgDAc", + speaker: "Jonathan Lewis", + title: "From Git to Game: DevOps for Modern Game Devs", + date: "2025-06-01", + year: 2025, + startTime: "19m11s", + endTime: "45m40s" + }, + // April 2025 + { + videoId: "mVY98um55Yo", + speaker: "Jonathan Lewis", + title: "Intro to game development with Godot", + date: "2025-04-27", + year: 2025, + startTime: "20m31s", + endTime: "41m16s" + }, + { + videoId: "mVY98um55Yo", + speaker: "Ezekiel Lopez", + title: "My Building Process", + date: "2025-04-27", + year: 2025, + startTime: "42m49s", + endTime: "58m24s" + }, + // SPECIAL BONUS - ED's Game Demo April 2025 + { + videoId: "mVY98um55Yo", + speaker: "Edward Chu", + title: "Demolition Man Game Demo", + date: "2025-04-27", + year: 2025, + startTime: "63m28s", + endTime: "68m4s" + }, + // March 2025 (3 speakers) + { + videoId: "pANiDn8O84g", + speaker: "AJ Caldwell", + title: "Flipping Animations, a simple way to animate between states", + date: "2025-03-30", + year: 2025, + startTime: "7m23s", + endTime: "20m37s" + }, + { + videoId: "pANiDn8O84g", + speaker: "Keith Chester", + title: "Making AI Break Bad", + date: "2025-03-30", + year: 2025, + startTime: "24m48s", + endTime: "43m45s" + }, + { + videoId: "pANiDn8O84g", + speaker: "Hans Baker", + title: "Building a Code Review tool for work", + date: "2025-03-30", + year: 2025, + startTime: "45m9s", + endTime: "68m51s" + }, + // February 2025 (3 speakers) + { + videoId: "2cMzN_4guQ0", + speaker: "David Fridley", + title: "Forging productive national discourse with React", + date: "2025-02-02", + year: 2025, + startTime: "10m54s", + endTime: "25m16s" + }, + { + videoId: "2cMzN_4guQ0", + speaker: "Keith Chester", + title: "Arkanine - An Agentic AI framework for Makers", + date: "2025-02-02", + year: 2025, + startTime: "29m48s", + endTime: "50m20s" + }, + { + videoId: "2cMzN_4guQ0", + speaker: "Ezekiel Lopez", + title: "Make your own AI images", + date: "2025-02-02", + year: 2025, + startTime: "56m1s", + endTime: "72m34s" + }, + // January 2025 (3 speakers) + { + videoId: "AifVBwTPLYc", + speaker: "AJ Caldwell", + title: "State Machines", + date: "2025-01-12", + year: 2025, + startTime: "32m38s", + endTime: "48m38s" + }, + { + videoId: "AifVBwTPLYc", + speaker: "Tryston Perry", + title: "Windmill - The Last Low-Code Solution You'll Ever Need", + date: "2025-01-12", + year: 2025, + startTime: "54m36s", + endTime: "87m34s" + }, + { + videoId: "AifVBwTPLYc", + speaker: "Adam Villarreal", + title: "Bulding Personalized Chatbots with LLMs and RAG", + date: "2025-01-12", + year: 2025, + startTime: "88m49s", + endTime: "126m17s" + }, + // November 2024 (3 speakers) + { + videoId: "Kf7paXoqFv0", + speaker: "David George Hoqqanen", + title: "How to make your own Jackbox Games", + date: "2024-11-03", + year: 2024, + startTime: "9m18s", + endTime: "28m34s" + }, + { + videoId: "Kf7paXoqFv0", + speaker: "Brandon Wong", + title: "Prompt Engineering", + date: "2024-11-03", + year: 2024, + startTime: "35m41s", + endTime: "65m14s" + }, + { + videoId: "Kf7paXoqFv0", + speaker: "Tryston Perry", + title: "Web Components and the Future of Micro Front-ends for Freelancers", + date: "2024-11-03", + year: 2024, + startTime: "69m36s", + endTime: "82m22s" + }, + // September 2024 (3 speakers) + { + videoId: "A3Nvq49D1YE", + speaker: "David Stone", + title: "Developing with the MIDI protocol", + date: "2024-09-01", + year: 2024, + startTime: "8m6s", + endTime: "33m44s" + }, + { + videoId: "A3Nvq49D1YE", + speaker: "Coriano Harris", + title: "Raise your hand if you want to help out with user interactions", + date: "2024-09-01", + year: 2024, + startTime: "41m41s", + endTime: "54m41s" + }, + { + videoId: "A3Nvq49D1YE", + speaker: "Faybien Chaynes", + title: "Git Bisect", + date: "2024-09-01", + year: 2024, + startTime: "63m15s", + endTime: "73m11s" + }, + // August 2024 (3 speakers) + { + videoId: "4ZdUv9zY-VU", + speaker: "AJ Caldwell", + title: "Ink Native Svelte", + date: "2024-09-01", + year: 2024, + startTime: "44m14s", + endTime: "55m30s" + }, + { + videoId: "4ZdUv9zY-VU", + speaker: "Ezekiel Lopez", + title: "How to fix the internet by building Chrome Extensions", + date: "2024-09-01", + year: 2024, + startTime: "65m47s", + endTime: "93m12s" + } +] diff --git a/app/watch/page.tsx b/app/watch/page.tsx new file mode 100644 index 0000000..9d0fe04 --- /dev/null +++ b/app/watch/page.tsx @@ -0,0 +1,452 @@ +"use client" +import { useMemo } from "react" +import styled from "styled-components" +import { talks } from "../info/talks" +import { PotionBackground } from "../components/PotionBackground" +import { ErrorBoundary } from "../components/ErrorBoundary" + +// Types + +interface Talk { + videoId: string + speaker: string + title: string + date: string + year: number + startTime: string + endTime: string +} + +// Components + +export default function Watch() { + // Memoize video processing: sort by date, partition featured vs archive + const { featuredTalks, talksByYear, years } = useMemo(() => { + const sorted = [...talks].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ) + + // Show 3 most recent talks in hero + const featured = sorted.slice(0, 3) + const remaining = sorted.slice(3) + + // Group remaining talks by year (excludes featured) + const grouped = remaining.reduce( + (acc: Record, talk) => { + if (!acc[talk.year]) acc[talk.year] = [] + acc[talk.year].push(talk) + return acc + }, + {} as Record + ) + + const sortedYears = Object.keys(grouped) + .map(Number) + .sort((a, b) => b - a) + + return { + featuredTalks: featured, + talksByYear: grouped, + years: sortedYears + } + }, []) + + return ( + <> + + } + > + + + +
+ {/* Hero section: intro blurb + featured talks grid */} + + + {`DEVx brings developers together to share ideas and spark conversations. +Our monthly events feature talks on topics in software development and engineering. +Explore our collection of presentations from the community.`} + + + {/* 3 most recent talks displayed as cards */} + + {featuredTalks.map((talk) => ( + + + + + + + + + {talk.title} + {talk.speaker} + + + ))} + + + + + {years.map((year) => { + const yearTalks = talksByYear[year] + + if (yearTalks.length === 0) return null + + return ( + + {year} + + {/* Render all talks in grid */} + + {yearTalks.map((talk: Talk) => ( + + + + + + + + + {talk.title} + {talk.speaker} + + + ))} + + + ) + })} + + + Watch More + + + +
+ + ) +} + +// Styles + +const BackgroundContainer = styled.section` + background-color: #0a0a0a; + position: fixed; + height: 100vh; + width: 100vw; + top: 0; + left: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` + +const Main = styled.main` + position: relative; + z-index: 1; + & ~ footer { + display: none; + } +` + +const WatchSection = styled.section` + background-color: transparent; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + margin-bottom: 3rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +` + +const LivestreamGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; + width: 100%; + + @media (min-width: 768px) { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + } + + @media (min-width: 1200px) { + grid-template-columns: repeat(4, 1fr); + } +` + +const LivestreamCard = styled.div` + display: flex; + flex-direction: column; + background-color: transparent; + border-radius: 0.5rem; + overflow: hidden; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-4px); + } +` + +const ThumbnailLink = styled.a` + display: block; + text-decoration: none; + position: relative; + width: 100%; + height: 100%; +` + +const ThumbnailContainer = styled.div` + width: 100%; + aspect-ratio: 16/9; + position: relative; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.8); + border-radius: 0.5rem; +` + +const ThumbnailImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + } +` + +const PlayButton = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(255, 255, 255, 0.5); + color: black; + border-radius: 50%; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: bold; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.8); + transform: translate(-50%, -50%) scale(1.1); + } +` + +const StreamInfo = styled.div` + padding: 1rem 0; +` + +const TalkTitle = styled.p` + font-size: 0.9rem; + line-height: 1.3; + color: #d1d5db; + margin: 0 0 0.5rem 0; + font-weight: 500; +` + +const SpeakerName = styled.p` + font-size: 0.85rem; + color: #9ca3af; + margin: 0; + font-weight: 600; +` + +const ButtonSection = styled.div` + margin-top: 3rem; + display: flex; + justify-content: center; +` + +const ViewAllButton = styled.a` + background-color: white; + color: black; + padding: 15px 30px; + border-radius: 0.25rem; + text-decoration: none; + display: inline-block; + font-weight: 600; + font-size: 1.1rem; + transition: background-color 0.2s ease; + + &:hover { + background-color: #e5e5e5; + } +` + +const HeroSection = styled.section` + max-width: 1200px; + width: 100%; + min-height: 100vh; + margin: 0 auto; + padding: 4rem 2rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3rem; + + @media (max-width: 968px) { + padding: 2rem 1rem; + gap: 2rem; + } +` + +const HeroBlurb = styled.p` + font-size: 1.25rem; + line-height: 1.8; + color: #d1d5db; + text-align: center; + max-width: 900px; + margin: 0; + white-space: pre-line; + + @media (max-width: 768px) { + font-size: 1.1rem; + line-height: 1.6; + } +` + +const FeaturedGrid = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + width: 100%; + + @media (max-width: 968px) { + grid-template-columns: 1fr; + gap: 1.5rem; + } +` + +const FeaturedCard = styled.div` + display: flex; + flex-direction: column; + background-color: transparent; + border-radius: 0.5rem; + overflow: hidden; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-4px); + } +` + +const FeaturedInfo = styled.div` + padding: 1rem 0; +` + +const FeaturedTitle = styled.p` + font-size: 1rem; + line-height: 1.4; + color: #d1d5db; + margin: 0 0 0.5rem 0; + font-weight: 500; +` + +const FeaturedSpeaker = styled.p` + font-size: 0.9rem; + color: #9ca3af; + margin: 0; + font-weight: 600; +` + +const HeroThumbnailContainer = styled.div` + width: 100%; + aspect-ratio: 16/9; + position: relative; + background-color: rgba(0, 0, 0, 0.8); + border-radius: 0.5rem; + overflow: hidden; +` + +const HeroThumbnailImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + } +` + +const HeroPlayButton = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(255, 255, 255, 0.5); + color: black; + border-radius: 50%; + width: 70px; + height: 70px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.8rem; + font-weight: bold; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.8); + transform: translate(-50%, -50%) scale(1.1); + } +` + +const YearSection = styled.div` + margin-bottom: 4rem; +` + +const YearHeader = styled.h2` + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 2rem; + text-align: center; + color: white; + border-bottom: 2px solid rgba(255, 255, 255, 0.2); + padding-bottom: 1rem; +` + +// Utility Functions + +const buildYouTubeUrl = (id: string, startTime?: string) => { + let url = `https://youtube.com/watch?v=${id}` + + if (startTime && startTime !== "0s") { + url += `&t=${startTime}` + } + + return url +} + +const getYouTubeThumbnail = (id: string) => { + // Use hqdefault for consistent availability across all videos + return `https://img.youtube.com/vi/${id}/hqdefault.jpg` +} From 2f2f8e09d6e9fdd9ad647daf92115b37cc13460b Mon Sep 17 00:00:00 2001 From: angelcodes95 Date: Sun, 12 Oct 2025 09:05:57 -0700 Subject: [PATCH 3/3] chore: configure Prettier as default formatter with format-on-save --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..779ad19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +}