From 3f1c3f960d8029c302770a4af9d95e8473f3a654 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Sat, 9 Aug 2025 10:50:03 -0700 Subject: [PATCH 01/55] wip: hackathon landing page stub Very early start of coding up the new hackathon landing page designs. --- .../pages/hackathon/touch-grass-apollo.astro | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 apps/website/src/pages/hackathon/touch-grass-apollo.astro diff --git a/apps/website/src/pages/hackathon/touch-grass-apollo.astro b/apps/website/src/pages/hackathon/touch-grass-apollo.astro new file mode 100644 index 0000000..dc3d246 --- /dev/null +++ b/apps/website/src/pages/hackathon/touch-grass-apollo.astro @@ -0,0 +1,98 @@ +--- +import Layout from '../../layouts/default.astro'; +import Newsletter from '../../components/home/newsletter.astro'; +import PageHeader from '../../components/page-header.astro'; + +export const prerender = true; + +const data = { + hero: { + image: { + public_id: 'codetv-home-hero', + alt: 'A group of developers sitting on bleachers in the Web Dev Challenge demo set, watching two developers present their project', + }, + line1: '“Touch Grass”', + line2: 'Take the Web Dev Challenge', + } +} +--- + + +
+ + +
+ +
+ +
+

The Challenge

+
+

+ So much of the software out there today seems to be reducing our connections — we talk to fewer people, have fewer reasons to go outside, etc. — so let’s build apps that help connect us to the physical world. +

+

+ Face-to-face. IRL. Meatspace. Between no-contact deliveries and every AI app trying to replace another human relationship — from pair programming to therapy to romantic partners — it feels like so much of the technology being produced right now is designed to minimize, or even eliminate, our need to interact with other humans. +

+

+ Let’s do our small part to change that. Your challenge is to build an app that encourages people to connect with the physical world and/or each other. +

+

+ What helps you disconnect from the digital firehose and spend time making offline connections? Go in whatever direction inspires you, whether that’s a hobby, different ways to spend time together with other people, connecting to nature, or something completely different. +

+

+ Your app should help ground your users and reconnect them to the world beyond their screens. +

+
+
+ + +
+
+ + + + From a24296cee3e1911caac6b14876c1cdc1499cdbd4 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Wed, 13 Aug 2025 08:47:39 -0700 Subject: [PATCH 02/55] wip: not responsive, a few blocks left, but closer --- apps/website/src/components/page-header.astro | 1 + apps/website/src/pages/hackathon/[id].astro | 544 ++++++++++++++++++ .../pages/hackathon/touch-grass-apollo.astro | 98 ---- apps/website/src/styles/global.css | 12 +- 4 files changed, 555 insertions(+), 100 deletions(-) create mode 100644 apps/website/src/pages/hackathon/[id].astro delete mode 100644 apps/website/src/pages/hackathon/touch-grass-apollo.astro diff --git a/apps/website/src/components/page-header.astro b/apps/website/src/components/page-header.astro index 9db8313..1357da4 100644 --- a/apps/website/src/components/page-header.astro +++ b/apps/website/src/components/page-header.astro @@ -34,6 +34,7 @@ const imgUrl = createImageUrl(image.public_id, { h1 { font-size: clamp(1rem, 11cqi, 11rem); + font-weight: 400; inline-size: min(90%, 1450px); inset-block-end: 0; line-height: 0.8; diff --git a/apps/website/src/pages/hackathon/[id].astro b/apps/website/src/pages/hackathon/[id].astro new file mode 100644 index 0000000..e627aed --- /dev/null +++ b/apps/website/src/pages/hackathon/[id].astro @@ -0,0 +1,544 @@ +--- +import Layout from '../../layouts/default.astro'; +import PageHeader from '../../components/page-header.astro'; +import YoutubeVideoPlayer from '../../components/youtube-video-player.astro'; + +export const prerender = true; + +export const getStaticPaths = () => { + return [ + { + params: { id: 'touch-grass-apollo' }, + }, + ]; +}; + +const data = { + hero: { + image: { + public_id: 'codetv-home-hero', + alt: 'A group of developers sitting on bleachers in the Web Dev Challenge demo set, watching two developers present their project', + }, + line1: '“Touch Grass”', + line2: 'Take the Web Dev Challenge', + }, + episode: { + youtube_id: 'rvr-xOwrY70', + title: 'Touch Grass — Web Dev Challenge S2.E7', + }, +}; +--- + + +
+ + +
+ + +
+
+

+ So much of the software out there today seems to be reducing our + connections — we talk to fewer people, have fewer reasons to go + outside, etc. — so let’s build apps that help connect us to the + physical world. +

+

+ Face-to-face. IRL. Meatspace. Between no-contact deliveries and + every AI app trying to replace another human relationship — from + pair programming to therapy to romantic partners — it feels like so + much of the technology being produced right now is designed to + minimize, or even eliminate, our need to interact with other humans. +

+

+ Let’s do our small part to change that. Your challenge is to build + an app that encourages people to connect with the physical world + and/or each other. +

+

+ What helps you disconnect from the digital firehose and spend time + making offline connections? Go in whatever direction inspires you, + whether that’s a hobby, different ways to spend time together with + other people, connecting to nature, or something completely + different. +

+

+ Your app should help ground your users and reconnect them to the + world beyond their screens. +

+
+
+
+ +
+

Watch the full episode for inspiration

+
+ +
+ 3 teams of devs tackle this challenge in Web Dev Challenge S2.E7 + Watch the episode → +
+
+
+ +
+

Submit an app to earn rewards

+ +
+
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+ +
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+ +
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+ +
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+ +
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+ +
+ +
+

$50 Credit at the CodeTV Store

+

+ The first 5 submissions get store credit for CodeTV gear and + goodies. +

+
+
+
+
+ + + +
+
+

Resources

+
+ +
+
+

+ Learn more about Apollo Connectors → +

+

+ Read the documentation and see examples of Apollo Connectors in + action. +

+
+ +
+

+ Learn more about Apollo Connectors → +

+

+ Read the documentation and see examples of Apollo Connectors in + action. +

+
+
+
+ +
+
+

The Rules

+
+ +
+
+

#1: Meet the Brief

+

+ Build an app to help people reconnect with the world outside their + screens. +

+
+ +
+

#2: Use the Required Tool

+

+ Include Apollo Connectors as part of your app build. +

+
+ +
+

#3: Find a Friend (or Don’t)

+

+ These challenges are more fun with a friend, or build on your own. +

+
+ +
+

#4: Time-Box the Build

+

+ Take 30 minutes to plan your app, and 4 hours to build it.¹ +

+
+ +
+

#5: Publish the source code

+

+ Release your app’s code on GitHub or a similar open source platform. +

+
+ +
+

+ #6: Submit before 11:59pm @ August 18, 2025 +

+

+ Submit your app before the deadline to qualify for rewards. +

+
+ +
+ ¹ The time limit is intended to make this a quick project that can fit + most schedules. That being said, if you decide to spend more time on + this, we’ll never let a silly thing like rules stand in the way of a + good time. +
+
+
+ +
+ +
+
+
+ + + + diff --git a/apps/website/src/pages/hackathon/touch-grass-apollo.astro b/apps/website/src/pages/hackathon/touch-grass-apollo.astro deleted file mode 100644 index dc3d246..0000000 --- a/apps/website/src/pages/hackathon/touch-grass-apollo.astro +++ /dev/null @@ -1,98 +0,0 @@ ---- -import Layout from '../../layouts/default.astro'; -import Newsletter from '../../components/home/newsletter.astro'; -import PageHeader from '../../components/page-header.astro'; - -export const prerender = true; - -const data = { - hero: { - image: { - public_id: 'codetv-home-hero', - alt: 'A group of developers sitting on bleachers in the Web Dev Challenge demo set, watching two developers present their project', - }, - line1: '“Touch Grass”', - line2: 'Take the Web Dev Challenge', - } -} ---- - - -
- - -
- -
- -
-

The Challenge

-
-

- So much of the software out there today seems to be reducing our connections — we talk to fewer people, have fewer reasons to go outside, etc. — so let’s build apps that help connect us to the physical world. -

-

- Face-to-face. IRL. Meatspace. Between no-contact deliveries and every AI app trying to replace another human relationship — from pair programming to therapy to romantic partners — it feels like so much of the technology being produced right now is designed to minimize, or even eliminate, our need to interact with other humans. -

-

- Let’s do our small part to change that. Your challenge is to build an app that encourages people to connect with the physical world and/or each other. -

-

- What helps you disconnect from the digital firehose and spend time making offline connections? Go in whatever direction inspires you, whether that’s a hobby, different ways to spend time together with other people, connecting to nature, or something completely different. -

-

- Your app should help ground your users and reconnect them to the world beyond their screens. -

-
-
- - -
-
- - - - diff --git a/apps/website/src/styles/global.css b/apps/website/src/styles/global.css index 94e20ac..90fad12 100644 --- a/apps/website/src/styles/global.css +++ b/apps/website/src/styles/global.css @@ -60,7 +60,7 @@ --text-muted: var(--gray-600); --text-emphasized: var(--gray-800); - --font-family: monaspace-argon, system-ui, sans-serif; + --font-family: system-ui, sans-serif; --font-family-heading: heading-font, impact, system-ui, sans-serif; --max-width: 1600px; @@ -77,7 +77,7 @@ body { :is(h1, h2, h3, h4, h5, h6) { color: var(--text-emphasized); - font-weight: normal; + font-weight: 600; letter-spacing: -0.02em; line-height: 1.35; text-wrap: pretty; @@ -157,6 +157,14 @@ a { cursor: not-allowed; opacity: 0.5; } + + &.secondary { + background: var(--bg); + border: 0.5px solid var(--text-muted); + color: var(--text-muted); + line-height: 1; + padding-block: 4px; + } } :is(input, select, textarea, button) { From 93af57380741bf6e1a9c24ac848b31c0c4fb1115 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Wed, 13 Aug 2025 20:15:18 -0700 Subject: [PATCH 03/55] wip: layout is mostly done, ready for data --- .../src/components/design-system/block.astro | 78 +++ .../src/components/design-system/card.astro | 110 ++++ .../src/components/design-system/grid.astro | 38 ++ .../src/components/design-system/list.astro | 15 + apps/website/src/components/page-header.astro | 55 +- apps/website/src/pages/hackathon/[id].astro | 583 ++++++++---------- apps/website/src/styles/global.css | 308 ++++----- 7 files changed, 696 insertions(+), 491 deletions(-) create mode 100644 apps/website/src/components/design-system/block.astro create mode 100644 apps/website/src/components/design-system/card.astro create mode 100644 apps/website/src/components/design-system/grid.astro create mode 100644 apps/website/src/components/design-system/list.astro diff --git a/apps/website/src/components/design-system/block.astro b/apps/website/src/components/design-system/block.astro new file mode 100644 index 0000000..c046af5 --- /dev/null +++ b/apps/website/src/components/design-system/block.astro @@ -0,0 +1,78 @@ +--- +export interface Props { + variant?: 'default' | 'two-column-asymmetrical'; + emphasized?: boolean; + name?: string; +} + +const { variant = 'default', emphasized = false, name } = Astro.props; +--- + +
+ +
+ + diff --git a/apps/website/src/components/design-system/card.astro b/apps/website/src/components/design-system/card.astro new file mode 100644 index 0000000..3424c39 --- /dev/null +++ b/apps/website/src/components/design-system/card.astro @@ -0,0 +1,110 @@ +--- +export interface Props { + title?: string; + variant?: 'primary' | 'secondary'; +} + +const { title, variant = 'primary' } = Astro.props; +--- + +
+ +
+
{title ?

{title}

: }
+
+ +
+
+
+ + diff --git a/apps/website/src/components/design-system/grid.astro b/apps/website/src/components/design-system/grid.astro new file mode 100644 index 0000000..38c757c --- /dev/null +++ b/apps/website/src/components/design-system/grid.astro @@ -0,0 +1,38 @@ +--- +export interface Props { + columns?: number; +} + +const { columns = 3 } = Astro.props; +--- + +
+ +
+ + diff --git a/apps/website/src/components/design-system/list.astro b/apps/website/src/components/design-system/list.astro new file mode 100644 index 0000000..1090098 --- /dev/null +++ b/apps/website/src/components/design-system/list.astro @@ -0,0 +1,15 @@ +--- + +--- + +
+ +
+ + diff --git a/apps/website/src/components/page-header.astro b/apps/website/src/components/page-header.astro index 1357da4..b39ddd6 100644 --- a/apps/website/src/components/page-header.astro +++ b/apps/website/src/components/page-header.astro @@ -29,35 +29,38 @@ const imgUrl = createImageUrl(image.public_id, { diff --git a/apps/website/src/pages/hackathon/[id].astro b/apps/website/src/pages/hackathon/[id].astro index e627aed..d2d02bf 100644 --- a/apps/website/src/pages/hackathon/[id].astro +++ b/apps/website/src/pages/hackathon/[id].astro @@ -2,6 +2,10 @@ import Layout from '../../layouts/default.astro'; import PageHeader from '../../components/page-header.astro'; import YoutubeVideoPlayer from '../../components/youtube-video-player.astro'; +import Grid from '../../components/design-system/grid.astro'; +import Card from '../../components/design-system/card.astro'; +import List from '../../components/design-system/list.astro'; +import Block from '../../components/design-system/block.astro'; export const prerender = true; @@ -26,6 +30,129 @@ const data = { youtube_id: 'rvr-xOwrY70', title: 'Touch Grass — Web Dev Challenge S2.E7', }, + rewards: [ + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124332/reward-store.png', + alt: 'a pile of rainbow corgi rubber duck toys', + }, + title: '$50 Credit at the CodeTV Store', + description: + 'The first 5 submissions get store credit for CodeTV gear and goodies.', + }, + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124333/reward-swag.png', + alt: 'close-up of the Apollo logo on a t-shirt', + }, + title: 'Apollo Swag Pack', + description: + 'The Apollo team will send the first 5 teams a GraphQL swag pack.', + }, + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124332/reward-gear.png', + alt: 'assortment of Drop.com keyboards, mice, and desk accessories arranged on a desk', + }, + title: '$50 Credit at Drop.com', + description: 'Get keyboards, desk accessories, and more.', + }, + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124332/reward-badge.png', + alt: 'example badges from a GitHub profile (FPO)', + }, + title: 'Exclusive Member Badge', + description: 'Builders get an exclusive badge on their CodeTV profile.', + }, + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124332/reward-role.png', + alt: 'badges displayed on a Discord profile (FPO)', + }, + title: 'Exclusive Discord Role', + description: 'Builders get an exclusive role in the CodeTV Discord.', + }, + { + img: { + src: 'https://res.cloudinary.com/jlengstorf/image/upload/v1755124332/reward-showcase.png', + alt: 'a developer demoing their hackathon app on a video call', + }, + title: 'Showcase Your App on CodeTV', + description: 'Demo your app in the hackathon showcase.', + }, + ], + resources: [ + { + link: '#resource', + title: 'Learn more about Apollo Connectors →', + description: + 'Read the documentation and see examples of Apollo Connectors in action.', + }, + { + link: 'https://codetv.link/discord', + title: 'Join the CodeTV Discord →', + description: + 'Connect with other devs who are in the challenge, get support from experts.', + }, + ], + rules: [ + { + title: '#1: Meet the Brief', + description: + 'Build an app to help people reconnect with the world outside their screens.', + }, + { + title: '#2: Use the Required Tool', + description: 'Include Apollo Connectors as part of your app build.', + }, + { + title: '#3: Find a Friend (or Don’t)', + description: + 'These challenges are more fun with a friend, or build on your own.', + }, + { + title: '#4: Time-Box the Build', + description: + 'Take 30 minutes to plan your app, and 4 hours to build it.¹', + finePrint: + '¹ The time limit is intended to make this a quick project that can fit most schedules. That being said, if you decide to spend more time on this, we’ll never let a silly thing like rules stand in the way of a good time.', + }, + { + title: '#5: Publish the source code', + description: + 'Release your app’s code on GitHub or a similar open source platform.', + }, + { + title: '#6: Submit before 11:59pm @ August 18, 2025', + description: + 'Submit your app before the deadline to qualify for rewards.', + }, + ], + faqs: [ + { + title: 'Are teams allowed?', + description: [ + 'Yes! While building solo is absolutely fine, we encourage teams of 2 for these challenges. Part of the fun is collaborating with other people.', + 'However, please note that while teams of any size are allowed, each project will receive a maximum of 2 of each monetary or product reward.', + ], + }, + { + title: 'Am I allowed to use AI?', + description: [ + 'You’re allowed to use any tool that you would use as part of your regular workflow. We only ask that you try to keep in the spirit of the challenge, which is to learn something new.', + 'We do reserve the right to deny rewards to any submissions that appear to be entirely generated by AI, as that violates the spirit of the challenge.', + ], + }, + { + title: + 'I’m not sure I have enough experience to build this. Should I still try?', + description: [ + 'Yes! These challenges are perfect for trying something new with a limited time commitment and low stakes. The only way to learn stuff is to do stuff!', + 'This is also a great opportunity to pair with someone and help each other level up. Ask a friend, or hit up the CodeTV Discord to find someone to work with.', + ], + }, + ], }; --- @@ -37,29 +164,31 @@ const data = { line2={data.hero.line2} /> -
- - -
+ + + + + +

So much of the software out there today seems to be reducing our @@ -92,9 +221,9 @@ const data = {

-
+ -
+

Watch the full episode for inspiration

Watch the episode →
-
+ -
+

Submit an app to earn rewards

-
-
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
- -
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
- -
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
- -
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
- -
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
+ + { + data.rewards.map((reward) => ( + + {reward.img.alt} +

{reward.description}

+
+ )) + } +
+ + + +
+

+ This challenge is brought to you by + +

-
- -
-

$50 Credit at the CodeTV Store

-

- The first 5 submissions get store credit for CodeTV gear and - goodies. -

-
-
+ + learn more → +
-
- - - -
+ + +

Resources

-
-
-

- Learn more about Apollo Connectors → -

-

- Read the documentation and see examples of Apollo Connectors in - action. -

-
- -
-

- Learn more about Apollo Connectors → -

-

- Read the documentation and see examples of Apollo Connectors in - action. -

-
-
-
+ + { + data.resources.map((resource) => ( + +

+ {resource.title} +

+

{resource.description}

+
+ )) + } +
+ -
+

The Rules

-
-
-

#1: Meet the Brief

-

- Build an app to help people reconnect with the world outside their - screens. -

-
- -
-

#2: Use the Required Tool

-

- Include Apollo Connectors as part of your app build. -

-
- -
-

#3: Find a Friend (or Don’t)

-

- These challenges are more fun with a friend, or build on your own. -

-
- -
-

#4: Time-Box the Build

-

- Take 30 minutes to plan your app, and 4 hours to build it.¹ -

-
+ + { + data.rules.map((rule) => ( + +

{rule.description}

+
+ )) + } -
-

#5: Publish the source code

-

- Release your app’s code on GitHub or a similar open source platform. -

+
+ { + data.rules + .filter((r) => r.finePrint) + .map((rule) =>

{rule.finePrint}

) + }
+ + -
-

- #6: Submit before 11:59pm @ August 18, 2025 -

-

- Submit your app before the deadline to qualify for rewards. -

-
+ +

Show us what you got!

+
-
- ¹ The time limit is intended to make this a quick project that can fit - most schedules. That being said, if you decide to spend more time on - this, we’ll never let a silly thing like rules stand in the way of a - good time. -
+ +
+

FAQ

-
-
- -
+ + { + data.faqs.map((q) => ( + + {q.description.map((d) => ( +

{d}

+ ))} +
+ )) + } +
+ diff --git a/apps/website/src/styles/global.css b/apps/website/src/styles/global.css index 90fad12..7118c10 100644 --- a/apps/website/src/styles/global.css +++ b/apps/website/src/styles/global.css @@ -1,3 +1,5 @@ +@layer design-system, global, site-base, overrides; + @font-face { font-display: swap; font-family: 'heading-font'; @@ -20,196 +22,198 @@ font-style: oblique 0deg 3deg; } -* { - box-sizing: border-box; - margin: 0; -} - -:root { - --green-500: oklch(79.17% 0.2297 158.55); +@layer global { + * { + box-sizing: border-box; + margin: 0; + } - --orange-500: oklch(72.96% 0.127 36.93); + :root { + --green-500: oklch(79.17% 0.2297 158.55); - --yellow-500: oklch(90.64% 0.17 106.57); + --orange-500: oklch(72.96% 0.127 36.93); - --purple-100: oklch(92.63% 0.116 309.73); - --purple-500: oklch(62.12% 0.259 305.9); - --purple-600: oklch(56.68% 0.281 302.97); + --yellow-500: oklch(90.64% 0.17 106.57); - --blue-500: oklch(56.81% 0.154 252.65); - --blue-600: oklch(46.53% 0.155 255.48); + --purple-100: oklch(92.63% 0.116 309.73); + --purple-500: oklch(62.12% 0.259 305.9); + --purple-600: oklch(56.68% 0.281 302.97); - --text-strong: var(--gray-600); - --text-link: var(--blue-500); + --blue-500: oklch(56.81% 0.154 252.65); + --blue-600: oklch(46.53% 0.155 255.48); - /* UPDATED COLOR PALETTE */ - --gray-000: #080217; - --gray-100: #18151f; - --gray-600: #79777e; - --gray-700: #b5b1be; - --gray-800: #ede9f6; - --gray-900: #fff; + --text-strong: var(--gray-600); + --text-link: var(--blue-500); - --orange: #f0b525; - --blue: #647cf6; + /* UPDATED COLOR PALETTE */ + --gray-000: #080217; + --gray-100: #18151f; + --gray-600: #79777e; + --gray-700: #b5b1be; + --gray-800: #ede9f6; + --gray-900: #fff; - --black: var(--gray-000); - --white: var(--gray-900); - --bg: var(--gray-100); - --text: var(--gray-700); - --text-muted: var(--gray-600); - --text-emphasized: var(--gray-800); + --orange: #f0b525; + --blue: #647cf6; - --font-family: system-ui, sans-serif; - --font-family-heading: heading-font, impact, system-ui, sans-serif; + --black: var(--gray-000); + --white: var(--gray-900); + --bg: var(--gray-100); + --text: var(--gray-700); + --text-muted: var(--gray-600); + --text-emphasized: var(--gray-800); - --max-width: 1600px; + --font-family: system-ui, sans-serif; + --font-family-heading: heading-font, impact, system-ui, sans-serif; - color: var(--text); - font-family: var(--font-family); - line-height: 1.45; -} + --max-width: 1600px; -body { - background: var(--bg); - margin: 0; -} - -:is(h1, h2, h3, h4, h5, h6) { - color: var(--text-emphasized); - font-weight: 600; - letter-spacing: -0.02em; - line-height: 1.35; - text-wrap: pretty; + color: var(--text); + font-family: var(--font-family); + line-height: 1.45; + } - &:not(:first-child) { - margin-block-start: 40px; + body { + background: var(--bg); + margin: 0; } -} -h1 { - font-family: var(--font-family-heading); - font-size: clamp(1.5em, 5cqi, 2.5em); - line-height: 0.9; -} + :is(h1, h2, h3, h4, h5, h6) { + color: var(--text-emphasized); + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.35; + text-wrap: pretty; -h2 { - font-size: clamp(1.25em, 3.5cqi, 1.75em); -} + &:not(:first-child) { + margin-block-start: 40px; + } + } -h3 { - font-size: clamp(1.125em, 3cqi, 1.875em); -} + h1 { + font-family: var(--font-family-heading); + font-size: clamp(1.5em, 5cqi, 2.5em); + line-height: 0.9; + } -a { - color: var(--blue); -} + h2 { + font-size: clamp(1.25em, 3.5cqi, 1.75em); + } -:is(strong, b) { - color: var(--text-emphasized); -} + h3 { + font-size: clamp(1.125em, 3cqi, 1.875em); + } -.row { - display: flex; - gap: 20px; - justify-content: start; - padding-inline: max((100% - var(--max-width)) / 2, 5%); -} + a { + color: var(--blue); + } -.three-up { - display: grid; - gap: 80px; - grid-template-columns: 100%; + :is(strong, b) { + color: var(--text-emphasized); + } - @media (min-width: 500px) { + .row { + display: flex; gap: 20px; - grid-template-columns: repeat(2, 1fr); + justify-content: start; + padding-inline: max((100% - var(--max-width)) / 2, 5%); } - @media (min-width: 750px) { - grid-template-columns: repeat(3, 1fr); + .three-up { + display: grid; + gap: 80px; + grid-template-columns: 100%; + + @media (min-width: 500px) { + gap: 20px; + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 750px) { + grid-template-columns: repeat(3, 1fr); + } } -} -.button { - background: var(--white); - border: none; - border-radius: 3px; - color: var(--black); - display: inline-block; - font-family: var(--font-family-heading); - font-size: clamp(0.875em, 6cqi, 1.25em); - font-weight: normal; - line-height: 0.9; - margin-block-start: 16px; - padding: 8px 12px; - text-decoration: none; - transition: 150ms background linear; - - &:is(:active, :focus, :hover) { - background: var(--orange); + .button { + background: var(--white); + border: none; + border-radius: 3px; color: var(--black); + display: inline-block; + font-family: var(--font-family-heading); + font-size: clamp(0.875em, 6cqi, 1.25em); + font-weight: normal; + line-height: 0.9; + margin-block-start: 16px; + padding: 8px 12px; text-decoration: none; + transition: 150ms background linear; + + &:is(:active, :focus, :hover) { + background: var(--orange); + color: var(--black); + text-decoration: none; + } + + &:disabled { + background: var(--white); + cursor: not-allowed; + opacity: 0.5; + } + + &.secondary { + background: var(--bg); + border: 0.5px solid var(--text-muted); + color: var(--text-muted); + line-height: 1; + padding-block: 4px; + } } - &:disabled { - background: var(--white); - cursor: not-allowed; - opacity: 0.5; + :is(input, select, textarea, button) { + font-family: var(--font-family); + font-size: 1rem; + font-weight: 400; } - &.secondary { - background: var(--bg); - border: 0.5px solid var(--text-muted); - color: var(--text-muted); - line-height: 1; - padding-block: 4px; + button { + cursor: pointer; } -} -:is(input, select, textarea, button) { - font-family: var(--font-family); - font-size: 1rem; - font-weight: 400; -} - -button { - cursor: pointer; -} - -.badge { - background: var(--blue); - border-radius: 3px; - color: var(--black); - display: inline-block; - font-size: 0.625rem; - font-weight: 900; - letter-spacing: 0.1em; - line-height: 1; - padding: 1px 4px 1px 5px; - text-box-trim: trim-both; - text-spacing-trim: space-all; - text-transform: uppercase; - - &.orange { - background: var(--orange); + .badge { + background: var(--blue); + border-radius: 3px; + color: var(--black); + display: inline-block; + font-size: 0.625rem; + font-weight: 900; + letter-spacing: 0.1em; + line-height: 1; + padding: 1px 4px 1px 5px; + text-box-trim: trim-both; + text-spacing-trim: space-all; + text-transform: uppercase; + + &.orange { + background: var(--orange); + } } -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; - white-space: nowrap; /* Prevents wrapping */ -} + .sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + white-space: nowrap; /* Prevents wrapping */ + } -.cl-rootBox { - margin: 4rem auto; + .cl-rootBox { + margin: 4rem auto; + } } @keyframes pulse { From 93470ba02a7c70bcd6a33334b4a5edd19fa3c942 Mon Sep 17 00:00:00 2001 From: Creeland Date: Mon, 6 Oct 2025 11:52:08 -0500 Subject: [PATCH 04/55] wip: add hackathon document schema and integrate into content types --- apps/content/schema.json | 115 +++++++++++++++++ .../schemaTypes/documents/hackathon.ts | 116 ++++++++++++++++++ apps/content/schemaTypes/index.ts | 12 +- 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 apps/content/schemaTypes/documents/hackathon.ts diff --git a/apps/content/schema.json b/apps/content/schema.json index 69b647e..1eaac4c 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -411,6 +411,121 @@ } } }, + { + "name": "hackathon", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "hackathon" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "pubDate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "body": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "share_image": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "cloudinary.asset" + }, + "optional": true + }, + "hidden": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "visible" + }, + { + "type": "string", + "value": "hidden" + } + ] + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "featured" + } + ] + }, + "optional": true + } + } + }, { "name": "episode", "type": "document", diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts new file mode 100644 index 0000000..38d2fe3 --- /dev/null +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -0,0 +1,116 @@ +import {defineField, defineType} from 'sanity' +import {RocketIcon} from '@sanity/icons' + +export const hackathon = defineType({ + name: 'hackathon', + type: 'document', + title: 'Hackathon', + icon: RocketIcon, + groups: [ + {name: 'content', title: 'Content', default: true}, + {name: 'seo', title: 'SEO & Publishing'}, + ], + fields: [ + defineField({ + name: 'title', + type: 'string', + validation: (Rule) => Rule.required().error('Hackathon title is required'), + group: 'content', + }), + defineField({ + name: 'slug', + type: 'slug', + description: 'URL-friendly identifier for this hackathon', + options: { + source: 'title', + maxLength: 96, + }, + validation: (Rule) => Rule.required().error('Slug is required for URL generation'), + group: 'content', + }), + defineField({ + name: 'pubDate', + type: 'datetime', + title: 'Publish Date', + description: 'When this hackathon announcement should be published', + options: { + timeStep: 30, + }, + validation: (Rule) => Rule.required().error('Publish date is required'), + group: 'content', + }), + defineField({ + name: 'description', + type: 'text', + description: 'Brief overview for previews and SEO', + validation: (Rule) => Rule.required().error('Description is required for SEO'), + group: 'content', + }), + defineField({ + name: 'body', + type: 'markdown', + description: 'Full hackathon details, rules, and content', + validation: (Rule) => Rule.required().error('Body content is required'), + group: 'content', + }), + defineField({ + name: 'share_image', + type: 'cloudinary.asset', + title: 'Share Image', + description: 'Image for social media sharing', + options: {hotspot: true}, + validation: (Rule) => Rule.required().error('Share image is required for social media'), + group: 'seo', + }), + defineField({ + name: 'hidden', + type: 'string', + description: 'Control whether this hackathon appears on the website', + options: { + list: [ + {title: 'Visible', value: 'visible'}, + {title: 'Hidden', value: 'hidden'}, + ], + layout: 'radio', + }, + initialValue: 'visible', + group: 'seo', + }), + defineField({ + name: 'featured', + type: 'string', + options: { + list: [ + {title: 'Normal', value: 'normal'}, + {title: 'Featured', value: 'featured'}, + ], + layout: 'radio', + }, + initialValue: 'normal', + group: 'seo', + }), + ], + preview: { + select: { + title: 'title', + pubDate: 'pubDate', + hidden: 'hidden', + featured: 'featured', + }, + prepare({title, pubDate, hidden, featured}) { + const date = pubDate ? new Date(pubDate).toLocaleDateString() : 'No date' + const status = hidden === 'hidden' ? ' (Hidden)' : '' + const featuredStatus = featured === 'featured' ? ' ⭐' : '' + + return { + title: title || 'Untitled Hackathon', + subtitle: `${date}${status}${featuredStatus}`, + media: RocketIcon, + } + }, + }, + initialValue: () => ({ + hidden: 'visible', + featured: 'normal', + }), +}) diff --git a/apps/content/schemaTypes/index.ts b/apps/content/schemaTypes/index.ts index 43cd866..82acda3 100644 --- a/apps/content/schemaTypes/index.ts +++ b/apps/content/schemaTypes/index.ts @@ -2,6 +2,7 @@ import {defineField, defineType} from 'sanity' import {PlayIcon, UserIcon, TagIcon, ImageIcon, FolderIcon, StarIcon} from '@sanity/icons' import {person} from './documents/person' import {episode} from './documents/episode' +import {hackathon} from './documents/hackathon' import {episodeTag} from './documents/tags' import {episodeImage} from './objects/episode-image' @@ -253,4 +254,13 @@ const sponsor = defineType({ }, }) -export const schemaTypes = [series, collection, episode, person, sponsor, episodeTag, episodeImage] +export const schemaTypes = [ + series, + collection, + episode, + hackathon, + person, + sponsor, + episodeTag, + episodeImage, +] From e9930358e9400349611909799c90fdb3e651dfa0 Mon Sep 17 00:00:00 2001 From: Creeland Date: Tue, 7 Oct 2025 11:12:27 -0500 Subject: [PATCH 05/55] fix: comment out required validation for share image in hackathon schema --- apps/content/schemaTypes/documents/hackathon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index 38d2fe3..d3e7cc5 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -59,7 +59,7 @@ export const hackathon = defineType({ title: 'Share Image', description: 'Image for social media sharing', options: {hotspot: true}, - validation: (Rule) => Rule.required().error('Share image is required for social media'), + // validation: (Rule) => Rule.required().error('Share image is required for social media'), group: 'seo', }), defineField({ From 201f2197e02c2caa2b14ed905ffe99b4121d1c05 Mon Sep 17 00:00:00 2001 From: Creeland Date: Tue, 7 Oct 2025 11:12:37 -0500 Subject: [PATCH 06/55] feat: enhance content structure by adding 'Episodes' list item to sanity configuration --- apps/content/sanity.config.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/content/sanity.config.ts b/apps/content/sanity.config.ts index f7834d0..2ff8a68 100644 --- a/apps/content/sanity.config.ts +++ b/apps/content/sanity.config.ts @@ -24,14 +24,19 @@ export default defineConfig({ structure: (S) => { return S.list() .title('Content') - .items( - S.documentTypeListItems().filter( + .items([ + ...S.documentTypeListItems().filter( (li) => !['Episode', 'Collection', 'Episode Tag', 'Video asset'].includes( li.getTitle() ?? '', ), ), - ) + S.divider(), + S.listItem() + .title('Episodes') + .schemaType('episode') + .child(S.documentTypeList('episode').title('Episodes')), + ]) }, }), ], From 7224ad1e9493f1ca845055a5e5c4440777ea5c91 Mon Sep 17 00:00:00 2001 From: Creeland Date: Tue, 7 Oct 2025 11:12:48 -0500 Subject: [PATCH 07/55] feat: add 'hackathons' field to episode schema for related hackathon associations --- apps/content/schema.json | 44 +++++++++++++++++++ apps/content/schemaTypes/documents/episode.ts | 8 ++++ 2 files changed, 52 insertions(+) diff --git a/apps/content/schema.json b/apps/content/schema.json index 1eaac4c..8157afe 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -685,6 +685,50 @@ }, "optional": true }, + "hackathons": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "hackathon", + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, "resources": { "type": "objectAttribute", "value": { diff --git a/apps/content/schemaTypes/documents/episode.ts b/apps/content/schemaTypes/documents/episode.ts index fe8c4a1..7cc5146 100644 --- a/apps/content/schemaTypes/documents/episode.ts +++ b/apps/content/schemaTypes/documents/episode.ts @@ -66,6 +66,14 @@ export const episode = defineType({ of: [defineArrayMember({type: 'reference', to: [{type: 'sponsor'}]})], group: 'details', }), + defineField({ + name: 'hackathons', + type: 'array', + title: 'Related Hackathons', + description: 'Hackathons that this episode is associated with', + of: [defineArrayMember({type: 'reference', to: [{type: 'hackathon'}]})], + group: 'details', + }), defineField({ name: 'resources', type: 'array', From 29b49e1a06ef40ccfb4b71d2c91787dcb3257ae6 Mon Sep 17 00:00:00 2001 From: Creeland Date: Tue, 14 Oct 2025 15:31:15 -0500 Subject: [PATCH 08/55] feat: add 'episodes' field to hackathon schema for related episode associations --- apps/content/schema.json | 44 ++++++++ .../schemaTypes/documents/hackathon.ts | 22 +++- libs/types/src/sanity.ts | 105 ++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/apps/content/schema.json b/apps/content/schema.json index 8157afe..1ddc135 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -482,6 +482,50 @@ }, "optional": true }, + "episodes": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "episode", + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, "share_image": { "type": "objectAttribute", "value": { diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index d3e7cc5..735a465 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -1,5 +1,5 @@ -import {defineField, defineType} from 'sanity' -import {RocketIcon} from '@sanity/icons' +import {defineArrayMember, defineField, defineType} from 'sanity' +import {RocketIcon, PlayIcon} from '@sanity/icons' export const hackathon = defineType({ name: 'hackathon', @@ -53,13 +53,29 @@ export const hackathon = defineType({ validation: (Rule) => Rule.required().error('Body content is required'), group: 'content', }), + defineField({ + name: 'episodes', + type: 'array', + title: 'Related Episodes', + description: 'Episodes associated with this hackathon', + of: [ + defineArrayMember({ + type: 'reference', + to: [{type: 'episode'}], + options: { + disableNew: true, + }, + }), + ], + group: 'content', + }), defineField({ name: 'share_image', type: 'cloudinary.asset', title: 'Share Image', description: 'Image for social media sharing', options: {hotspot: true}, - // validation: (Rule) => Rule.required().error('Share image is required for social media'), + validation: (Rule) => Rule.required().error('Share image is required for social media'), group: 'seo', }), defineField({ diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index 0ae13c4..c0bfdaa 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -87,6 +87,29 @@ export type Person = { user_id?: string; }; +export type Hackathon = { + _id: string; + _type: 'hackathon'; + _createdAt: string; + _updatedAt: string; + _rev: string; + title?: string; + slug?: Slug; + pubDate?: string; + description?: string; + body?: string; + episodes?: Array<{ + _ref: string; + _type: 'reference'; + _weak?: boolean; + _key: string; + [internalGroqTypeReferenceTo]?: 'episode'; + }>; + share_image?: CloudinaryAsset; + hidden?: 'visible' | 'hidden'; + featured?: 'normal' | 'featured'; +}; + export type Episode = { _id: string; _type: 'episode'; @@ -112,6 +135,13 @@ export type Episode = { _key: string; [internalGroqTypeReferenceTo]?: 'sponsor'; }>; + hackathons?: Array<{ + _ref: string; + _type: 'reference'; + _weak?: boolean; + _key: string; + [internalGroqTypeReferenceTo]?: 'hackathon'; + }>; resources?: Array<{ label?: string; url?: string; @@ -447,6 +477,7 @@ export type AllSanitySchemaTypes = | EpisodeTag | Sponsor | Person + | Hackathon | Episode | Collection | Series @@ -918,6 +949,78 @@ export type SupportersQueryResult = Array<{ | null; } | null; }>; +// Variable: allHackathonsQuery +// Query: *[_type == "hackathon" && hidden != "hidden"] | order(pubDate desc) { _id, title, 'slug': slug.current, pubDate, description, body, episodes[]-> { _id, 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, } }, share_image { public_id, width, height, }, featured, hidden } +export type AllHackathonsQueryResult = Array<{ + _id: string; + title: string | null; + slug: string | null; + pubDate: string | null; + description: string | null; + body: string | null; + episodes: Array<{ + _id: string; + title: string | null; + slug: string | null; + short_description: string | null; + publish_date: string | null; + thumbnail: { + public_id: string | null; + width: number | null; + height: number | null; + alt: string | null; + }; + }> | null; + share_image: { + public_id: string | null; + width: number | null; + height: number | null; + } | null; + featured: 'featured' | 'normal' | null; + hidden: 'hidden' | 'visible' | null; +}>; +// Variable: hackathonBySlugQuery +// Query: *[_type == "hackathon" && slug.current == $slug][0] { _id, title, 'slug': slug.current, pubDate, description, body, episodes[]-> { _id, 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, mux_video, }, 'collection': *[_type=="collection" && references(^._id)][0] { 'slug': slug.current, title, }, 'series': *[_type=="collection" && references(^._id)][0].series->{ 'slug': slug.current, title, }, }, share_image { public_id, width, height, }, featured, hidden } +export type HackathonBySlugQueryResult = { + _id: string; + title: string | null; + slug: string | null; + pubDate: string | null; + description: string | null; + body: string | null; + episodes: Array<{ + _id: string; + title: string | null; + slug: string | null; + short_description: string | null; + publish_date: string | null; + thumbnail: { + public_id: string | null; + width: number | null; + height: number | null; + alt: string | null; + }; + video: { + youtube_id: string | null; + mux_video: MuxVideo | null; + } | null; + collection: { + slug: string | null; + title: string | null; + } | null; + series: { + slug: string | null; + title: string | null; + } | null; + }> | null; + share_image: { + public_id: string | null; + width: number | null; + height: number | null; + } | null; + featured: 'featured' | 'normal' | null; + hidden: 'hidden' | 'visible' | null; +} | null; // Query TypeMap import '@sanity/client'; @@ -937,5 +1040,7 @@ declare module '@sanity/client' { "\n *[_type == \"person\" && slug.current == $slug][0] {\n _id,\n name,\n \"slug\": slug.current,\n photo {\n public_id,\n height,\n width,\n },\n bio,\n links[],\n user_id,\n \"episodes\": *[_type == \"episode\" && references(^._id) && 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 'alt': video.thumbnail_alt,\n 'width': video.thumbnail.width,\n 'height': video.thumbnail.height,\n },\n video {\n youtube_id,\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 } | order(publish_date desc)[0...6]\n }\n": PersonBySlugQueryResult; '\n *[_type == "person" && user_id == $user_id][0] {\n _id,\n name,\n slug,\n user_id,\n }\n': PersonByClerkIdQueryResult; '\n *[_type == "person" && subscription.status == "active"] | order(subscription.date asc) {\n _id,\n name,\n photo {\n public_id,\n height,\n width,\n },\n \'username\': slug.current,\n subscription {\n level,\n status\n }\n }\n': SupportersQueryResult; + "\n *[_type == \"hackathon\" && hidden != \"hidden\"] | order(pubDate desc) {\n _id,\n title,\n 'slug': slug.current,\n pubDate,\n description,\n body,\n episodes[]-> {\n _id,\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 },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": AllHackathonsQueryResult; + "\n *[_type == \"hackathon\" && slug.current == $slug][0] {\n _id,\n title,\n 'slug': slug.current,\n pubDate,\n description,\n body,\n episodes[]-> {\n _id,\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 mux_video,\n },\n 'collection': *[_type==\"collection\" && references(^._id)][0] {\n 'slug': slug.current,\n title,\n },\n 'series': *[_type==\"collection\" && references(^._id)][0].series->{\n 'slug': slug.current,\n title,\n },\n },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": HackathonBySlugQueryResult; } } From f5ce4d31c0cdde76545b2e60272a48d2020fdd12 Mon Sep 17 00:00:00 2001 From: Creeland Date: Tue, 14 Oct 2025 15:31:22 -0500 Subject: [PATCH 09/55] feat: add queries for fetching all hackathons and individual hackathon by slug --- libs/sanity/src/index.ts | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/libs/sanity/src/index.ts b/libs/sanity/src/index.ts index b166ec7..04ec523 100644 --- a/libs/sanity/src/index.ts +++ b/libs/sanity/src/index.ts @@ -15,6 +15,8 @@ import type { RecentEpisodesQueryResult, FeaturedSeriesQueryResult, AllUsersQueryResult, + AllHackathonsQueryResult, + HackathonBySlugQueryResult, } from '@codetv/types'; import type { UploadApiResponse } from '@codetv/cloudinary'; @@ -472,6 +474,80 @@ const supportersQuery = groq` } `; +const allHackathonsQuery = groq` + *[_type == "hackathon" && hidden != "hidden"] | order(pubDate desc) { + _id, + title, + 'slug': slug.current, + pubDate, + description, + body, + episodes[]-> { + _id, + 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, + } + }, + share_image { + public_id, + width, + height, + }, + featured, + hidden + } +`; + +const hackathonBySlugQuery = groq` + *[_type == "hackathon" && slug.current == $slug][0] { + _id, + title, + 'slug': slug.current, + pubDate, + description, + body, + episodes[]-> { + _id, + 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, + mux_video, + }, + 'collection': *[_type=="collection" && references(^._id)][0] { + 'slug': slug.current, + title, + }, + 'series': *[_type=="collection" && references(^._id)][0].series->{ + 'slug': slug.current, + title, + }, + }, + share_image { + public_id, + width, + height, + }, + featured, + hidden + } +`; + export async function getAllSeries() { return client.fetch( allSeriesQuery, @@ -639,6 +715,28 @@ export async function getSupporters() { ); } +export async function getAllHackathons() { + return client.fetch( + allHackathonsQuery, + {}, + { useCdn: true }, + ); +} + +export async function getHackathonBySlug(params: { slug: string }) { + const hackathon = await client.fetch( + hackathonBySlugQuery, + params, + { useCdn: true }, + ); + + if (!hackathon) { + throw new Error(`Invalid hackathon ${params.slug}`); + } + + return hackathon; +} + export async function createPerson( name: string, user_id: string, From a03987fa4bb80ae9ed05c5c725945505ebb8e4a3 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 09:51:57 -0500 Subject: [PATCH 10/55] feat: add FAQ document schema and integrate into content structure --- apps/content/sanity.config.ts | 4 ++ apps/content/schemaTypes/documents/faq.ts | 49 ++++++++++++++++++++ apps/content/schemaTypes/index.ts | 4 ++ apps/content/schemaTypes/objects/faq-item.ts | 34 ++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 apps/content/schemaTypes/documents/faq.ts create mode 100644 apps/content/schemaTypes/objects/faq-item.ts diff --git a/apps/content/sanity.config.ts b/apps/content/sanity.config.ts index 2ff8a68..2bb2694 100644 --- a/apps/content/sanity.config.ts +++ b/apps/content/sanity.config.ts @@ -36,6 +36,10 @@ export default defineConfig({ .title('Episodes') .schemaType('episode') .child(S.documentTypeList('episode').title('Episodes')), + S.listItem() + .title('FAQs') + .schemaType('faq') + .child(S.documentTypeList('faq').title('FAQs')), ]) }, }), diff --git a/apps/content/schemaTypes/documents/faq.ts b/apps/content/schemaTypes/documents/faq.ts new file mode 100644 index 0000000..282bae8 --- /dev/null +++ b/apps/content/schemaTypes/documents/faq.ts @@ -0,0 +1,49 @@ +import {defineField, defineType} from 'sanity' + +export const faq = defineType({ + name: 'faq', + type: 'document', + title: 'FAQ', + fields: [ + defineField({ + name: 'name', + type: 'string', + title: 'Name', + description: 'Internal name for this FAQ collection', + validation: (Rule) => Rule.required().error('Name is required'), + }), + defineField({ + name: 'slug', + type: 'slug', + title: 'Slug', + description: 'URL-friendly identifier for this FAQ', + options: { + source: 'name', + maxLength: 96, + }, + validation: (Rule) => Rule.required().error('Slug is required for URL generation'), + }), + defineField({ + name: 'items', + title: 'FAQ Items', + type: 'array', + description: 'Frequently Asked Questions (auto-generated or manually added for SEO)', + of: [{type: 'faqItem'}], + options: { + sortable: true, + }, + }), + ], + preview: { + select: { + title: 'name', + itemCount: 'items.length', + }, + prepare({title, itemCount}) { + return { + title: title || 'Untitled FAQ', + subtitle: `${itemCount || 0} items`, + } + }, + }, +}) diff --git a/apps/content/schemaTypes/index.ts b/apps/content/schemaTypes/index.ts index 82acda3..08abf07 100644 --- a/apps/content/schemaTypes/index.ts +++ b/apps/content/schemaTypes/index.ts @@ -3,8 +3,10 @@ import {PlayIcon, UserIcon, TagIcon, ImageIcon, FolderIcon, StarIcon} from '@san import {person} from './documents/person' import {episode} from './documents/episode' import {hackathon} from './documents/hackathon' +import {faq} from './documents/faq' import {episodeTag} from './documents/tags' import {episodeImage} from './objects/episode-image' +import faqItem from './objects/faq-item' function slugify(str: string) { return String(str) @@ -259,8 +261,10 @@ export const schemaTypes = [ collection, episode, hackathon, + faq, person, sponsor, episodeTag, episodeImage, + faqItem, ] diff --git a/apps/content/schemaTypes/objects/faq-item.ts b/apps/content/schemaTypes/objects/faq-item.ts new file mode 100644 index 0000000..6426d98 --- /dev/null +++ b/apps/content/schemaTypes/objects/faq-item.ts @@ -0,0 +1,34 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'faqItem', + title: 'FAQ Item', + type: 'object', + fields: [ + defineField({ + name: 'question', + title: 'Question', + type: 'string', + validation: (Rule) => Rule.required().min(10).max(200), + }), + defineField({ + name: 'answer', + title: 'Answer', + type: 'text', + validation: (Rule) => Rule.required().min(20).max(1000), + }), + ], + preview: { + select: { + title: 'question', + subtitle: 'answer', + }, + prepare(selection) { + const {title, subtitle} = selection + return { + title, + subtitle: subtitle ? `${subtitle.substring(0, 60)}...` : '', + } + }, + }, +}) From 411cef86a474a57aa30e3943a2354c5338c0599d Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 10:45:33 -0500 Subject: [PATCH 11/55] feat: add Rewards and FAQ document schemas, enhance content structure with new list items --- apps/content/sanity.config.ts | 6 +- apps/content/schema.json | 216 ++++++++++++++++++ apps/content/schemaTypes/documents/faq.ts | 9 +- apps/content/schemaTypes/documents/rewards.ts | 52 +++++ apps/content/schemaTypes/index.ts | 4 + .../schemaTypes/objects/reward-item.ts | 42 ++++ libs/types/src/sanity.ts | 47 ++++ 7 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 apps/content/schemaTypes/documents/rewards.ts create mode 100644 apps/content/schemaTypes/objects/reward-item.ts diff --git a/apps/content/sanity.config.ts b/apps/content/sanity.config.ts index 2bb2694..9ac7c0f 100644 --- a/apps/content/sanity.config.ts +++ b/apps/content/sanity.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ .items([ ...S.documentTypeListItems().filter( (li) => - !['Episode', 'Collection', 'Episode Tag', 'Video asset'].includes( + !['Episode', 'Collection', 'Episode Tag', 'Video asset', 'Rewards', 'FAQ'].includes( li.getTitle() ?? '', ), ), @@ -40,6 +40,10 @@ export default defineConfig({ .title('FAQs') .schemaType('faq') .child(S.documentTypeList('faq').title('FAQs')), + S.listItem() + .title('Rewards') + .schemaType('rewards') + .child(S.documentTypeList('rewards').title('Rewards')), ]) }, }), diff --git a/apps/content/schema.json b/apps/content/schema.json index 1ddc135..cc8cf02 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -1,4 +1,72 @@ [ + { + "name": "rewardItem", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "rewardItem" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "image": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "cloudinary.asset" + }, + "optional": true + } + } + } + }, + { + "name": "faqItem", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "faqItem" + } + }, + "question": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "answer": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, { "name": "episodeImage", "type": "type", @@ -411,6 +479,154 @@ } } }, + { + "name": "rewards", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "rewards" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "items": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "rewardItem" + } + } + }, + "optional": true + } + } + }, + { + "name": "faq", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "faq" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "items": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "faqItem" + } + } + }, + "optional": true + } + } + }, { "name": "hackathon", "type": "document", diff --git a/apps/content/schemaTypes/documents/faq.ts b/apps/content/schemaTypes/documents/faq.ts index 282bae8..44fb48f 100644 --- a/apps/content/schemaTypes/documents/faq.ts +++ b/apps/content/schemaTypes/documents/faq.ts @@ -1,9 +1,11 @@ import {defineField, defineType} from 'sanity' +import { BookIcon } from '@sanity/icons' export const faq = defineType({ name: 'faq', type: 'document', title: 'FAQ', + icon: BookIcon, fields: [ defineField({ name: 'name', @@ -37,12 +39,13 @@ export const faq = defineType({ preview: { select: { title: 'name', - itemCount: 'items.length', + items: 'items', }, - prepare({title, itemCount}) { + prepare({title, items}) { + const itemCount = items?.length || 0 return { title: title || 'Untitled FAQ', - subtitle: `${itemCount || 0} items`, + subtitle: `${itemCount} items`, } }, }, diff --git a/apps/content/schemaTypes/documents/rewards.ts b/apps/content/schemaTypes/documents/rewards.ts new file mode 100644 index 0000000..eb152bc --- /dev/null +++ b/apps/content/schemaTypes/documents/rewards.ts @@ -0,0 +1,52 @@ +import {defineField, defineType} from 'sanity' +import {StarIcon} from '@sanity/icons' + +export const rewards = defineType({ + name: 'rewards', + type: 'document', + title: 'Rewards', + icon: StarIcon, + fields: [ + defineField({ + name: 'name', + type: 'string', + title: 'Name', + description: 'Internal name for this rewards collection', + validation: (Rule) => Rule.required().error('Name is required'), + }), + defineField({ + name: 'slug', + type: 'slug', + title: 'Slug', + description: 'URL-friendly identifier for this rewards collection', + options: { + source: 'name', + maxLength: 96, + }, + validation: (Rule) => Rule.required().error('Slug is required for URL generation'), + }), + defineField({ + name: 'items', + title: 'Reward Items', + type: 'array', + description: 'Individual rewards in this collection', + of: [{type: 'rewardItem'}], + options: { + sortable: true, + }, + }), + ], + preview: { + select: { + title: 'name', + items: 'items', + }, + prepare({title, items}) { + const itemCount = items?.length || 0 + return { + title: title || 'Untitled Rewards', + subtitle: `${itemCount} items`, + } + }, + }, +}) diff --git a/apps/content/schemaTypes/index.ts b/apps/content/schemaTypes/index.ts index 08abf07..d58ef3f 100644 --- a/apps/content/schemaTypes/index.ts +++ b/apps/content/schemaTypes/index.ts @@ -4,9 +4,11 @@ import {person} from './documents/person' import {episode} from './documents/episode' import {hackathon} from './documents/hackathon' import {faq} from './documents/faq' +import {rewards} from './documents/rewards' import {episodeTag} from './documents/tags' import {episodeImage} from './objects/episode-image' import faqItem from './objects/faq-item' +import rewardItem from './objects/reward-item' function slugify(str: string) { return String(str) @@ -262,9 +264,11 @@ export const schemaTypes = [ episode, hackathon, faq, + rewards, person, sponsor, episodeTag, episodeImage, faqItem, + rewardItem, ] diff --git a/apps/content/schemaTypes/objects/reward-item.ts b/apps/content/schemaTypes/objects/reward-item.ts new file mode 100644 index 0000000..c9c5e60 --- /dev/null +++ b/apps/content/schemaTypes/objects/reward-item.ts @@ -0,0 +1,42 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'rewardItem', + title: 'Reward Item', + type: 'object', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => Rule.required().min(3).max(100), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + validation: (Rule) => Rule.required().min(10).max(500), + }), + defineField({ + name: 'image', + title: 'Image', + type: 'cloudinary.asset', + options: {hotspot: true}, + validation: (Rule) => Rule.required().error('Image is required'), + }), + ], + preview: { + select: { + title: 'title', + subtitle: 'description', + media: 'image', + }, + prepare(selection) { + const {title, subtitle} = selection + return { + title, + subtitle: subtitle ? `${subtitle.substring(0, 60)}...` : '', + } + }, + }, +}) diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index c0bfdaa..cf4f0f3 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -13,6 +13,19 @@ */ // Source: schema.json +export type RewardItem = { + _type: 'rewardItem'; + title?: string; + description?: string; + image?: CloudinaryAsset; +}; + +export type FaqItem = { + _type: 'faqItem'; + question?: string; + answer?: string; +}; + export type EpisodeImage = { _type: 'episodeImage'; asset?: { @@ -87,6 +100,36 @@ export type Person = { user_id?: string; }; +export type Rewards = { + _id: string; + _type: 'rewards'; + _createdAt: string; + _updatedAt: string; + _rev: string; + name?: string; + slug?: Slug; + items?: Array< + { + _key: string; + } & RewardItem + >; +}; + +export type Faq = { + _id: string; + _type: 'faq'; + _createdAt: string; + _updatedAt: string; + _rev: string; + name?: string; + slug?: Slug; + items?: Array< + { + _key: string; + } & FaqItem + >; +}; + export type Hackathon = { _id: string; _type: 'hackathon'; @@ -473,10 +516,14 @@ export type SanityAssetSourceData = { }; export type AllSanitySchemaTypes = + | RewardItem + | FaqItem | EpisodeImage | EpisodeTag | Sponsor | Person + | Rewards + | Faq | Hackathon | Episode | Collection From e8e517ace184883ad30c7199402d4b9bba32eedf Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 10:53:29 -0500 Subject: [PATCH 12/55] refactor: add rewards and faq fields to hackathon document with default values --- apps/content/schema.json | 258 +++++++++++------- .../schemaTypes/documents/hackathon.ts | 47 ++++ libs/types/src/sanity.ts | 64 +++-- 3 files changed, 244 insertions(+), 125 deletions(-) diff --git a/apps/content/schema.json b/apps/content/schema.json index cc8cf02..9e88253 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -480,7 +480,7 @@ } }, { - "name": "rewards", + "name": "hackathon", "type": "document", "attributes": { "_id": { @@ -493,7 +493,7 @@ "type": "objectAttribute", "value": { "type": "string", - "value": "rewards" + "value": "hackathon" } }, "_createdAt": { @@ -514,7 +514,7 @@ "type": "string" } }, - "name": { + "title": { "type": "objectAttribute", "value": { "type": "string" @@ -529,27 +529,172 @@ }, "optional": true }, - "items": { + "pubDate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "body": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "episodes": { "type": "objectAttribute", "value": { "type": "array", "of": { "type": "object", "attributes": { - "_key": { + "_ref": { "type": "objectAttribute", "value": { "type": "string" } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true } }, + "dereferencesTo": "episode", "rest": { - "type": "inline", - "name": "rewardItem" + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } } } }, "optional": true + }, + "rewards": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "rewards" + }, + "optional": true + }, + "faq": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "faq" + }, + "optional": true + }, + "share_image": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "cloudinary.asset" + }, + "optional": true + }, + "hidden": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "visible" + }, + { + "type": "string", + "value": "hidden" + } + ] + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "featured" + } + ] + }, + "optional": true } } }, @@ -628,7 +773,7 @@ } }, { - "name": "hackathon", + "name": "rewards", "type": "document", "attributes": { "_id": { @@ -641,7 +786,7 @@ "type": "objectAttribute", "value": { "type": "string", - "value": "hackathon" + "value": "rewards" } }, "_createdAt": { @@ -662,7 +807,7 @@ "type": "string" } }, - "title": { + "name": { "type": "objectAttribute", "value": { "type": "string" @@ -677,112 +822,27 @@ }, "optional": true }, - "pubDate": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "description": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "body": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "episodes": { + "items": { "type": "objectAttribute", "value": { "type": "array", "of": { "type": "object", "attributes": { - "_ref": { + "_key": { "type": "objectAttribute", "value": { "type": "string" } - }, - "_type": { - "type": "objectAttribute", - "value": { - "type": "string", - "value": "reference" - } - }, - "_weak": { - "type": "objectAttribute", - "value": { - "type": "boolean" - }, - "optional": true } }, - "dereferencesTo": "episode", "rest": { - "type": "object", - "attributes": { - "_key": { - "type": "objectAttribute", - "value": { - "type": "string" - } - } - } + "type": "inline", + "name": "rewardItem" } } }, "optional": true - }, - "share_image": { - "type": "objectAttribute", - "value": { - "type": "inline", - "name": "cloudinary.asset" - }, - "optional": true - }, - "hidden": { - "type": "objectAttribute", - "value": { - "type": "union", - "of": [ - { - "type": "string", - "value": "visible" - }, - { - "type": "string", - "value": "hidden" - } - ] - }, - "optional": true - }, - "featured": { - "type": "objectAttribute", - "value": { - "type": "union", - "of": [ - { - "type": "string", - "value": "normal" - }, - { - "type": "string", - "value": "featured" - } - ] - }, - "optional": true } } }, diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index 735a465..bf24231 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -69,6 +69,53 @@ export const hackathon = defineType({ ], group: 'content', }), + defineField({ + name: 'rewards', + type: 'reference', + title: 'Rewards', + description: 'Rewards collection for this hackathon', + to: [{type: 'rewards'}], + options: { + disableNew: true, + filter: '_type == "rewards"', + }, + initialValue: async (_props: any, {getClient}: any) => { + const client = getClient({apiVersion: '2024-01-01'}) + const rewardsId = await client.fetch( + '*[_type == "rewards" && slug.current == $slug][0]._id', + { + slug: 'hackathon-rewards', + }, + ) + if (rewardsId) { + return {_ref: rewardsId} + } + return {_ref: ''} + }, + group: 'content', + }), + defineField({ + name: 'faq', + type: 'reference', + title: 'FAQ', + description: 'FAQ collection for this hackathon', + to: [{type: 'faq'}], + options: { + disableNew: true, + filter: '_type == "faq"', + }, + initialValue: async (_props: any, {getClient}: any) => { + const client = getClient({apiVersion: '2024-01-01'}) + const faqId = await client.fetch('*[_type == "faq" && slug.current == $slug][0]._id', { + slug: 'hackathon-faq', + }) + if (faqId) { + return {_ref: faqId} + } + return {_ref: ''} + }, + group: 'content', + }), defineField({ name: 'share_image', type: 'cloudinary.asset', diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index cf4f0f3..691a1ae 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -100,19 +100,39 @@ export type Person = { user_id?: string; }; -export type Rewards = { +export type Hackathon = { _id: string; - _type: 'rewards'; + _type: 'hackathon'; _createdAt: string; _updatedAt: string; _rev: string; - name?: string; + title?: string; slug?: Slug; - items?: Array< - { - _key: string; - } & RewardItem - >; + pubDate?: string; + description?: string; + body?: string; + episodes?: Array<{ + _ref: string; + _type: 'reference'; + _weak?: boolean; + _key: string; + [internalGroqTypeReferenceTo]?: 'episode'; + }>; + rewards?: { + _ref: string; + _type: 'reference'; + _weak?: boolean; + [internalGroqTypeReferenceTo]?: 'rewards'; + }; + faq?: { + _ref: string; + _type: 'reference'; + _weak?: boolean; + [internalGroqTypeReferenceTo]?: 'faq'; + }; + share_image?: CloudinaryAsset; + hidden?: 'visible' | 'hidden'; + featured?: 'normal' | 'featured'; }; export type Faq = { @@ -130,27 +150,19 @@ export type Faq = { >; }; -export type Hackathon = { +export type Rewards = { _id: string; - _type: 'hackathon'; + _type: 'rewards'; _createdAt: string; _updatedAt: string; _rev: string; - title?: string; + name?: string; slug?: Slug; - pubDate?: string; - description?: string; - body?: string; - episodes?: Array<{ - _ref: string; - _type: 'reference'; - _weak?: boolean; - _key: string; - [internalGroqTypeReferenceTo]?: 'episode'; - }>; - share_image?: CloudinaryAsset; - hidden?: 'visible' | 'hidden'; - featured?: 'normal' | 'featured'; + items?: Array< + { + _key: string; + } & RewardItem + >; }; export type Episode = { @@ -522,9 +534,9 @@ export type AllSanitySchemaTypes = | EpisodeTag | Sponsor | Person - | Rewards - | Faq | Hackathon + | Faq + | Rewards | Episode | Collection | Series From 6f55ab36a4f2431504bcf3c728da937445574494 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 11:03:25 -0500 Subject: [PATCH 13/55] feat: introduce resourceItem and ruleItem schemas, enhance hackathon document with rules and resources fields --- apps/content/schema.json | 111 ++++++++++++++++++ .../schemaTypes/documents/hackathon.ts | 22 ++++ apps/content/schemaTypes/index.ts | 4 + .../schemaTypes/objects/resource-item.ts | 42 +++++++ apps/content/schemaTypes/objects/rule-item.ts | 34 ++++++ libs/types/src/sanity.ts | 25 ++++ 6 files changed, 238 insertions(+) create mode 100644 apps/content/schemaTypes/objects/resource-item.ts create mode 100644 apps/content/schemaTypes/objects/rule-item.ts diff --git a/apps/content/schema.json b/apps/content/schema.json index 9e88253..fb7b8eb 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -1,4 +1,71 @@ [ + { + "name": "resourceItem", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "resourceItem" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "ruleItem", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "ruleItem" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, { "name": "rewardItem", "type": "type", @@ -654,6 +721,50 @@ }, "optional": true }, + "rules": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "ruleItem" + } + } + }, + "optional": true + }, + "resources": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "resourceItem" + } + } + }, + "optional": true + }, "share_image": { "type": "objectAttribute", "value": { diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index bf24231..9e60e91 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -116,6 +116,28 @@ export const hackathon = defineType({ }, group: 'content', }), + defineField({ + name: 'rules', + type: 'array', + title: 'Rules', + description: 'Hackathon rules and guidelines', + of: [{type: 'ruleItem'}], + options: { + sortable: true, + }, + group: 'content', + }), + defineField({ + name: 'resources', + type: 'array', + title: 'Resources', + description: 'Helpful resources for hackathon participants', + of: [{type: 'resourceItem'}], + options: { + sortable: true, + }, + group: 'content', + }), defineField({ name: 'share_image', type: 'cloudinary.asset', diff --git a/apps/content/schemaTypes/index.ts b/apps/content/schemaTypes/index.ts index d58ef3f..1f46c45 100644 --- a/apps/content/schemaTypes/index.ts +++ b/apps/content/schemaTypes/index.ts @@ -9,6 +9,8 @@ import {episodeTag} from './documents/tags' import {episodeImage} from './objects/episode-image' import faqItem from './objects/faq-item' import rewardItem from './objects/reward-item' +import ruleItem from './objects/rule-item' +import resourceItem from './objects/resource-item' function slugify(str: string) { return String(str) @@ -271,4 +273,6 @@ export const schemaTypes = [ episodeImage, faqItem, rewardItem, + ruleItem, + resourceItem, ] diff --git a/apps/content/schemaTypes/objects/resource-item.ts b/apps/content/schemaTypes/objects/resource-item.ts new file mode 100644 index 0000000..6ace69e --- /dev/null +++ b/apps/content/schemaTypes/objects/resource-item.ts @@ -0,0 +1,42 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'resourceItem', + title: 'Resource Item', + type: 'object', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => Rule.required().min(3).max(100), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + validation: (Rule) => Rule.required().min(10).max(500), + }), + defineField({ + name: 'url', + title: 'URL', + type: 'url', + validation: (Rule) => Rule.required().error('URL is required'), + }), + ], + preview: { + select: { + title: 'title', + subtitle: 'description', + url: 'url', + }, + prepare(selection) { + const {title, subtitle, url} = selection + return { + title, + subtitle: subtitle ? `${subtitle.substring(0, 60)}...` : '', + url: url ? new URL(url).hostname : 'No URL', + } + }, + }, +}) diff --git a/apps/content/schemaTypes/objects/rule-item.ts b/apps/content/schemaTypes/objects/rule-item.ts new file mode 100644 index 0000000..202dd4f --- /dev/null +++ b/apps/content/schemaTypes/objects/rule-item.ts @@ -0,0 +1,34 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'ruleItem', + title: 'Rule Item', + type: 'object', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => Rule.required().min(3).max(100), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + validation: (Rule) => Rule.required().min(10).max(500), + }), + ], + preview: { + select: { + title: 'title', + subtitle: 'description', + }, + prepare(selection) { + const {title, subtitle} = selection + return { + title, + subtitle: subtitle ? `${subtitle.substring(0, 60)}...` : '', + } + }, + }, +}) diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index 691a1ae..d05218a 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -13,6 +13,19 @@ */ // Source: schema.json +export type ResourceItem = { + _type: 'resourceItem'; + title?: string; + description?: string; + url?: string; +}; + +export type RuleItem = { + _type: 'ruleItem'; + title?: string; + description?: string; +}; + export type RewardItem = { _type: 'rewardItem'; title?: string; @@ -130,6 +143,16 @@ export type Hackathon = { _weak?: boolean; [internalGroqTypeReferenceTo]?: 'faq'; }; + rules?: Array< + { + _key: string; + } & RuleItem + >; + resources?: Array< + { + _key: string; + } & ResourceItem + >; share_image?: CloudinaryAsset; hidden?: 'visible' | 'hidden'; featured?: 'normal' | 'featured'; @@ -528,6 +551,8 @@ export type SanityAssetSourceData = { }; export type AllSanitySchemaTypes = + | ResourceItem + | RuleItem | RewardItem | FaqItem | EpisodeImage From d7e33c9ad6c3a9df49a065f5a6cda88a01072d00 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 11:39:17 -0500 Subject: [PATCH 14/55] feat: add sponsor reference to hackathon document --- apps/content/schema.json | 164 +++++++++++------- .../schemaTypes/documents/hackathon.ts | 12 ++ libs/types/src/sanity.ts | 32 ++-- 3 files changed, 128 insertions(+), 80 deletions(-) diff --git a/apps/content/schema.json b/apps/content/schema.json index fb7b8eb..9d24438 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -276,73 +276,6 @@ } } }, - { - "name": "sponsor", - "type": "document", - "attributes": { - "_id": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_type": { - "type": "objectAttribute", - "value": { - "type": "string", - "value": "sponsor" - } - }, - "_createdAt": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_updatedAt": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_rev": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "title": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "slug": { - "type": "objectAttribute", - "value": { - "type": "inline", - "name": "slug" - }, - "optional": true - }, - "logo": { - "type": "objectAttribute", - "value": { - "type": "inline", - "name": "cloudinary.asset" - }, - "optional": true - }, - "link": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - } - } - }, { "name": "person", "type": "document", @@ -721,6 +654,36 @@ }, "optional": true }, + "sponsors": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sponsor" + }, + "optional": true + }, "rules": { "type": "objectAttribute", "value": { @@ -809,6 +772,73 @@ } } }, + { + "name": "sponsor", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsor" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "logo": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "cloudinary.asset" + }, + "optional": true + }, + "link": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, { "name": "faq", "type": "document", diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index 9e60e91..14e803b 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -116,6 +116,18 @@ export const hackathon = defineType({ }, group: 'content', }), + defineField({ + name: 'sponsors', + type: 'reference', + title: 'Sponsors', + description: 'Sponsors for this hackathon', + to: [{type: 'sponsor'}], + options: { + disableNew: true, + filter: '_type == "sponsor"', + }, + group: 'content', + }), defineField({ name: 'rules', type: 'array', diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index d05218a..f37acb5 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -65,18 +65,6 @@ export type EpisodeTag = { description?: string; }; -export type Sponsor = { - _id: string; - _type: 'sponsor'; - _createdAt: string; - _updatedAt: string; - _rev: string; - title?: string; - slug?: Slug; - logo?: CloudinaryAsset; - link?: string; -}; - export type Person = { _id: string; _type: 'person'; @@ -143,6 +131,12 @@ export type Hackathon = { _weak?: boolean; [internalGroqTypeReferenceTo]?: 'faq'; }; + sponsors?: { + _ref: string; + _type: 'reference'; + _weak?: boolean; + [internalGroqTypeReferenceTo]?: 'sponsor'; + }; rules?: Array< { _key: string; @@ -158,6 +152,18 @@ export type Hackathon = { featured?: 'normal' | 'featured'; }; +export type Sponsor = { + _id: string; + _type: 'sponsor'; + _createdAt: string; + _updatedAt: string; + _rev: string; + title?: string; + slug?: Slug; + logo?: CloudinaryAsset; + link?: string; +}; + export type Faq = { _id: string; _type: 'faq'; @@ -557,9 +563,9 @@ export type AllSanitySchemaTypes = | FaqItem | EpisodeImage | EpisodeTag - | Sponsor | Person | Hackathon + | Sponsor | Faq | Rewards | Episode From ed06b9a45edc1fa08a09ca3b4a49f7c61fc54209 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 11:46:48 -0500 Subject: [PATCH 15/55] feat: add deadline field to hackathon document schema --- apps/content/schemaTypes/documents/hackathon.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index 14e803b..145e4ea 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -39,6 +39,17 @@ export const hackathon = defineType({ validation: (Rule) => Rule.required().error('Publish date is required'), group: 'content', }), + defineField({ + name: 'deadline', + type: 'datetime', + title: 'Deadline', + description: 'When this hackathon submission deadline is', + options: { + timeStep: 30, + }, + validation: (Rule) => Rule.required().error('Deadline is required'), + group: 'content', + }), defineField({ name: 'description', type: 'text', From b0a08ad1641ed98747fd72ba1d145940b858ab40 Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 16:47:45 -0500 Subject: [PATCH 16/55] feat: update hackathon by slug query - simplifies episode object --- apps/content/schema.json | 7 ++++++ libs/sanity/src/index.ts | 44 +++++++++++++++++++++++------------- libs/types/src/sanity.ts | 49 +++++++++++++++++++++++++--------------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/apps/content/schema.json b/apps/content/schema.json index 9d24438..60ebfbe 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -536,6 +536,13 @@ }, "optional": true }, + "deadline": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, "description": { "type": "objectAttribute", "value": { diff --git a/libs/sanity/src/index.ts b/libs/sanity/src/index.ts index 04ec523..c713525 100644 --- a/libs/sanity/src/index.ts +++ b/libs/sanity/src/index.ts @@ -510,33 +510,45 @@ const hackathonBySlugQuery = groq` _id, title, 'slug': slug.current, - pubDate, description, body, - episodes[]-> { - _id, + 'episode': episodes[0]-> { 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, - mux_video, - }, - 'collection': *[_type=="collection" && references(^._id)][0] { - 'slug': slug.current, - title, - }, - 'series': *[_type=="collection" && references(^._id)][0].series->{ - 'slug': slug.current, + }, + 'rewardsData': rewards-> { + name, + items[] { title, - }, + description, + image { + public_id, + width, + height, + } + } + }, + 'faqData': faq-> { + name, + items[] { + question, + answer, + } + }, + rules[] { + title, + description, + }, + resources[] { + title, + description, + url, }, share_image { public_id, diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index f37acb5..e7f6646 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -110,6 +110,7 @@ export type Hackathon = { title?: string; slug?: Slug; pubDate?: string; + deadline?: string; description?: string; body?: string; episodes?: Array<{ @@ -1070,38 +1071,50 @@ export type AllHackathonsQueryResult = Array<{ hidden: 'hidden' | 'visible' | null; }>; // Variable: hackathonBySlugQuery -// Query: *[_type == "hackathon" && slug.current == $slug][0] { _id, title, 'slug': slug.current, pubDate, description, body, episodes[]-> { _id, 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, mux_video, }, 'collection': *[_type=="collection" && references(^._id)][0] { 'slug': slug.current, title, }, 'series': *[_type=="collection" && references(^._id)][0].series->{ 'slug': slug.current, title, }, }, share_image { public_id, width, height, }, featured, hidden } +// Query: *[_type == "hackathon" && slug.current == $slug][0] { _id, title, 'slug': slug.current, description, body, 'episode': episodes[0]-> { title, 'slug': slug.current, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, }, 'rewardsData': rewards-> { name, items[] { title, description, image { public_id, width, height, } } }, 'faqData': faq-> { name, items[] { question, answer, } }, rules[] { title, description, }, resources[] { title, description, url, }, share_image { public_id, width, height, }, featured, hidden } export type HackathonBySlugQueryResult = { _id: string; title: string | null; slug: string | null; - pubDate: string | null; description: string | null; body: string | null; - episodes: Array<{ - _id: string; + episode: { title: string | null; slug: string | null; - short_description: string | null; - publish_date: string | null; thumbnail: { public_id: string | null; width: number | null; height: number | null; alt: string | null; }; - video: { - youtube_id: string | null; - mux_video: MuxVideo | null; - } | null; - collection: { - slug: string | null; - title: string | null; - } | null; - series: { - slug: string | null; + } | null; + rewardsData: { + name: string | null; + items: Array<{ title: string | null; - } | null; + description: string | null; + image: { + public_id: string | null; + width: number | null; + height: number | null; + } | null; + }> | null; + } | null; + faqData: { + name: string | null; + items: Array<{ + question: string | null; + answer: string | null; + }> | null; + } | null; + rules: Array<{ + title: string | null; + description: string | null; + }> | null; + resources: Array<{ + title: string | null; + description: string | null; + url: string | null; }> | null; share_image: { public_id: string | null; @@ -1131,6 +1144,6 @@ declare module '@sanity/client' { '\n *[_type == "person" && user_id == $user_id][0] {\n _id,\n name,\n slug,\n user_id,\n }\n': PersonByClerkIdQueryResult; '\n *[_type == "person" && subscription.status == "active"] | order(subscription.date asc) {\n _id,\n name,\n photo {\n public_id,\n height,\n width,\n },\n \'username\': slug.current,\n subscription {\n level,\n status\n }\n }\n': SupportersQueryResult; "\n *[_type == \"hackathon\" && hidden != \"hidden\"] | order(pubDate desc) {\n _id,\n title,\n 'slug': slug.current,\n pubDate,\n description,\n body,\n episodes[]-> {\n _id,\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 },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": AllHackathonsQueryResult; - "\n *[_type == \"hackathon\" && slug.current == $slug][0] {\n _id,\n title,\n 'slug': slug.current,\n pubDate,\n description,\n body,\n episodes[]-> {\n _id,\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 mux_video,\n },\n 'collection': *[_type==\"collection\" && references(^._id)][0] {\n 'slug': slug.current,\n title,\n },\n 'series': *[_type==\"collection\" && references(^._id)][0].series->{\n 'slug': slug.current,\n title,\n },\n },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": HackathonBySlugQueryResult; + "\n *[_type == \"hackathon\" && slug.current == $slug][0] {\n _id,\n title,\n 'slug': slug.current,\n description,\n body,\n 'episode': episodes[0]-> {\n title,\n 'slug': slug.current,\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 },\n 'rewardsData': rewards-> {\n name,\n items[] {\n title,\n description,\n image {\n public_id,\n width,\n height,\n }\n }\n },\n 'faqData': faq-> {\n name,\n items[] {\n question,\n answer,\n }\n },\n rules[] {\n title,\n description,\n },\n resources[] {\n title,\n description,\n url,\n },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": HackathonBySlugQueryResult; } } From df9c8630502bf76c815e367b8e803761f9114afb Mon Sep 17 00:00:00 2001 From: Creeland Date: Thu, 23 Oct 2025 16:52:02 -0500 Subject: [PATCH 17/55] feat: add submissionForm field to hackathon schema and update related queries - Introduced a new optional submissionForm field in the hackathon document schema. - Updated the hackathon query to include submissionForm. - Adjusted types to accommodate the new field. --- apps/content/schema.json | 7 +++++++ apps/content/schemaTypes/documents/hackathon.ts | 11 +++++++++++ libs/sanity/src/index.ts | 1 + libs/types/src/sanity.ts | 6 ++++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/content/schema.json b/apps/content/schema.json index 60ebfbe..844be7b 100644 --- a/apps/content/schema.json +++ b/apps/content/schema.json @@ -601,6 +601,13 @@ }, "optional": true }, + "submissionForm": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, "rewards": { "type": "objectAttribute", "value": { diff --git a/apps/content/schemaTypes/documents/hackathon.ts b/apps/content/schemaTypes/documents/hackathon.ts index 145e4ea..b243791 100644 --- a/apps/content/schemaTypes/documents/hackathon.ts +++ b/apps/content/schemaTypes/documents/hackathon.ts @@ -80,6 +80,17 @@ export const hackathon = defineType({ ], group: 'content', }), + defineField({ + name: 'submissionForm', + type: 'url', + title: 'Submission Form', + description: 'URL to the submission form for this hackathon', + validation: (Rule) => + Rule.required() + .uri({scheme: ['http', 'https']}) + .error('Submission form must be a valid URL'), + group: 'content', + }), defineField({ name: 'rewards', type: 'reference', diff --git a/libs/sanity/src/index.ts b/libs/sanity/src/index.ts index c713525..6816ee6 100644 --- a/libs/sanity/src/index.ts +++ b/libs/sanity/src/index.ts @@ -512,6 +512,7 @@ const hackathonBySlugQuery = groq` 'slug': slug.current, description, body, + submissionForm, 'episode': episodes[0]-> { title, 'slug': slug.current, diff --git a/libs/types/src/sanity.ts b/libs/types/src/sanity.ts index e7f6646..7de3a94 100644 --- a/libs/types/src/sanity.ts +++ b/libs/types/src/sanity.ts @@ -120,6 +120,7 @@ export type Hackathon = { _key: string; [internalGroqTypeReferenceTo]?: 'episode'; }>; + submissionForm?: string; rewards?: { _ref: string; _type: 'reference'; @@ -1071,13 +1072,14 @@ export type AllHackathonsQueryResult = Array<{ hidden: 'hidden' | 'visible' | null; }>; // Variable: hackathonBySlugQuery -// Query: *[_type == "hackathon" && slug.current == $slug][0] { _id, title, 'slug': slug.current, description, body, 'episode': episodes[0]-> { title, 'slug': slug.current, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, }, 'rewardsData': rewards-> { name, items[] { title, description, image { public_id, width, height, } } }, 'faqData': faq-> { name, items[] { question, answer, } }, rules[] { title, description, }, resources[] { title, description, url, }, share_image { public_id, width, height, }, featured, hidden } +// Query: *[_type == "hackathon" && slug.current == $slug][0] { _id, title, 'slug': slug.current, description, body, submissionForm, 'episode': episodes[0]-> { title, 'slug': slug.current, 'thumbnail': { 'public_id': video.thumbnail.public_id, 'width': video.thumbnail.width, 'height': video.thumbnail.height, 'alt': video.thumbnail_alt, }, }, 'rewardsData': rewards-> { name, items[] { title, description, image { public_id, width, height, } } }, 'faqData': faq-> { name, items[] { question, answer, } }, rules[] { title, description, }, resources[] { title, description, url, }, share_image { public_id, width, height, }, featured, hidden } export type HackathonBySlugQueryResult = { _id: string; title: string | null; slug: string | null; description: string | null; body: string | null; + submissionForm: string | null; episode: { title: string | null; slug: string | null; @@ -1144,6 +1146,6 @@ declare module '@sanity/client' { '\n *[_type == "person" && user_id == $user_id][0] {\n _id,\n name,\n slug,\n user_id,\n }\n': PersonByClerkIdQueryResult; '\n *[_type == "person" && subscription.status == "active"] | order(subscription.date asc) {\n _id,\n name,\n photo {\n public_id,\n height,\n width,\n },\n \'username\': slug.current,\n subscription {\n level,\n status\n }\n }\n': SupportersQueryResult; "\n *[_type == \"hackathon\" && hidden != \"hidden\"] | order(pubDate desc) {\n _id,\n title,\n 'slug': slug.current,\n pubDate,\n description,\n body,\n episodes[]-> {\n _id,\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 },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": AllHackathonsQueryResult; - "\n *[_type == \"hackathon\" && slug.current == $slug][0] {\n _id,\n title,\n 'slug': slug.current,\n description,\n body,\n 'episode': episodes[0]-> {\n title,\n 'slug': slug.current,\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 },\n 'rewardsData': rewards-> {\n name,\n items[] {\n title,\n description,\n image {\n public_id,\n width,\n height,\n }\n }\n },\n 'faqData': faq-> {\n name,\n items[] {\n question,\n answer,\n }\n },\n rules[] {\n title,\n description,\n },\n resources[] {\n title,\n description,\n url,\n },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": HackathonBySlugQueryResult; + "\n *[_type == \"hackathon\" && slug.current == $slug][0] {\n _id,\n title,\n 'slug': slug.current,\n description,\n body,\n submissionForm,\n 'episode': episodes[0]-> {\n title,\n 'slug': slug.current,\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 },\n 'rewardsData': rewards-> {\n name,\n items[] {\n title,\n description,\n image {\n public_id,\n width,\n height,\n }\n }\n },\n 'faqData': faq-> {\n name,\n items[] {\n question,\n answer,\n }\n },\n rules[] {\n title,\n description,\n },\n resources[] {\n title,\n description,\n url,\n },\n share_image {\n public_id,\n width,\n height,\n },\n featured,\n hidden\n }\n": HackathonBySlugQueryResult; } } From e01f89fd1592d1d48c9c544baab3320606ee5a17 Mon Sep 17 00:00:00 2001 From: Creeland Date: Fri, 24 Oct 2025 11:48:11 -0500 Subject: [PATCH 18/55] Revert "Merge branch 'main' of https://github.com/codetv-dev/codetv.dev into feat/hackathon-landing-pages" This reverts commit 08b4849386315dc9c18b0a7ede1a48b86e867642, reversing changes made to d7e33c9ad6c3a9df49a065f5a6cda88a01072d00. --- .gitignore | 1 - apps/shortlinks/public/_redirects | 20 +- apps/website/public/_redirects | 1 - apps/website/src/actions/index.ts | 19 +- apps/website/src/components/eyebrow.astro | 33 +- apps/website/src/components/header.astro | 4 +- apps/website/src/content/.obsidian/app.json | 4 +- .../src/content/.obsidian/appearance.json | 4 +- .../content/.obsidian/community-plugins.json | 5 +- .../src/content/.obsidian/core-plugins.json | 53 +- .../.obsidian/plugins/quickadd/data.json | 235 +- .../.obsidian/plugins/quickadd/main.js | 21592 +++++++++++++++- .../.obsidian/plugins/quickadd/manifest.json | 7 +- .../.obsidian/plugins/quickadd/styles.css | 239 +- .../src/content/.obsidian/workspace.json | 204 + ...v-challenge-hackathon-s2e7-touch-grass.mdx | 95 - .../2025-08-30-astro-css-overrides-layers.mdx | 255 - ...hallenge-hackathon-s2e9-breakfast-apps.mdx | 89 - apps/website/src/layouts/default.astro | 23 +- .../src/pages/forms/web-dev-challenge.astro | 21 +- apps/website/src/pages/newsletter.astro | 5 +- libs/inngest/src/integrations/sanity/steps.ts | 2 +- 22 files changed, 22092 insertions(+), 819 deletions(-) create mode 100644 apps/website/src/content/.obsidian/workspace.json delete mode 100644 apps/website/src/content/blog/2025-08-26-2024-08-26-web-dev-challenge-hackathon-s2e7-touch-grass.mdx delete mode 100644 apps/website/src/content/blog/2025-08-30-astro-css-overrides-layers.mdx delete mode 100644 apps/website/src/content/blog/2025-09-22-web-dev-challenge-hackathon-s2e9-breakfast-apps.mdx diff --git a/.gitignore b/.gitignore index 631f556..279ef03 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md node_modules -apps/website/src/content/.obsidian/workspace.json diff --git a/apps/shortlinks/public/_redirects b/apps/shortlinks/public/_redirects index 933772c..75ce68c 100644 --- a/apps/shortlinks/public/_redirects +++ b/apps/shortlinks/public/_redirects @@ -16,7 +16,7 @@ https://codetv-links.netlify.app/* https://codetv.dev/:splat 301! # quick links -/discord https://discord.gg/codetv +/discord https://discord.gg/lwj /youtube https://youtube.com/@codetv-dev /store https://store.codetv.dev/ /4d1a https://codetv.dev/series/web-dev-challenge/s0 @@ -46,17 +46,12 @@ https://codetv-links.netlify.app/* https://codetv.dev/:splat 301! /wdc/s2e5 https://codetv.dev/series/web-dev-challenge/s2/e5-worst-developer-urges /wdc/s2e6 https://codetv.dev/series/web-dev-challenge/s2/e6-future-of-ai-native-ux /wdc/s2e7 https://codetv.dev/series/web-dev-challenge/s2/e7-touch-grass -/wdc/s2e8 https://codetv.dev/series/web-dev-challenge/s2/e8-personal-software -/wdc/s2e9 https://codetv.dev/series/web-dev-challenge/s2/e9-most-important-app -/wdc/s2e10 https://codetv.dev/series/web-dev-challenge/s2/e10-no-keyboards-allowed -/wdc/s2e11 https://codetv.dev/series/web-dev-challenge/s2/e11-build-a-hotline -/wdc/hackathon https://nokeyboardsallowed.dev/ -/wdc/hackathon/submit https://docs.google.com/forms/d/e/1FAIpQLSevu6FJQFlKCUQOiIDYymD8QhtmfQMo3ua-SqaYRT62ym7aFQ/viewform +/wdc/hackathon https://codetv.dev/blog/web-dev-challenge-hackathon-s2e5-worst-dev-urges +/wdc/hackathon/submit https://forms.gle/k1AwazF2KriwXTTm7 /wdc-giveaway /wdc/hackathon /wdc-hackathon /wdc/hackathon -/challenge /wdc/hackathon /wdc-hackathon-3-submit https://forms.gle/hPkGDCb1R66TGocw9 /wdc-hackathon-4-submit https://forms.gle/BfQtaRbpZhZDAMKx6 /wdc-hackathon-5-submit https://forms.gle/48BA8qeHykdN5pXp9 @@ -68,9 +63,6 @@ https://codetv-links.netlify.app/* https://codetv.dev/:splat 301! /wdc-hackathon-s2e2-submit https://forms.gle/esXkSsNCLA3fDqdt5 /wdc-hackathon-s2e3-submit https://forms.gle/7aKhquYNduXTaJrE8 /wdc-hackathon-s2e5-submit https://forms.gle/k1AwazF2KriwXTTm7 -/wdc-hackathon-s2e7-submit https://forms.gle/kjhm9desbh5THB5K6 -/wdc-hackathon-s2e9-submit https://forms.gle/BCzYMR61epV6RgRFA -/wdc-hackathon-s2e10-submit https://docs.google.com/forms/d/e/1FAIpQLSevu6FJQFlKCUQOiIDYymD8QhtmfQMo3ua-SqaYRT62ym7aFQ/viewform /wdc-slc https://ti.to/epicweb/epicweb-conf-2025/with/web-dev-challenge#tickets @@ -174,9 +166,6 @@ https://codetv-links.netlify.app/* https://codetv.dev/:splat 301! /nordcraft https://nordcraft.com/?utm_source=codetv&utm_medium=video&utm_campaign=web-dev-challenge /intuit https://intuit.avature.net/eventlisting?pipelineId=13447 /apollo https://www.apollographql.com/codetv/connectors?utm_source=codetv&utm_medium=video&utm_campaign=web-dev-challenge -/hashbrown https://hashbrown.dev/?utm_source=codetv&utm_medium=video&utm_campaign=web-dev-challenge -/goose https://block.github.io/goose/?utm_source=codetv&utm_medium=video&utm_campaign=web-dev-challenge -/twilio https://twil.io/wdc-signup /benq https://www.benq.com/en-us/campaign/best-coding-monitor-for-programmers.html?utm_source=influencer_codetv&utm_medium=affiliate&utm_campaign=lcd_programming_rd280u_na_2025_us_awareness /uplift https://www.upliftdesk.com/?utm_source=codetv&utm_medium=video&utm_campaign=web-dev-challenge @@ -201,9 +190,6 @@ https://codetv-links.netlify.app/* https://codetv.dev/:splat 301! /book https://calendly.com/codetv/livestream /30min https://calendly.com/codetv/30min -# DevSquares -/ds https://devsquares.codetv.dev/webu/play - # this link goes to the active or next-scheduled live stream /live https://youtube.com/@codetv-dev/live diff --git a/apps/website/public/_redirects b/apps/website/public/_redirects index 1002475..d985e1c 100644 --- a/apps/website/public/_redirects +++ b/apps/website/public/_redirects @@ -18,7 +18,6 @@ /partners https://partners.codetv.dev 301! /support /dashboard/sign-up 301! -/series/web-dev-challenge/s2/most-important-app /series/web-dev-challenge/s2/e9-most-important-app 301! /what-s-new-in-redux-toolkit-2-0 /series/learn-with-jason/s7/what-s-new-in-redux-toolkit-2-0 /blog/oklch-better-color-css-browse /blog/oklch-better-color-css-browser /blog/false /blog diff --git a/apps/website/src/actions/index.ts b/apps/website/src/actions/index.ts index 5c94c4a..7d2ac4c 100644 --- a/apps/website/src/actions/index.ts +++ b/apps/website/src/actions/index.ts @@ -2,7 +2,6 @@ import { defineAction } from 'astro:actions'; import { z } from 'astro:content'; import { inngest } from '@codetv/inngest'; import { addSubscriber } from '@codetv/kit'; -import { getPersonById } from '@codetv/sanity'; export const server = { user: { @@ -80,7 +79,7 @@ export const server = { forms: { wdc: defineAction({ accept: 'form', - handler: async (formData, context) => { + handler: async (formData) => { const linkLabels = formData.getAll('link_label[]'); const linkUrls = formData.getAll('link_url[]'); const links = linkUrls @@ -112,22 +111,6 @@ export const server = { username: formData.get('username'), }; - // if someone created a new account, the Sanity ID might not exist - // in time to populate the form, so grab it here - if (!rawInput.id) { - const user = await context.locals.currentUser(); - const userDetails = await getPersonById( - { user_id: user?.id ?? '' }, - { useCdn: false }, - ); - - if (!userDetails?._id) { - throw new Error(`missing Sanity ID for ${rawInput.signature}`); - } - - rawInput.id = userDetails._id; - } - const InputSchema = z.object({ signature: z.string(), role: z.union([z.literal('developer'), z.literal('advisor')]), diff --git a/apps/website/src/components/eyebrow.astro b/apps/website/src/components/eyebrow.astro index 9569b49..a6629b1 100644 --- a/apps/website/src/components/eyebrow.astro +++ b/apps/website/src/components/eyebrow.astro @@ -1,35 +1,32 @@ --- // TODO: should this be a field in the CMS to make it easier to announce things? + +// disable for now +return null; --- -