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" +} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 5789598..9d6b9c1 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -124,6 +124,9 @@ const NavLinks = () => { Event Calendar + + Watch + {/* Hide Events until the page design is ready and finalized */} {/* Events 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` +}