diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 9a40768434..51e4b0cabe 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -1,6 +1,7 @@ import { CommandMenuProvider } from '@ui-patterns/Cmdk' import { ThemeProvider } from 'common' import { PortalToast } from 'ui' +import { PromoToast } from 'ui-patterns' import { type PropsWithChildren } from 'react' import SiteLayout from '~/layouts/SiteLayout' @@ -26,6 +27,7 @@ function GlobalProviders({ children }: PropsWithChildren) {
+ {children} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 259c5e70ed..b1e0446d9a 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -53,6 +53,7 @@ const nextConfig = { 'img.youtube.com', 'archbee-image-uploads.s3.amazonaws.com', 'obuldanrptloktxcffvn.supabase.co', + 'xguihxuzqibwxjnimxev.supabase.co', ], }, // TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported. diff --git a/apps/www/.env.local.example b/apps/www/.env.local.example index a5955cbb55..af319dc983 100644 --- a/apps/www/.env.local.example +++ b/apps/www/.env.local.example @@ -8,4 +8,5 @@ NEXT_PUBLIC_REFERENCE_DOCS_URL="https://localhost:3010" NEXT_PUBLIC_STUDIO_URL="http://localhost:8082" NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w" NEXT_PUBLIC_SUPABASE_URL="https://xguihxuzqibwxjnimxev.supabase.co" +SUPABASE_COM_SERVICE_ROLE_KEY="secret" NEXT_PUBLIC_URL="http://localhost:3000" diff --git a/apps/www/app/api-v2/ticket-og/route.tsx b/apps/www/app/api-v2/ticket-og/route.tsx new file mode 100644 index 0000000000..04bc1ca37d --- /dev/null +++ b/apps/www/app/api-v2/ticket-og/route.tsx @@ -0,0 +1,550 @@ +import React from 'react' +import { ImageResponse } from '@vercel/og' +import { createClient } from '@supabase/supabase-js' +import { themes } from '~/components/LaunchWeek/12/Ticket/ticketThemes' + +export const runtime = 'edge' // 'nodejs' is the default +export const dynamic = 'force-dynamic' // defaults to auto +export const fetchCache = 'force-no-store' +export const revalidate = 0 + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL + +const STORAGE_URL = `${SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw12` + +// Load custom font +const FONT_URL = `${STORAGE_URL}/assets/font/CircularStd-Book.otf` +const MONO_FONT_URL = `${STORAGE_URL}/assets/font/SourceCodePro-Regular.ttf` +const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer()) +const mono_font = fetch(new URL(MONO_FONT_URL, import.meta.url)).then((res) => res.arrayBuffer()) +// const BUCKET_FOLDER_VERSION = 'v1' + +const LW_TABLE = 'tickets' +const LW_MATERIALIZED_VIEW = 'tickets_view' + +export async function GET(req: Request, res: Response) { + const url = new URL(req.url) + console.log(process.env.NEXT_PUBLIC_SUPABASE_URL) + const username = url.searchParams.get('username') ?? url.searchParams.get('amp;username') + const assumePlatinum = url.searchParams.get('platinum') ?? url.searchParams.get('amp;platinum') + const userAgent = req.headers.get('user-agent') + + try { + if (!username) throw new Error('missing username param') + + const supabaseAdminClient = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL as string, + process.env.LIVE_SUPABASE_COM_SERVICE_ROLE_KEY as string + ) + + // Track social shares + if (userAgent?.toLocaleLowerCase().includes('twitter')) { + await supabaseAdminClient + .from(LW_TABLE) + .update({ shared_on_twitter: 'now' }) + .eq('launch_week', 'lw12') + .eq('username', username) + .is('shared_on_twitter', null) + } else if (userAgent?.toLocaleLowerCase().includes('linkedin')) { + await supabaseAdminClient + .from(LW_TABLE) + .update({ shared_on_linkedin: 'now' }) + .eq('launch_week', 'lw12') + .eq('username', username) + .is('shared_on_linkedin', null) + } + + // Get ticket data + const { data: user, error } = await supabaseAdminClient + .from(LW_MATERIALIZED_VIEW) + .select( + 'id, name, ticket_number, shared_on_twitter, shared_on_linkedin, platinum, secret, role, company, location' + ) + .eq('launch_week', 'lw12') + .eq('username', username) + .maybeSingle() + + if (error) console.log('fetch error', error.message) + if (!user) throw new Error(error?.message ?? 'user not found') + + const { + name, + ticket_number: ticketNumber, + secret, + platinum: isPlatinum, + shared_on_twitter: sharedOnTwitter, + shared_on_linkedin: sharedOnLinkedIn, + } = user + + console.log(user) + + const platinum = isPlatinum ?? (!!sharedOnTwitter && !!sharedOnLinkedIn) ?? false + if (assumePlatinum && !platinum) + return await fetch(`${STORAGE_URL}/assets/platinum_no_meme.jpg`) + + // Generate image and upload to storage. + const ticketType = secret ? 'secret' : platinum ? 'platinum' : 'regular' + + const STYLING_CONFIG = { + BACKGROUND: themes[ticketType].OG_BACKGROUND, + FOREGROUND: themes[ticketType].TICKET_FOREGROUND, + FOREGROUND_LIGHT: themes[ticketType].TICKET_FOREGROUND_LIGHT, + TICKET_BORDER: themes[ticketType].TICKET_BORDER, + TICKET_FOREGROUND: themes[ticketType].TICKET_FOREGROUND, + TICKET_BACKGROUND: themes[ticketType].TICKET_BACKGROUND, + TICKET_BACKGROUND_CODE: themes[ticketType].TICKET_BACKGROUND_CODE, + TICKET_FOREGROUND_LIGHT: themes[ticketType].TICKET_FOREGROUND_LIGHT, + BORDER: themes[ticketType].TICKET_BORDER, + CODE_LINE_NUMBER: themes[ticketType].CODE_LINE_NUMBER, + CODE_BASE: themes[ticketType].CODE_THEME['hljs'].color, + CODE_HIGHLIGHT: themes[ticketType].CODE_HIGHLIGHT_BACKGROUND, + CODE_FUNCTION: themes[ticketType].CODE_THEME['hljs'].color, + CODE_VARIABLE: themes[ticketType].CODE_THEME['hljs'].color, + CODE_METHOD: themes[ticketType].CODE_THEME['hljs'].color, + CODE_EXPRESSION: themes[ticketType].CODE_THEME['hljs-keyword'].color, + CODE_STRING: themes[ticketType].CODE_THEME['hljs-string'].color, + CODE_NUMBER: themes[ticketType].CODE_THEME['hljs'].color, + CODE_NULL: themes[ticketType].CODE_THEME['hljs'].color, + JSON_KEY: themes[ticketType].CODE_THEME['hljs-attr'].color, + } + + const fontData = await font + const monoFontData = await mono_font + const OG_WIDTH = 1200 + const OG_HEIGHT = 628 + const OG_PADDING_X = 60 + const OG_PADDING_Y = 60 + const TICKET_WIDTH = 550 + const TICKET_RATIO = 396 / 613 + const TICKET_HEIGHT = TICKET_WIDTH / TICKET_RATIO + const TICKET_POS_TOP = OG_PADDING_Y + const TICKET_POS_LEFT = 540 + const LOGO_WIDTH = 40 + const LOGO_RATIO = 436 / 449 + const DISPLAY_NAME = name || username + const FIRST_NAME = DISPLAY_NAME?.split(' ')[0] + + const BACKGROUND = { + regular: { + LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png`, + BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-dark.png?t=2024-07-26T11%3A13%3A36.534Z`, + }, + platinum: { + LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png`, + BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-dark.png?t=2024-07-26T11%3A13%3A36.534Z`, + }, + secret: { + LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon-white.png`, + BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-light.png`, + }, + } + + const lineNumberStyle = { + paddingLeft: 24, + width: 46, + color: STYLING_CONFIG.CODE_LINE_NUMBER, + } + + const generatedTicketImage = new ImageResponse( + ( + <> +
+ {/* Background */} + + {/* Ticket */} +
+ + Launch Week 12 + + Ticket + + + {/* Request code snippet */} +
+
+ 1 + 2 + 3 + 4 + 5 +
+
+ + await{' '} + + supabase + + + + . + from + ( + 'tickets' + ) + + + . + select + ( + '*' + ) + + + . + eq + ( + 'username' + , + {username} + ) + + + . + single + ( + ) + +
+
+ {/* Response Json */} +
+
+ TICKET RESPONSE +
+
+
+ 1 + { +
+
+ 2 + + + "data" + + : + { + +
+
+
+ 3 + + + "name" + + : + + "{name}" + + , + +
+
+ 4 + + + "username" + + : + + "{username}" + + , + +
+
+ 6 + + + "ticket_number" + + : + + "{ticketNumber}" + + , + +
+
+ 7 + + + "role" + + : + + "{user.role}" + + , + +
+
+ 8 + + + "company" + + : + + "{user.company}" + + , + +
+
+ 9 + + + "location" + + : + + "{user.location}" + + , + +
+
+
+ 10 + }, +
+
+ 11 + + + "error" + + : + + null + + +
+
+ 12 + } +
+
+
+
+ +
+
+ logo +
+ +

+ + Join {FIRST_NAME} for + + + Launch Week 12 + +

+

+ August 12-16 / 7AM PT +

+
+
+ + ), + { + width: OG_WIDTH, + height: OG_HEIGHT, + fonts: [ + { + name: 'Circular', + data: fontData, + style: 'normal', + }, + { + name: 'SourceCodePro', + data: monoFontData, + style: 'normal', + }, + ], + headers: { + 'content-type': 'image/png', + 'cache-control': 'public, max-age=31536000, s-maxage=31536000, no-transform, immutable', + 'cdn-cache-control': 'max-age=31536000', + }, + } + ) + + // [Note] Uncomment only for local testing to return the image directly and skip storage upload. + // return await generatedTicketImage + + // Upload image to storage. + const { error: storageError } = await supabaseAdminClient.storage + .from('images') + .upload(`launch-week/lw12/og/${ticketType}/${username}.png`, generatedTicketImage.body!, { + contentType: 'image/png', + // cacheControl: `${60 * 60 * 24 * 7}`, + cacheControl: `0`, + // Update cached og image, people might need to update info + upsert: true, + }) + + if (storageError) throw new Error(`storageError: ${storageError.message}`) + + const NEW_TIMESTAMP = new Date() + + return await fetch(`${STORAGE_URL}/og/${ticketType}/${username}.png?t=${NEW_TIMESTAMP}`) + } catch (error: any) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + }) + } +} diff --git a/apps/www/components/Footer/index.tsx b/apps/www/components/Footer/index.tsx index 2625e44407..e4a61d91f0 100644 --- a/apps/www/components/Footer/index.tsx +++ b/apps/www/components/Footer/index.tsx @@ -1,5 +1,4 @@ import Link from 'next/link' -import { useTheme } from 'next-themes' import { Badge, IconDiscord, IconGitHubSolid, IconTwitterX, IconYoutubeSolid, cn } from 'ui' import Image from 'next/image' import { useRouter } from 'next/router' @@ -10,6 +9,7 @@ import footerData from 'data/Footer' import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png' import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png' import { ThemeToggle } from 'ui-patterns/ThemeToggle' +import useDarkLaunchWeeks from '../../hooks/useDarkLaunchWeeks' interface Props { className?: string @@ -17,12 +17,11 @@ interface Props { } const Footer = (props: Props) => { - const { resolvedTheme } = useTheme() const { pathname } = useRouter() - const isLaunchWeek = pathname.includes('/launch-week') + const isDarkLaunchWeek = useDarkLaunchWeeks() const isGAWeek = pathname.includes('/ga-week') - const forceDark = isLaunchWeek || pathname === '/' + const forceDark = isDarkLaunchWeek || pathname === '/' if (props.hideFooter) { return null @@ -32,7 +31,7 @@ const Footer = (props: Props) => {