From fb5dc5f7ed7a7666e348d19ec8e172a4b65388a1 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 1 Jan 2026 16:45:02 -0600 Subject: [PATCH 01/10] chore: add .opencode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b5bc445..d41a5a9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md node_modules +.opencode/ \ No newline at end of file From 77a9c51e3f780a1123d799e53a182d95a9dac2c8 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 1 Jan 2026 16:59:11 -0600 Subject: [PATCH 02/10] feat: add hackathon details section to episode pages - Introduced a new section to display active hackathons associated with episodes, including title, deadline, sponsors, and a join button. - Updated the episode schema to include hackathon data retrieval. - Enhanced styling for the new hackathon card layout to improve visual presentation. --- .../[collectionSlug]/[episodeSlug].astro | 146 +++++++++++ libs/sanity/src/index.ts | 14 ++ libs/types/src/sanity.ts | 233 +++++++++--------- 3 files changed, 282 insertions(+), 111 deletions(-) diff --git a/apps/website/src/pages/series/[seriesSlug]/[collectionSlug]/[episodeSlug].astro b/apps/website/src/pages/series/[seriesSlug]/[collectionSlug]/[episodeSlug].astro index 58dddb4..ac6e2a4 100644 --- a/apps/website/src/pages/series/[seriesSlug]/[collectionSlug]/[episodeSlug].astro +++ b/apps/website/src/pages/series/[seriesSlug]/[collectionSlug]/[episodeSlug].astro @@ -304,6 +304,73 @@ if (episode.video && !episode.video.youtube_id && episode.video.mux) {
+ { + episode.hackathons && + episode.hackathons.filter((h) => new Date(h.deadline!) > new Date()) + .length > 0 && ( +
+ {episode.hackathons + .filter((h) => new Date(h.deadline!) > new Date()) + .map((hackathon) => ( +
+
+

{hackathon.title}

+ Active +
+ + {hackathon.sponsors && hackathon.sponsors.length > 0 && ( +
+ Sponsored by +
+ {hackathon.sponsors.map((sponsor) => { + if ( + !sponsor.logo?.public_id || + !sponsor.logo?.width || + !sponsor.logo?.height + ) { + return null; + } + + const src = createImageUrl( + sponsor.logo.public_id, + { + height: sponsor.logo.height * 2, + width: sponsor.logo.width * 2, + }, + ); + + return ( + + {sponsor.title} + + ); + })} +
+
+ )} + +
+ Deadline + + {format(new Date(hackathon.deadline!), 'MMM d, yyyy')}{' '} + at {format(new Date(hackathon.deadline!), 'h:mm a')} + +
+ + + Join Hackathon + +
+ ))} +
+ ) + } + { episode.sponsors && episode.sponsors.length > 0 ? (
@@ -574,6 +641,85 @@ if (episode.video && !episode.video.youtube_id && episode.video.mux) { font-size: 0.875em; } + .hackathon-section { + margin-block-end: 2rem; + } + + .hackathon-card { + background: var(--gray-200); + border: 1px solid var(--gray-600); + border-radius: 12px; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.25rem; + } + + .hackathon-header { + align-items: center; + display: flex; + gap: 0.75rem; + justify-content: space-between; + + h3 { + font-size: 1.125rem; + margin: 0; + } + } + + .hackathon-sponsors { + display: flex; + flex-direction: column; + gap: 0.375rem; + + .label { + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; + } + } + + .hackathon-sponsor-logos { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 1rem; + + a { + display: block; + } + + img { + block-size: 28px; + display: block; + inline-size: auto; + object-fit: contain; + } + } + + .hackathon-deadline { + display: flex; + flex-direction: column; + gap: 0.25rem; + + .label { + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; + } + + .deadline-date { + color: var(--text-emphasized); + font-size: 0.9375rem; + font-weight: 600; + } + } + + .hackathon-card .button { + margin-block-start: 0.5rem; + text-align: center; + } + .additional-details, .description, .supporters { diff --git a/libs/sanity/src/index.ts b/libs/sanity/src/index.ts index a0ab1d5..20808f3 100644 --- a/libs/sanity/src/index.ts +++ b/libs/sanity/src/index.ts @@ -206,6 +206,20 @@ const allEpisodesQuery = groq` 'slug': slug.current, title, }, + 'hackathons': hackathons[]-> { + 'slug': slug.current, + title, + deadline, + 'sponsors': sponsors[]->{ + title, + logo { + public_id, + width, + height + }, + link, + }, + }, } `; diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index 1752747..1985fc1 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -35,22 +35,6 @@ export type EpisodeImage = { caption?: string; }; -export type SanityImageCrop = { - _type: 'sanity.imageCrop'; - top: number; - bottom: number; - left: number; - right: number; -}; - -export type SanityImageHotspot = { - _type: 'sanity.imageHotspot'; - x: number; - y: number; - height: number; - width: number; -}; - export type EpisodeTag = { _id: string; _type: 'episodeTag'; @@ -62,12 +46,6 @@ export type EpisodeTag = { description?: string; }; -export type Slug = { - _type: 'slug'; - current: string; - source?: string; -}; - export type Sponsor = { _id: string; _type: 'sponsor'; @@ -80,26 +58,6 @@ export type Sponsor = { link: string; }; -export type CloudinaryAsset = { - _type: 'cloudinary.asset'; - public_id?: string; - resource_type?: string; - type?: string; - format?: string; - version?: number; - url?: string; - secure_url?: string; - width?: number; - height?: number; - bytes?: number; - duration?: number; - tags?: Array; - created_at?: string; - derived?: Array; // Unable to locate the referenced type "derived" in schema - access_mode?: string; - context?: CloudinaryAssetContext; -}; - export type Rules = { _id: string; _type: 'rules'; @@ -170,7 +128,7 @@ export type Person = { name: string; slug: Slug; photo?: CloudinaryAsset; - bio?: Markdown; + bio?: string; links?: Array<{ label?: string; url?: string; @@ -211,8 +169,6 @@ export type Person = { }>; }; -export type Markdown = string; - export type Hackathon = { _id: string; _type: 'hackathon'; @@ -226,7 +182,7 @@ export type Hackathon = { description: string; hero_image?: CloudinaryAsset; hero_title?: string; - body: Markdown; + body: string; episodes?: Array<{ _ref: string; _type: 'reference'; @@ -282,7 +238,7 @@ export type Episode = { slug: Slug; publish_date: string; short_description: string; - description: Markdown; + description: string; people?: Array<{ _ref: string; _type: 'reference'; @@ -326,22 +282,12 @@ export type Episode = { }; thumbnail?: CloudinaryAsset; thumbnail_alt?: string; - transcript?: Markdown; + transcript?: string; }; hidden?: 'visible' | 'hidden'; featured?: 'normal' | 'featured'; }; -export type MuxVideo = { - _type: 'mux.video'; - asset?: { - _ref: string; - _type: 'reference'; - _weak?: boolean; - [internalGroqTypeReferenceTo]?: 'mux.videoAsset'; - }; -}; - export type Collection = { _id: string; _type: 'collection'; @@ -394,6 +340,16 @@ export type Series = { featured?: 'normal' | 'featured'; }; +export type MuxVideo = { + _type: 'mux.video'; + asset?: { + _ref: string; + _type: 'reference'; + _weak?: boolean; + [internalGroqTypeReferenceTo]?: 'mux.videoAsset'; + }; +}; + export type MuxVideoAsset = { _id: string; _type: 'mux.videoAsset'; @@ -418,7 +374,6 @@ export type MuxAssetData = { max_stored_resolution?: string; passthrough?: string; encoding_tier?: string; - video_quality?: string; master_access?: string; aspect_ratio?: string; duration?: number; @@ -450,18 +405,12 @@ export type MuxStaticRenditions = { export type MuxStaticRenditionFile = { _type: 'mux.staticRenditionFile'; - name?: string; ext?: string; - height?: number; + name?: string; width?: number; bitrate?: number; - filesize?: string; - type?: string; - status?: string; - resolution_tier?: string; - resolution?: string; - id?: string; - passthrough?: string; + filesize?: number; + height?: number; }; export type MuxPlaybackId = { @@ -486,11 +435,6 @@ export type CloudinaryAssetContextCustom = { caption?: string; }; -export type CloudinaryAssetContext = { - _type: 'cloudinary.assetContext'; - custom?: CloudinaryAssetContextCustom; -}; - export type CloudinaryAssetDerived = { _type: 'cloudinary.assetDerived'; raw_transformation?: string; @@ -498,6 +442,37 @@ export type CloudinaryAssetDerived = { secure_url?: string; }; +export type CloudinaryAsset = { + _type: 'cloudinary.asset'; + public_id?: string; + resource_type?: string; + type?: string; + format?: string; + version?: number; + url?: string; + secure_url?: string; + width?: number; + height?: number; + bytes?: number; + duration?: number; + tags?: Array; + created_at?: string; + derived?: Array< + { + _key: string; + } & CloudinaryAssetDerived + >; + access_mode?: string; + context?: CloudinaryAssetContext; +}; + +export type CloudinaryAssetContext = { + _type: 'cloudinary.assetContext'; + custom?: CloudinaryAssetContextCustom; +}; + +export type Markdown = string; + export type SanityImagePaletteSwatch = { _type: 'sanity.imagePaletteSwatch'; background?: string; @@ -524,15 +499,20 @@ export type SanityImageDimensions = { aspectRatio: number; }; -export type SanityImageMetadata = { - _type: 'sanity.imageMetadata'; - location?: Geopoint; - dimensions?: SanityImageDimensions; - palette?: SanityImagePalette; - lqip?: string; - blurHash?: string; - hasAlpha?: boolean; - isOpaque?: boolean; +export type SanityImageHotspot = { + _type: 'sanity.imageHotspot'; + x: number; + y: number; + height: number; + width: number; +}; + +export type SanityImageCrop = { + _type: 'sanity.imageCrop'; + top: number; + bottom: number; + left: number; + right: number; }; export type SanityFileAsset = { @@ -557,13 +537,6 @@ export type SanityFileAsset = { source?: SanityAssetSourceData; }; -export type SanityAssetSourceData = { - _type: 'sanity.assetSourceData'; - name?: string; - id?: string; - url?: string; -}; - export type SanityImageAsset = { _id: string; _type: 'sanity.imageAsset'; @@ -587,6 +560,17 @@ export type SanityImageAsset = { source?: SanityAssetSourceData; }; +export type SanityImageMetadata = { + _type: 'sanity.imageMetadata'; + location?: Geopoint; + dimensions?: SanityImageDimensions; + palette?: SanityImagePalette; + lqip?: string; + blurHash?: string; + hasAlpha?: boolean; + isOpaque?: boolean; +}; + export type Geopoint = { _type: 'geopoint'; lat?: number; @@ -594,26 +578,34 @@ export type Geopoint = { alt?: number; }; +export type Slug = { + _type: 'slug'; + current: string; + source?: string; +}; + +export type SanityAssetSourceData = { + _type: 'sanity.assetSourceData'; + name?: string; + id?: string; + url?: string; +}; + export type AllSanitySchemaTypes = | ResourceItem | EpisodeImage - | SanityImageCrop - | SanityImageHotspot | EpisodeTag - | Slug | Sponsor - | CloudinaryAsset | Rules | Rewards | Faq | HackathonSubmission | Person - | Markdown | Hackathon | Episode - | MuxVideo | Collection | Series + | MuxVideo | MuxVideoAsset | MuxAssetData | MuxStaticRenditions @@ -621,16 +613,21 @@ export type AllSanitySchemaTypes = | MuxPlaybackId | MuxTrack | CloudinaryAssetContextCustom - | CloudinaryAssetContext | CloudinaryAssetDerived + | CloudinaryAsset + | CloudinaryAssetContext + | Markdown | SanityImagePaletteSwatch | SanityImagePalette | SanityImageDimensions - | SanityImageMetadata + | SanityImageHotspot + | SanityImageCrop | SanityFileAsset - | SanityAssetSourceData | SanityImageAsset - | Geopoint; + | SanityImageMetadata + | Geopoint + | Slug + | SanityAssetSourceData; export declare const internalGroqTypeReferenceTo: unique symbol; // Source: ../../libs/sanity/src/index.ts // Variable: allSeriesQuery @@ -736,11 +733,11 @@ export type SeriesBySlugQueryResult = { }> | null; } | null; // Variable: allEpisodesQuery -// Query: *[_type=="episode" && hidden != true] { title, 'slug': slug.current, description, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id, 'mux': mux_video.asset->data.playback_ids, 'captions': captions.asset->url, transcript, }, people[]-> { user_id, name, "slug": slug.current, photo { public_id } }, resources[] { label, url, }, 'sponsors': sponsors[]->{ title, logo { public_id, width, height }, link, }, 'related_episodes': *[_type=="collection" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> { title, 'slug': slug.current, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id } }, 'collection': *[_type=="collection" && references(^._id)][0] { 'slug': slug.current, title, 'episodeSlugs': episodes[]->slug.current, }, 'series': *[_type=="collection" && references(^._id)][0].series->{ 'slug': slug.current, title, }, } +// Query: *[_type=="episode" && hidden != true] { title, 'slug': slug.current, description, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id, 'mux': mux_video.asset->data.playback_ids, 'captions': captions.asset->url, transcript, }, people[]-> { user_id, name, "slug": slug.current, photo { public_id } }, resources[] { label, url, }, 'sponsors': sponsors[]->{ title, logo { public_id, width, height }, link, }, 'related_episodes': *[_type=="collection" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> { title, 'slug': slug.current, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id } }, 'collection': *[_type=="collection" && references(^._id)][0] { 'slug': slug.current, title, 'episodeSlugs': episodes[]->slug.current, }, 'series': *[_type=="collection" && references(^._id)][0].series->{ 'slug': slug.current, title, }, 'hackathons': hackathons[]-> { 'slug': slug.current, title, deadline, 'sponsors': sponsors[]->{ title, logo { public_id, width, height }, link, }, }, } export type AllEpisodesQueryResult = Array<{ title: string; slug: string; - description: Markdown; + description: string; short_description: string; publish_date: string; thumbnail: { @@ -757,7 +754,7 @@ export type AllEpisodesQueryResult = Array<{ } & MuxPlaybackId > | null; captions: string | null; - transcript: Markdown | null; + transcript: string | null; } | null; people: Array<{ user_id: string | null; @@ -804,13 +801,27 @@ export type AllEpisodesQueryResult = Array<{ slug: string; title: string; } | null; + hackathons: Array<{ + slug: string; + title: string; + deadline: string; + sponsors: Array<{ + title: string; + logo: { + public_id: string | null; + width: number | null; + height: number | null; + }; + link: string; + }> | null; + }> | null; }>; // Variable: episodeBySlugQuery // Query: *[_type=="episode" && slug.current==$episode][0] { title, 'slug': slug.current, description, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id, 'mux': mux_video.asset->data.playback_ids, 'captions': captions.asset->url, transcript, }, people[]-> { user_id, name, "slug": slug.current, photo { public_id } }, resources[] { label, url, }, 'sponsors': sponsors[]->{ title, logo { public_id, width, height }, link, }, 'related_episodes': *[_type=="collection" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> { title, 'slug': slug.current, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, video { youtube_id } }, 'collection': *[_type=="collection" && references(^._id)][0] { 'slug': slug.current, title, 'episodeSlugs': episodes[]->slug.current, }, 'series': *[_type=="collection" && references(^._id)][0].series->{ 'slug': slug.current, title, }, } export type EpisodeBySlugQueryResult = { title: string; slug: string; - description: Markdown; + description: string; short_description: string; publish_date: string; thumbnail: { @@ -827,7 +838,7 @@ export type EpisodeBySlugQueryResult = { } & MuxPlaybackId > | null; captions: string | null; - transcript: Markdown | null; + transcript: string | null; } | null; people: Array<{ user_id: string | null; @@ -877,7 +888,7 @@ export type EpisodeBySlugQueryResult = { } | null; // Variable: episodeTranscriptBySlugQuery // Query: *[_type=="episode" && slug.current==$episode][0].video.transcript -export type EpisodeTranscriptBySlugQueryResult = Markdown | null; +export type EpisodeTranscriptBySlugQueryResult = string | null; // Variable: earlyAccessEpisodesQuery // Query: *[_type=="episode" && dateTime(publish_date) > dateTime(now()) && defined(video.mux_video) && hidden != true] { title, 'slug': slug.current, short_description, publish_date, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, 'youtube_id': video.youtube_id, 'path': "/series/" + *[_type=="collection" && references(^._id)][0].series->slug.current + "/" + *[_type=="collection" && references(^._id)][0].slug.current + "/" + slug.current, 'series': *[_type=="collection" && references(^._id)][0].series->title, 'collection_number': upper(*[_type=="collection" && references(^._id)][0].slug.current), 'episodes': *[_type=="collection" && references(^._id)][0].episodes[]->slug.current, } export type EarlyAccessEpisodesQueryResult = Array<{ @@ -941,7 +952,7 @@ export type PersonByIdQueryResult = { height: number | null; width: number | null; } | null; - bio: Markdown | null; + bio: string | null; links: Array<{ label?: string; url?: string; @@ -957,7 +968,7 @@ export type AllUsersQueryResult = Array<{ _id: string; name: string; slug: string; - bio: Markdown | null; + bio: string | null; photo: { public_id: string | null; height: number | null; @@ -1023,7 +1034,7 @@ export type PersonBySlugQueryResult = { height: number | null; width: number | null; } | null; - bio: Markdown | null; + bio: string | null; links: Array<{ label?: string; url?: string; @@ -1102,7 +1113,7 @@ export type AllHackathonsQueryResult = Array<{ pubDate: string; deadline: string; description: string; - body: Markdown; + body: string; hero_image: { public_id: string | null; width: number | null; @@ -1137,7 +1148,7 @@ export type HackathonBySlugQueryResult = { title: string; slug: string; description: string; - body: Markdown; + body: string; pubDate: string; deadline: string; submissionForm: string; @@ -1220,7 +1231,7 @@ declare module '@sanity/client' { "\n *[_type==\"series\"] {\n title,\n 'slug': slug.current,\n description,\n image {\n public_id,\n height,\n width,\n },\n cover {\n public_id,\n height,\n width,\n },\n \"collections\": collections[]->{\n title,\n 'slug': slug.current,\n release_year,\n 'episode_count': count(episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))])\n } | order(release_year desc),\n 'path': '/series/' + slug.current + '/' + collections[-1]->slug.current,\n 'total_episode_count': count(collections[]->episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]),\n 'total_season_count': count(collections[]),\n 'latestEpisodeDate': collections[]->episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))] | order(@->publish_date desc)[0]->publish_date,\n featured\n } | order(latestEpisodeDate desc)\n": AllSeriesQueryResult; "\n *[_type==\"series\" && featured == 'featured'] {\n 'slug': slug.current,\n title,\n description,\n image {\n public_id,\n height,\n width,\n },\n cover {\n public_id,\n height,\n width,\n },\n 'path': '/series/' + slug.current + '/' + collections[-1]->slug.current,\n 'total_episode_count': count(collections[]->episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]),\n 'total_season_count': count(collections[])\n }\n": FeaturedSeriesQueryResult; "\n *[_type==\"series\" && slug.current==$series][0] {\n title,\n 'slug': slug.current,\n description,\n image {\n public_id,\n height,\n width,\n },\n cover {\n public_id,\n height,\n width,\n },\n 'sponsors': sponsors[]->{\n 'title': title,\n logo {\n public_id,\n width,\n height\n },\n link,\n },\n 'collection': collections[@->slug.current==$collection && @->series._ref==^._id][0]->{\n title,\n 'slug': slug.current,\n release_year,\n episodes[@->hidden != true]->{\n title,\n 'slug': slug.current,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'alt': video.thumbnail_alt,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n },\n video {\n youtube_id,\n mux_video,\n members_only\n }\n }\n },\n collections[]->{\n title,\n 'slug': slug.current,\n release_year,\n 'episode_count': count(episodes[@->hidden != true])\n }\n }\n": SeriesBySlugQueryResult; - "\n *[_type==\"episode\" && hidden != true] {\n title,\n 'slug': slug.current,\n description,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id,\n 'mux': mux_video.asset->data.playback_ids,\n 'captions': captions.asset->url,\n transcript,\n },\n people[]-> {\n user_id,\n name,\n \"slug\": slug.current,\n photo {\n public_id\n }\n },\n resources[] {\n label,\n url,\n },\n 'sponsors': sponsors[]->{\n title,\n logo {\n public_id,\n width,\n height\n },\n link,\n },\n 'related_episodes': *[_type==\"collection\" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> {\n title,\n 'slug': slug.current,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id\n }\n },\n 'collection': *[_type==\"collection\" && references(^._id)][0] {\n 'slug': slug.current,\n title,\n 'episodeSlugs': episodes[]->slug.current,\n },\n 'series': *[_type==\"collection\" && references(^._id)][0].series->{\n 'slug': slug.current,\n title,\n },\n }\n": AllEpisodesQueryResult; + "\n *[_type==\"episode\" && hidden != true] {\n title,\n 'slug': slug.current,\n description,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id,\n 'mux': mux_video.asset->data.playback_ids,\n 'captions': captions.asset->url,\n transcript,\n },\n people[]-> {\n user_id,\n name,\n \"slug\": slug.current,\n photo {\n public_id\n }\n },\n resources[] {\n label,\n url,\n },\n 'sponsors': sponsors[]->{\n title,\n logo {\n public_id,\n width,\n height\n },\n link,\n },\n 'related_episodes': *[_type==\"collection\" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> {\n title,\n 'slug': slug.current,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id\n }\n },\n 'collection': *[_type==\"collection\" && references(^._id)][0] {\n 'slug': slug.current,\n title,\n 'episodeSlugs': episodes[]->slug.current,\n },\n 'series': *[_type==\"collection\" && references(^._id)][0].series->{\n 'slug': slug.current,\n title,\n },\n 'hackathons': hackathons[]-> {\n 'slug': slug.current,\n title,\n deadline,\n 'sponsors': sponsors[]->{\n title,\n logo {\n public_id,\n width,\n height\n },\n link,\n },\n },\n }\n": AllEpisodesQueryResult; "\n *[_type==\"episode\" && slug.current==$episode][0] {\n title,\n 'slug': slug.current,\n description,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id,\n 'mux': mux_video.asset->data.playback_ids,\n 'captions': captions.asset->url,\n transcript,\n },\n people[]-> {\n user_id,\n name,\n \"slug\": slug.current,\n photo {\n public_id\n }\n },\n resources[] {\n label,\n url,\n },\n 'sponsors': sponsors[]->{\n title,\n logo {\n public_id,\n width,\n height\n },\n link,\n },\n 'related_episodes': *[_type==\"collection\" && references(^._id)][0].episodes[@->hidden != true && (defined(@->video.youtube_id) || defined(@->video.mux_video))]-> {\n title,\n 'slug': slug.current,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n video {\n youtube_id\n }\n },\n 'collection': *[_type==\"collection\" && references(^._id)][0] {\n 'slug': slug.current,\n title,\n 'episodeSlugs': episodes[]->slug.current,\n },\n 'series': *[_type==\"collection\" && references(^._id)][0].series->{\n 'slug': slug.current,\n title,\n },\n }\n": EpisodeBySlugQueryResult; '\n *[_type=="episode" && slug.current==$episode][0].video.transcript\n': EpisodeTranscriptBySlugQueryResult; "\n *[_type==\"episode\" && dateTime(publish_date) > dateTime(now()) && defined(video.mux_video) && hidden != true] {\n title,\n 'slug': slug.current,\n short_description,\n publish_date,\n 'thumbnail': {\n 'public_id': video.thumbnail.public_id,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n 'alt': video.thumbnail_alt,\n },\n 'youtube_id': video.youtube_id,\n 'path': \"/series/\" + *[_type==\"collection\" && references(^._id)][0].series->slug.current + \"/\" + *[_type==\"collection\" && references(^._id)][0].slug.current + \"/\" + slug.current,\n 'series': *[_type==\"collection\" && references(^._id)][0].series->title,\n 'collection_number': upper(*[_type==\"collection\" && references(^._id)][0].slug.current),\n 'episodes': *[_type==\"collection\" && references(^._id)][0].episodes[]->slug.current,\n }\n": EarlyAccessEpisodesQueryResult; From 4b114f2bbe48900365ccc0faad8132ef9a1c984b Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 1 Jan 2026 21:30:48 -0800 Subject: [PATCH 03/10] fix: button alignment --- apps/website/src/components/supporter-button.astro | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/website/src/components/supporter-button.astro b/apps/website/src/components/supporter-button.astro index 827d0df..1c52470 100644 --- a/apps/website/src/components/supporter-button.astro +++ b/apps/website/src/components/supporter-button.astro @@ -15,3 +15,9 @@ const status = await getSubscriptionStatus(user); ) } + + From 14c11c2d0a44b56df3d972a3a7fd74147847be62 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 1 Jan 2026 21:31:24 -0800 Subject: [PATCH 04/10] fix: better scope for preview card button styles --- .../src/components/design-system/card.astro | 2 +- .../components/hackathon-status-card.astro | 145 ++++++++++++++++++ apps/website/src/components/home/series.astro | 24 ++- 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 apps/website/src/components/hackathon-status-card.astro diff --git a/apps/website/src/components/design-system/card.astro b/apps/website/src/components/design-system/card.astro index f2d9d9c..9de7557 100644 --- a/apps/website/src/components/design-system/card.astro +++ b/apps/website/src/components/design-system/card.astro @@ -245,7 +245,7 @@ const TitleElement = toggle ? 'summary' : 'div'; text-box-edge: cap alphabetic; } - .button { + .button:is([data-variant='more-link']) { background: transparent; color: var(--white); display: inline-block; diff --git a/apps/website/src/components/hackathon-status-card.astro b/apps/website/src/components/hackathon-status-card.astro new file mode 100644 index 0000000..8ba4d53 --- /dev/null +++ b/apps/website/src/components/hackathon-status-card.astro @@ -0,0 +1,145 @@ +--- +import { createImageUrl } from '@codetv/cloudinary'; +import Card from '../components/design-system/card.astro'; + +export interface Props { + deadline: string; + sponsors: Array<{ + title: string; + logo: { + public_id: string | null; + width: number | null; + height: number | null; + }; + link: string; + }> | null; + submissionForm: string; + buttonText?: string; +} + +const props = Astro.props; + +// Format deadline for display +const deadline = props.deadline ? new Date(props.deadline) : null; +const deadlineFormatted = deadline + ? deadline.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }) + : 'TBD'; + +// Get first sponsor if available +const now = new Date(); +const isActive = deadline ? now < deadline : false; +const statusText = isActive ? 'Active' : 'Closed'; +const statusClass = isActive ? 'active' : 'closed'; +const primarySponsor = props.sponsors.length > 0 ? props.sponsors.at(0) : null; + +const formURL = new URL(props.submissionForm); +--- + + + + + + diff --git a/apps/website/src/components/home/series.astro b/apps/website/src/components/home/series.astro index c6b2c33..b0ce286 100644 --- a/apps/website/src/components/home/series.astro +++ b/apps/website/src/components/home/series.astro @@ -75,7 +75,12 @@ const series = await getAllSeries();

{earlyAccess.short_description}

- + See details and watch now

@@ -87,8 +92,7 @@ const series = await getAllSeries(); {recentEps.map((ep) => { // magic numbers to determine if an episode was published within the last N days const DAY_IN_MS = 86_400_000; - const NEW_CUTOFF_TIMESTAMP = - new Date().getTime() - DAY_IN_MS * 21; + const NEW_CUTOFF_TIMESTAMP = new Date().getTime() - DAY_IN_MS * 7; const publishTimestamp = new Date(ep.publish_date).getTime(); const showNewTag = publishTimestamp > NEW_CUTOFF_TIMESTAMP; const thumbnailURL = getImageURLWithFallback({ @@ -150,7 +154,12 @@ const series = await getAllSeries();

{description}

- + See details and watch now

@@ -205,7 +214,12 @@ const series = await getAllSeries();

{s.description}

- + See episodes

From df1eb91ee3ac80ce8d55c57066beb192cbf4edc9 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 1 Jan 2026 21:32:21 -0800 Subject: [PATCH 05/10] feat: use the shared card component --- apps/website/src/pages/hackathon/[id].astro | 108 +------------ .../[collectionSlug]/[episodeSlug].astro | 152 +++--------------- 2 files changed, 28 insertions(+), 232 deletions(-) diff --git a/apps/website/src/pages/hackathon/[id].astro b/apps/website/src/pages/hackathon/[id].astro index 7eb1884..02ad297 100644 --- a/apps/website/src/pages/hackathon/[id].astro +++ b/apps/website/src/pages/hackathon/[id].astro @@ -9,6 +9,7 @@ import Block from '../../components/design-system/block.astro'; import { getAllHackathons, getHackathonBySlug } from '@codetv/sanity'; import { marked } from 'marked'; import { createImageUrl, generateDefaultImage } from '@codetv/cloudinary'; +import HackathonStatusCard from '../../components/hackathon-status-card.astro'; export const prerender = true; @@ -131,54 +132,11 @@ const primarySponsor = hasSponsors ? sponsors?.[0] : null; /> - - - +
@@ -362,60 +320,6 @@ const primarySponsor = hasSponsors ? sponsors?.[0] : null;