From 004bd27ac1d67b4ff800252772b2677a35d03286 Mon Sep 17 00:00:00 2001 From: capJavert Date: Wed, 29 Apr 2026 17:45:34 +0200 Subject: [PATCH 1/6] feat: hackathon landing --- .../components/HackathonClosingCTA.tsx | 31 +++++ .../hackathon/components/HackathonFAQ.tsx | 79 ++++++++++++ .../hackathon/components/HackathonHero.tsx | 55 +++++++++ .../components/HackathonHowItWorks.tsx | 115 ++++++++++++++++++ .../components/HackathonSignupButton.tsx | 94 ++++++++++++++ .../hackathon/components/HackathonTracks.tsx | 107 ++++++++++++++++ .../shared/src/features/hackathon/queries.ts | 21 ++++ packages/shared/src/graphql/users.ts | 24 ++++ packages/shared/src/lib/auth.ts | 1 + packages/shared/src/lib/log.ts | 1 + packages/shared/src/lib/query.ts | 1 + packages/webapp/pages/hackathon/index.tsx | 93 ++++++++++++++ .../webapp/public/assets/hackathon-og.png | Bin 0 -> 1012476 bytes 13 files changed, 622 insertions(+) create mode 100644 packages/shared/src/features/hackathon/components/HackathonClosingCTA.tsx create mode 100644 packages/shared/src/features/hackathon/components/HackathonFAQ.tsx create mode 100644 packages/shared/src/features/hackathon/components/HackathonHero.tsx create mode 100644 packages/shared/src/features/hackathon/components/HackathonHowItWorks.tsx create mode 100644 packages/shared/src/features/hackathon/components/HackathonSignupButton.tsx create mode 100644 packages/shared/src/features/hackathon/components/HackathonTracks.tsx create mode 100644 packages/shared/src/features/hackathon/queries.ts create mode 100644 packages/webapp/pages/hackathon/index.tsx create mode 100644 packages/webapp/public/assets/hackathon-og.png diff --git a/packages/shared/src/features/hackathon/components/HackathonClosingCTA.tsx b/packages/shared/src/features/hackathon/components/HackathonClosingCTA.tsx new file mode 100644 index 00000000000..2d790e1413a --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonClosingCTA.tsx @@ -0,0 +1,31 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../../components/typography/Typography'; +import { FlexCol } from '../../../components/utilities'; +import { HackathonSignupButton } from './HackathonSignupButton'; + +export const HackathonClosingCTA = (): ReactElement => { + return ( + + + Ready to build? + + + Sign up now and we'll send you the kickoff details and how to get + your API access before the hackathon opens. + +
+ +
+
+ ); +}; diff --git a/packages/shared/src/features/hackathon/components/HackathonFAQ.tsx b/packages/shared/src/features/hackathon/components/HackathonFAQ.tsx new file mode 100644 index 00000000000..3e187fed2f2 --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonFAQ.tsx @@ -0,0 +1,79 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../../components/typography/Typography'; +import { FlexCol } from '../../../components/utilities'; +import { RadixAccordion } from '../../../components/accordion'; +import { useLogContext } from '../../../contexts/LogContext'; +import { LogEvent, TargetId } from '../../../lib/log'; + +const faq = [ + { + title: 'When does the hackathon start?', + description: 'It will happen in May. Concrete dates are TBD.', + }, + { + title: 'Who can participate?', + description: 'Any developer with a daily.dev account. Beginners welcome.', + }, + { + title: 'Do I need a Plus subscription?', + description: + 'No. The Public API is open to all hackathon participants for the duration of the event.', + }, + { + title: 'What stack should I use?', + description: + 'Whatever you want. Next.js, TanStack Start, Vite, Python. Pick whatever lets you ship fastest. The OpenAPI spec works with any language.', + }, + { + title: 'Can I use AI?', + description: + 'Yes. Claude Code, Cursor, Codex, vibe-code the whole thing if you want.', + }, + { + title: 'How do I submit my project?', + description: + 'Post about your project on social media tagging @dailydotdev with a link to your live URL and a short summary. That post is your submission.', + }, + { + title: "What happens if I don't finish?", + description: + 'Submit what you have. Half-finished but interesting beats polished but boring. We value creativity and usefulness as much as execution.', + }, +]; + +export const HackathonFAQ = (): ReactElement => { + const { logEvent } = useLogContext(); + + const handleFAQChange = (value: string) => { + if (!value) { + return; + } + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.HackathonPage, + extra: JSON.stringify({ faq: value }), + }); + }; + + return ( + + + Frequently asked questions (FAQ) + + + + ); +}; diff --git a/packages/shared/src/features/hackathon/components/HackathonHero.tsx b/packages/shared/src/features/hackathon/components/HackathonHero.tsx new file mode 100644 index 00000000000..76343e0ae4b --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonHero.tsx @@ -0,0 +1,55 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { FlexCol, FlexRow } from '../../../components/utilities'; +import { DailyIcon } from '../../../components/icons'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../../components/typography/Typography'; +import { useIsLightTheme } from '../../../hooks/utils'; +import { briefButtonBg } from '../../../styles/custom'; +import { HackathonSignupButton } from './HackathonSignupButton'; + +export const HackathonHero = (): ReactElement => { + const isLightTheme = useIsLightTheme(); + + return ( + + + + + daily.dev Hackathon + + + + Build something for developers, from developers. + + + 72 hours, three tracks, and full access to the daily.dev Public API. + + + + + + ); +}; diff --git a/packages/shared/src/features/hackathon/components/HackathonHowItWorks.tsx b/packages/shared/src/features/hackathon/components/HackathonHowItWorks.tsx new file mode 100644 index 00000000000..a47f9dd872a --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonHowItWorks.tsx @@ -0,0 +1,115 @@ +import type { ReactElement, ReactNode } from 'react'; +import React from 'react'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../../components/typography/Typography'; +import { FlexCol } from '../../../components/utilities'; +import Link from '../../../components/utilities/Link'; +import { anchorDefaultRel } from '../../../lib/strings'; +import { webappUrl } from '../../../lib/constants'; + +type Section = { + title: string; + items: ReactNode[]; +}; + +const sections: Section[] = [ + { + title: 'Rules for all tracks', + items: [ + 'Public URL required. Must be deployed, not localhost.', + 'Every project produces something a user can share (image, card, page, link) with working OG/preview tags.', + 'No heavy onboarding. Connect an account or enter a query, get a result.', + "Don't rebuild things we already have. DevCard, Presidential Briefing, Smart Prompts, Bookmark Folders, Custom Feeds, Reading Streaks, Top Reader, Cores. Build on top, not copies.", + ], + }, + { + title: 'What we provide', + items: [ + <> + + + API + + {' '} + access for all registered participants (no Plus requirement). + , + <> + + + OpenAPI spec + + {' '} + for client generation in any language. + , + 'Use whatever stack you want. Next.js, TanStack Start, Vite, Go, even PHP.', + <> + Agent skills like{' '} + + /daily-dev-ask + {' '} + to help you out + , + ], + }, + { + title: 'Format & prizes', + items: [ + '72 hours, async. Join from anywhere.', + 'To submit, post about your project on social media tagging @dailydotdev with your live URL and a short summary.', + 'Prizes: 1 year of daily.dev Plus with all the benefits for best submissions.', + 'Winning projects featured on daily.dev and social media.', + ], + }, +]; + +export const HackathonHowItWorks = (): ReactElement => { + return ( + + + How it works + +
+ {sections.map(({ title, items }) => ( + + + {title} + +
    + {items.map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key +
  • + + • {item} + +
  • + ))} +
+
+ ))} +
+
+ ); +}; diff --git a/packages/shared/src/features/hackathon/components/HackathonSignupButton.tsx b/packages/shared/src/features/hackathon/components/HackathonSignupButton.tsx new file mode 100644 index 00000000000..ac4dd34e884 --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonSignupButton.tsx @@ -0,0 +1,94 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '../../../components/buttons/Button'; +import { VIcon } from '../../../components/icons'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useLogContext } from '../../../contexts/LogContext'; +import { AuthTriggers } from '../../../lib/auth'; +import { LogEvent, TargetId } from '../../../lib/log'; +import { gqlClient } from '../../../graphql/common'; +import { JOIN_HACKATHON_MUTATION } from '../../../graphql/users'; +import { useUpdateQuery } from '../../../hooks/useUpdateQuery'; +import { hackathonParticipationQueryOptions } from '../queries'; + +type HackathonSignupButtonProps = { + size?: ButtonSize; + className?: string; +}; + +export const HackathonSignupButton = ({ + size = ButtonSize.Large, + className, +}: HackathonSignupButtonProps): ReactElement => { + const { user, isLoggedIn, showLogin } = useAuthContext(); + const { logEvent } = useLogContext(); + const participationOptions = hackathonParticipationQueryOptions(user); + const { data, isFetched } = useQuery(participationOptions); + const [getParticipation, setParticipation] = + useUpdateQuery(participationOptions); + const isParticipant = !!data?.whoami?.isHackathonParticipant; + + const { mutateAsync: join, isPending: isJoining } = useMutation({ + mutationFn: () => gqlClient.request(JOIN_HACKATHON_MUTATION), + onMutate: () => { + const previous = getParticipation(); + if (previous) { + setParticipation({ + whoami: { ...previous.whoami, isHackathonParticipant: true }, + }); + } + return { previous }; + }, + onError: (_, __, context) => { + if (context?.previous) { + setParticipation(context.previous); + } + }, + }); + + if (isLoggedIn && isParticipant) { + return ( + + ); + } + + const handleClick = async () => { + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.HackathonPage, + extra: JSON.stringify({ action: isLoggedIn ? 'join' : 'login' }), + }); + + if (!isLoggedIn) { + showLogin({ trigger: AuthTriggers.Hackathon }); + return; + } + + await join(); + }; + + return ( + + ); +}; diff --git a/packages/shared/src/features/hackathon/components/HackathonTracks.tsx b/packages/shared/src/features/hackathon/components/HackathonTracks.tsx new file mode 100644 index 00000000000..a0b1236d57e --- /dev/null +++ b/packages/shared/src/features/hackathon/components/HackathonTracks.tsx @@ -0,0 +1,107 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../../components/typography/Typography'; +import { FlexCol } from '../../../components/utilities'; + +const tracks = [ + { + emoji: '🪞', + name: 'Developer Identity', + tagline: 'Tell developers something about themselves.', + description: + 'Your daily.dev profile has a lot in it. What you read, what you save, what you follow, your tech stack, your career. Turn that into something a developer would want to share.', + examples: [ + 'Profile, reputation, streak stats', + 'Tech stack and experiences', + 'Bookmarks, follows, blocks', + 'Reading patterns and recommendations', + ], + }, + { + emoji: '📊', + name: 'Content Intelligence', + tagline: 'Use daily.dev as a dataset.', + description: + 'daily.dev pulls from 1,300+ sources. Tag feeds, source feeds, search, comments, recommendations. Turn all that data into something useful for other developers.', + examples: [ + 'Tag and source feeds', + 'Post metadata + engagement', + 'Search and recommendations', + 'Popular / most discussed', + ], + }, + { + emoji: '⚡', + name: 'Content → Action', + tagline: 'Turn reading into doing.', + description: + 'Developers bookmark things for later and never come back to them. Build the bridge between reading on daily.dev and actually doing something with what you read.', + examples: [ + 'Post details + summaries', + 'Bookmarks (the backlog)', + 'Tech stack + experiences', + 'Custom feeds + search', + ], + }, +]; + +export const HackathonTracks = (): ReactElement => { + return ( + + + + Three tracks. Pick one. + + + Each track is a different angle on the daily.dev API. Pick the one + that fits what you want to build. + + +
+ {tracks.map(({ emoji, name, tagline, description, examples }) => ( + + {emoji} + + {name} + + + {tagline} + + + {description} + +
    + {examples.map((example) => ( +
  • + + • {example} + +
  • + ))} +
+
+ ))} +
+
+ ); +}; diff --git a/packages/shared/src/features/hackathon/queries.ts b/packages/shared/src/features/hackathon/queries.ts new file mode 100644 index 00000000000..5cee806f652 --- /dev/null +++ b/packages/shared/src/features/hackathon/queries.ts @@ -0,0 +1,21 @@ +import { queryOptions } from '@tanstack/react-query'; +import { gqlClient } from '../../graphql/common'; +import { + HACKATHON_PARTICIPATION_QUERY, + type HackathonParticipationData, +} from '../../graphql/users'; +import { generateQueryKey, RequestKey } from '../../lib/query'; +import type { LoggedUser } from '../../lib/user'; + +export const hackathonParticipationQueryOptions = ( + user?: Pick, +) => + queryOptions({ + queryKey: generateQueryKey(RequestKey.HackathonParticipation, user), + queryFn: () => + gqlClient.request( + HACKATHON_PARTICIPATION_QUERY, + ), + enabled: !!user?.id, + staleTime: Infinity, + }); diff --git a/packages/shared/src/graphql/users.ts b/packages/shared/src/graphql/users.ts index ecb44386323..41acf8be6c8 100644 --- a/packages/shared/src/graphql/users.ts +++ b/packages/shared/src/graphql/users.ts @@ -34,6 +34,30 @@ export const CHECK_LOCATION_QUERY = gql` } `; +export const JOIN_HACKATHON_MUTATION = gql` + mutation JoinHackathon { + joinHackathon { + _ + } + } +`; + +export const HACKATHON_PARTICIPATION_QUERY = gql` + query HackathonParticipation { + whoami { + id + isHackathonParticipant + } + } +`; + +export type HackathonParticipationData = { + whoami: { + id: string; + isHackathonParticipant: boolean; + }; +}; + export const USER_BY_ID_STATIC_FIELDS_QUERY = ` query User($id: ID!) { user(id: $id) { diff --git a/packages/shared/src/lib/auth.ts b/packages/shared/src/lib/auth.ts index 80f76643382..50c5c6d1d9a 100644 --- a/packages/shared/src/lib/auth.ts +++ b/packages/shared/src/lib/auth.ts @@ -77,6 +77,7 @@ export enum AuthTriggers { Gear = 'gear', AddToStack = 'add to stack', PostPage = 'post page', + Hackathon = 'hackathon', } export type AuthTriggersType = diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 2a392afef63..5cc0400ac40 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -562,6 +562,7 @@ export enum TargetId { HighlightsCard = 'highlights card', AskPage = 'ask page', AskUpsellSearch = 'ask upsell search', + HackathonPage = 'hackathon page', // Onboarding v2 GitHub = 'github', AI = 'ai', diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 15121115c14..4a9d42f2ee0 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -270,6 +270,7 @@ export enum RequestKey { ShowcaseAchievements = 'showcase_achievements', PostHighlights = 'post_highlights', MarketingCtas = 'marketing_ctas', + HackathonParticipation = 'hackathon_participation', } export const getPostByIdKey = (id: string): QueryKey => [RequestKey.Post, id]; diff --git a/packages/webapp/pages/hackathon/index.tsx b/packages/webapp/pages/hackathon/index.tsx new file mode 100644 index 00000000000..360f9aa8fe9 --- /dev/null +++ b/packages/webapp/pages/hackathon/index.tsx @@ -0,0 +1,93 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import Head from 'next/head'; +import type { NextSeoProps } from 'next-seo'; +import { FlexCol } from '@dailydotdev/shared/src/components/utilities'; +import { + Typography, + TypographyColor, + TypographyType, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { fromCDN } from '@dailydotdev/shared/src/lib/links'; +import { HackathonHero } from '@dailydotdev/shared/src/features/hackathon/components/HackathonHero'; +import { HackathonTracks } from '@dailydotdev/shared/src/features/hackathon/components/HackathonTracks'; +import { HackathonHowItWorks } from '@dailydotdev/shared/src/features/hackathon/components/HackathonHowItWorks'; +import { HackathonFAQ } from '@dailydotdev/shared/src/features/hackathon/components/HackathonFAQ'; +import { HackathonClosingCTA } from '@dailydotdev/shared/src/features/hackathon/components/HackathonClosingCTA'; +import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import { getLayout } from '../../components/layouts/MainLayout'; +import { defaultOpenGraph, defaultSeo } from '../../next-seo'; +import { getPageSeoTitles } from '../../components/layouts/utils'; + +const HACKATHON_URL = 'https://app.daily.dev/hackathon'; +const HACKATHON_TITLE = 'Hackathon'; +const HACKATHON_DESCRIPTION = + '72 hours, the daily.dev Public API, and three open tracks. Build something for developers, from developers.'; + +// TODO move to cloudinary +const HACKATHON_OG_IMAGE = fromCDN('/assets/hackathon-og.png'); + +const seoTitles = getPageSeoTitles(HACKATHON_TITLE); +const seo: NextSeoProps = { + title: seoTitles.title, + openGraph: { + ...defaultOpenGraph, + ...seoTitles.openGraph, + images: [{ url: HACKATHON_OG_IMAGE }], + }, + ...defaultSeo, + description: HACKATHON_DESCRIPTION, +}; + +const getHackathonJsonLd = (): string => + JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'Event', + name: HACKATHON_TITLE, + description: HACKATHON_DESCRIPTION, + url: HACKATHON_URL, + eventAttendanceMode: 'https://schema.org/OnlineEventAttendanceMode', + organizer: { + '@type': 'Organization', + name: 'daily.dev', + url: 'https://app.daily.dev', + }, + }); + +const HackathonPage = (): ReactElement => { + return ( +
+ +