├── .eslintrc.json ├── .github ├── CODEOWNERS └── workflows │ └── prettify.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── bun.lockb ├── components.json ├── docs ├── CNAME ├── HighSeasVulnGIF-FoxMoss.webp ├── dino_debugging.png ├── index.html └── vuln-report.md ├── example.env ├── lib ├── battles │ ├── airtable.ts │ └── matchupGenerator.ts ├── flavor.js ├── pluralize.js ├── redis-cache.ts ├── redis-lock.ts ├── sum.js ├── transcript.js ├── transcript.yml ├── useEventEmitter.ts ├── useLocalStorageState.js └── yap.js ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── arrows.svg ├── audio │ └── yapping │ │ ├── _.wav │ │ ├── __attribution.txt │ │ ├── a.wav │ │ ├── b.wav │ │ ├── c.wav │ │ ├── d.wav │ │ ├── e.wav │ │ ├── f.wav │ │ ├── g.wav │ │ ├── h.wav │ │ ├── i.wav │ │ ├── j.wav │ │ ├── k.wav │ │ ├── l.wav │ │ ├── m.wav │ │ ├── n.wav │ │ ├── o.wav │ │ ├── p.wav │ │ ├── q.wav │ │ ├── r.wav │ │ ├── s.wav │ │ ├── sh.wav │ │ ├── t.wav │ │ ├── th.wav │ │ ├── thonk1.wav │ │ ├── thonk2.wav │ │ ├── thonk3.wav │ │ ├── u.wav │ │ ├── v.wav │ │ ├── w.wav │ │ ├── x.wav │ │ ├── y.wav │ │ └── z.wav ├── background.png ├── background.svg ├── bg.png ├── bg.svg ├── bg.webp ├── bubbledivider.svg ├── chest.png ├── comback.svg ├── compass.svg ├── curly-arrow.svg ├── dino_debugging.svg ├── dino_debugging_white.svg ├── divider.svg ├── dolphin.gif ├── doubloon.svg ├── faqbkgr.svg ├── favicons │ ├── apple-touch-icon.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── site.webmanifest │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png ├── flag-orpheus-top.svg ├── floorboard.svg ├── fonts │ ├── ADLaMDisplay-Regular.ttf │ ├── arialroundedmtbold.ttf │ └── minecraft_font.ttf ├── footerbelow.png ├── footerbelow.svg ├── footerbkgr.svg ├── gp.png ├── handraise.png ├── highlogo.png ├── highlogo.svg ├── howtobacks.svg ├── hr.svg ├── hydra.svg ├── idle.png ├── imgbkgr.png ├── jagged-card-tall-glow.svg ├── jagged-card-tall.svg ├── jagged-small-card.svg ├── logo.png ├── no-img-banner.png ├── no-img-dino.png ├── ogcard.png ├── party-orpheus.svg ├── pictures │ ├── pic1.png │ ├── pic2.png │ ├── pic3.png │ ├── pic4.png │ ├── pic5.png │ ├── pic6.png │ └── pic7.png ├── readme-helper.png ├── scripts │ ├── hackatime-install.ps1 │ └── hackatime-install.sh ├── ship.svg ├── shopback.svg ├── signpost.png ├── skull.svg ├── slack.svg ├── slidebkgr.png ├── special1.png ├── special2.png ├── stars.svg ├── sticky-holidays │ ├── d1.png │ ├── d1b.png │ └── d2b.png ├── talking.png ├── tavern.png ├── textures │ ├── cardboard.png │ └── paper.png ├── thinking.png ├── trashbeard_pfp_1.png └── videos │ ├── WakaSetupScriptLinux.mp4 │ ├── WakaSetupScriptMacOS.mp4 │ ├── WakaSetupScriptWindows.mp4 │ └── Wakatime Install.mp4 ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── app │ ├── [tab] │ │ └── page.tsx │ ├── api │ │ ├── admin │ │ │ └── impersonate │ │ │ │ └── [slackId] │ │ │ │ └── route.ts │ │ ├── battles │ │ │ ├── matchups │ │ │ │ ├── get-cached-projects.ts │ │ │ │ └── route.ts │ │ │ └── vote │ │ │ │ └── route.ts │ │ ├── buy │ │ │ └── [item] │ │ │ │ └── route.js │ │ ├── cron │ │ │ ├── create-background-job.ts │ │ │ ├── every-day │ │ │ │ └── route.ts │ │ │ ├── every-minute │ │ │ │ └── route.ts │ │ │ ├── process-background-jobs.ts │ │ │ └── ysws-updates.ts │ │ ├── ideagen │ │ │ └── route.js │ │ ├── marketing │ │ │ └── route.ts │ │ ├── project_ideas │ │ │ └── route.ts │ │ ├── referral │ │ │ └── [autonum] │ │ │ │ └── route.ts │ │ ├── slack_redirect │ │ │ └── route.ts │ │ └── stats │ │ │ └── route.tsx │ ├── favicon.ico │ ├── global-error.tsx │ ├── globals.css │ ├── harbor │ │ ├── battles │ │ │ ├── battles.tsx │ │ │ ├── blessed.tsx │ │ │ ├── cursed.tsx │ │ │ ├── fraud-utils.ts │ │ │ ├── mdc.tsx │ │ │ └── project-card.tsx │ │ ├── events │ │ │ └── events.tsx │ │ ├── gallery │ │ │ ├── gallery.tsx │ │ │ └── utils.ts │ │ ├── shipyard │ │ │ ├── edit-ship-form.tsx │ │ │ ├── idea-generator │ │ │ │ ├── data.js │ │ │ │ ├── dataa.js │ │ │ │ ├── datab.js │ │ │ │ ├── generator.js │ │ │ │ └── impl.js │ │ │ ├── new-ship-form.tsx │ │ │ ├── new-update-form.tsx │ │ │ ├── ship-utils.ts │ │ │ ├── ships.tsx │ │ │ ├── shipyard.tsx │ │ │ └── test.tsx │ │ ├── shop │ │ │ ├── progress.tsx │ │ │ ├── shop-item-component.js │ │ │ ├── shop-utils.ts │ │ │ ├── shop.tsx │ │ │ └── shopkeeper.js │ │ ├── signpost │ │ │ ├── best-ships.tsx │ │ │ ├── countdown.tsx │ │ │ ├── feed-items.tsx │ │ │ ├── help.ts │ │ │ ├── leaderboard.tsx │ │ │ ├── referral.tsx │ │ │ ├── signpost.tsx │ │ │ ├── verification.tsx │ │ │ ├── wakatime-config-tabs.tsx │ │ │ └── wakatime-setup-instructions.tsx │ │ ├── tabs │ │ │ ├── shepherd.css │ │ │ ├── tabs.tsx │ │ │ ├── tour.ts │ │ │ └── wakatime-setup-tutorial-modal.tsx │ │ └── tavern │ │ │ ├── map.tsx │ │ │ ├── tavern-utils.ts │ │ │ └── tavern.tsx │ ├── layout.tsx │ ├── marketing │ │ ├── art │ │ │ ├── divider.png │ │ │ ├── divider2.png │ │ │ ├── how1.png │ │ │ ├── how2.png │ │ │ ├── how3.png │ │ │ ├── orphwoah.png │ │ │ ├── paper.png │ │ │ ├── scales.png │ │ │ └── shop │ │ │ │ ├── shop1.png │ │ │ │ ├── shop2.png │ │ │ │ ├── shop3.png │ │ │ │ ├── shop4.png │ │ │ │ ├── shop5.png │ │ │ │ └── shop6.png │ │ ├── components │ │ │ ├── ScrollShop.jsx │ │ │ ├── ScrollShopReverse.jsx │ │ │ └── email-submission-form.tsx │ │ ├── index.css │ │ ├── invite-job.js │ │ ├── marketing-utils.ts │ │ ├── marquee │ │ │ ├── 1.jpeg │ │ │ ├── 10.jpeg │ │ │ ├── 12.jpeg │ │ │ ├── 13.jpeg │ │ │ ├── 14.jpeg │ │ │ ├── 15.jpeg │ │ │ ├── 16.jpeg │ │ │ ├── 17.jpeg │ │ │ ├── 18.jpeg │ │ │ ├── 19.jpeg │ │ │ ├── 2.png │ │ │ ├── 20.jpeg │ │ │ ├── 21.jpeg │ │ │ ├── 3.jpeg │ │ │ ├── 4.jpeg │ │ │ ├── 5.jpeg │ │ │ ├── 6.jpeg │ │ │ ├── 7.jpeg │ │ │ ├── 8.jpeg │ │ │ └── 9.jpeg │ │ └── page.tsx │ ├── page.tsx │ ├── signout │ │ └── route.ts │ ├── slack-error │ │ ├── page.tsx │ │ └── report-error.ts │ └── utils │ │ ├── airtable.ts │ │ ├── auth.ts │ │ ├── data.ts │ │ ├── server │ │ ├── airtable.ts │ │ ├── auth.ts │ │ └── data.ts │ │ ├── tavern.ts │ │ ├── waka.ts │ │ └── wakatime-setup │ │ ├── platforms.tsx │ │ ├── setup-modal.tsx │ │ ├── tutorial-utils.client.tsx │ │ └── tutorial-utils.ts ├── components │ ├── fullstory.js │ ├── idea-generator.css │ ├── idea-generator.js │ ├── jagged-card-small.tsx │ ├── jagged-card.tsx │ ├── markdown.tsx │ ├── nav.tsx │ ├── sign_in.tsx │ ├── sign_out.tsx │ ├── sound-button.js │ ├── speech-to-text.tsx │ ├── steps.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── loading_spinner.js │ │ ├── modal.tsx │ │ ├── multi-select.tsx │ │ ├── pill.tsx │ │ ├── popover.tsx │ │ ├── repo_link.tsx │ │ ├── separator.tsx │ │ ├── ship-pill-cluster.tsx │ │ ├── single-select.tsx │ │ ├── tabs.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── waka-lock.tsx ├── hooks │ └── use-toast.ts ├── instrumentation.ts ├── lib │ └── utils.ts ├── middleware.ts └── pages │ └── _error.jsx ├── tailwind.config.ts ├── test.ts ├── tsconfig.json ├── types └── battles │ └── airtable.ts └── vercel.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off", 5 | "@typescript-eslint/no-unused-vars": "off", 6 | "@typescript-eslint/no-empty-object-type": "off", 7 | "react/no-unescaped-entities": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @malted @maxwofford @polytroper 2 | -------------------------------------------------------------------------------- /.github/workflows/prettify.yml: -------------------------------------------------------------------------------- 1 | name: prettify check 2 | permissions: read-all 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | prettier: 12 | runs-on: ubuntu-latest 13 | name: Prettier 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v2 17 | 18 | - name: Use Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '20.x' 22 | 23 | - name: Install Dependencies 24 | run: npm install --no-save 25 | 26 | - name: Run Prettier 27 | run: npx prettier --check . 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | package-lock.json 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | yarn.lock 39 | 40 | .vscode 41 | .idea/ 42 | 43 | # Sentry Config File 44 | .env.sentry-build-plugin 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /docs -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 | 9 |

Build personal projects. Get free stuff.

10 |
11 | 12 | _Welcome to the Hack Club High Seas._ 13 | Built by a team of 20 teenagers from around the world, High Seas is a global challenge to every teenager to build awesome projects and get rewarded. 14 | 15 | Prizes range from iPads, Framework Laptops, Flipper Zeroes, and more. 16 | 17 | High Seas is the successor to [Arcade](https://hackclub.com/arcade/), Hack Club’s summer 2024 celebration of making. 5,000 high schoolers from 119 countries built over 2,000 projects and were shipped over 11,000 packages of prizes. 18 | 19 | ## Local development 20 | 21 | Set sail with a simple `bun i; bun run dev` 22 | 23 | You should configure your tokens/env based on the `example.env` (`cp example.env .env`), or if you're on the vercel team you can `bunx vercel env pull` to automatically load them up. 24 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | highseas.hackclub.com -------------------------------------------------------------------------------- /docs/HighSeasVulnGIF-FoxMoss.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/docs/HighSeasVulnGIF-FoxMoss.webp -------------------------------------------------------------------------------- /docs/dino_debugging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/docs/dino_debugging.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Maintenance mode 5 | 16 | 17 | 18 |

Ooops!

19 |

site under maintanence

20 | Under maintenance 21 |

We're currently working on a fix. Please check back soon!

22 | 23 | 24 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | AIRTABLE_API_KEY=REPLACE_ME 2 | AIRTABLE_ENDPOINT_URL="https://middleman.hackclub.com/airtable" 3 | AUTH_SECRET=REPLACE_ME 4 | BASE_ID="appTeNFYcUiYfGcR6" 5 | CRON_SECRET=AUTOMATICALLY_ASSIGNED_IN_PROD 6 | EDGE_CONFIG=AUTOMATICALLY_ASSIGNED_IN_PROD 7 | GITHUB_CLIENT_ID=REPLACE_ME 8 | GITHUB_CLIENT_SECRET=REPLACE_ME 9 | MATCHUP_SECRET=REPLACE_ME 10 | OPENAI_API_KEY=REPLACE_ME 11 | REDIS_URL=REPLACE_ME 12 | SLACK_AUTH_COOKIE_NAME="slackopenid" 13 | SLACK_CLIENT_ID=REPLACE_ME 14 | SLACK_CLIENT_SECRET=REPLACE_ME 15 | TURNSTILE_SECRET_KEY=REPLACE_ME 16 | WAKA_API_KEY=REPLACE_ME 17 | -------------------------------------------------------------------------------- /lib/flavor.js: -------------------------------------------------------------------------------- 1 | export const sample = (arr, seed = '') => { 2 | const random = seed === '' ? Math.random() : pseudoRandom(seed) 3 | return arr[Math.floor(random * arr.length)] 4 | } 5 | 6 | export const bound = (num, min, max) => { 7 | return Math.min(Math.max(num, min), max) 8 | } 9 | 10 | export const loadingSpinners = ['compass.svg', 'skull.svg'] 11 | 12 | const pseudoRandom = (seed) => { 13 | return Math.sin(seed * 10000) / 2 + 0.5 14 | } 15 | 16 | export const zeroMessage = [ 17 | 'Arrr, ye be flat broke!', 18 | "Ye're as poor as a landlubber!", 19 | "Ye've got no doubloons!", 20 | "Can't buy nothin' with nothin'!", 21 | ] 22 | 23 | // export const shopBlessed = [ 24 | // "Blimey! You got a blessing, didja? Well that's gotta do good for business seeing such upstanding pirate folk shoping here.", 25 | // "Wow! You got the boon of a pirate's blessing!", 26 | // `Blimey, a pirate's blessing! *She ${transcript('superstitions')}, according to pirate tradition.* Please, shop to your heart's content!`, 27 | // ] 28 | 29 | export const purchaseWords = [ 30 | // Don't uniquify this! Some are rarer than others 31 | 'Acquire', 32 | 'Acquire', 33 | 'Buye', 34 | 'Buye', 35 | 'Obtain', 36 | 'Obtain', 37 | 'Procure', 38 | 'Procure', 39 | 'Steal', 40 | 'Plunder', 41 | 'Plunder', 42 | 'Plunder', 43 | 'BitTorrent', 44 | 'Loot', 45 | ] 46 | 47 | export const cantAffordWords = [ 48 | 'too expensive...', 49 | // "can't afford this...", 50 | "can't afford...", 51 | 'unaffordable...', 52 | 'out of reach...', 53 | 'need doubloons...', 54 | 'too pricey...', 55 | // "when you're richer...", 56 | ] 57 | 58 | export const shopIcons = { 59 | freaking: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/0freaking.png', 60 | ded: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/1ded.png', 61 | info: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/2info.png', 62 | peefest: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/3pf.png', 63 | threat: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/4threatened.png', 64 | reading: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/5reading_2.png', 65 | reading2: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/6reading.png', 66 | scallywag: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/7scallywag.png', 67 | search: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/8searching_2.png', 68 | search2: 'https://cloud-iz667ljca-hack-club-bot.vercel.app/9searching.png', 69 | question: 'https://cloud-nfmmdwony-hack-club-bot.vercel.app/0000.png', 70 | cute: 'https://cloud-nfmmdwony-hack-club-bot.vercel.app/3003.png', 71 | tinfoil: 'https://cloud-nfmmdwony-hack-club-bot.vercel.app/4004.png', 72 | holdingEars: 'https://cloud-nfmmdwony-hack-club-bot.vercel.app/4004.png', // todo: draw this 73 | notamused: 'https://cloud-nfmmdwony-hack-club-bot.vercel.app/4004.png', // todo: draw this 74 | woah: 'https://cloud-e9zuzn0u0-hack-club-bot.vercel.app/3003.png', 75 | thumbs: 'https://cloud-r04a8za6c-hack-club-bot.vercel.app/1001.png', 76 | fluster: 77 | 'https://cloud-3wb98dblo-hack-club-bot.vercel.app/0untitled_artwork.gif', 78 | bapanada: 79 | 'https://cloud-o4qa8261z-hack-club-bot.vercel.app/0270a4575-6fa8-4946-b5c8-3fb5b8d9b722-1660621656235.webp', 80 | sticker: 'https://cloud-jckns370p-hack-club-bot.vercel.app/0find_out.png', 81 | stickerSheet: 'https://cloud-opr8cwuhm-hack-club-bot.vercel.app/0the-bin.png', 82 | sticker2: 'https://cloud-4wvt8n7o9-hack-club-bot.vercel.app/0heidiflag.png', 83 | } 84 | 85 | export const outOfStock = [ 86 | "icon:question|where'd all the stock go?", 87 | 'icon:question|come back when more washes up on shore', 88 | ] 89 | -------------------------------------------------------------------------------- /lib/pluralize.js: -------------------------------------------------------------------------------- 1 | // pluralize(1, 'apple') => '1 apple' 2 | // pluralize(2, 'apple') => '2 apples' 3 | // pluralize(2, 'apple', false) => 'apples' 4 | 5 | export default function pluralize(count, singular, includeCount = true) { 6 | return ( 7 | (includeCount ? `${count} ` : '') + 8 | (count === 1 ? singular : singular + 's') 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /lib/redis-cache.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | 3 | import { kv } from '@vercel/kv' 4 | 5 | const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds 6 | 7 | export async function cached( 8 | key: string, 9 | action: () => Promise, 10 | timeout = DEFAULT_TIMEOUT, 11 | ): Promise { 12 | const cachedValue = await kv.get(key) 13 | if (cachedValue) { 14 | console.log(`Cache hit for ${key}`) 15 | return cachedValue 16 | } 17 | 18 | const value = await action() 19 | console.log(`Setting cache for ${key}`) 20 | console.log('value', value) 21 | await kv.set(key, JSON.stringify(value), { 22 | ex: timeout, 23 | }) 24 | return value 25 | } 26 | -------------------------------------------------------------------------------- /lib/redis-lock.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | 3 | import { kv } from '@vercel/kv' 4 | import { v4 as uuidv4 } from 'uuid' 5 | 6 | const LOCK_TIMEOUT = 30 * 1000 // 30 seconds 7 | 8 | async function aquireLock(key: string): Promise { 9 | const lockKey = `lock:${key}` 10 | const lockValue = uuidv4() 11 | const acquired = await kv.set(lockKey, lockValue, { 12 | nx: true, 13 | px: LOCK_TIMEOUT, 14 | }) 15 | return acquired ? lockValue : null 16 | } 17 | 18 | async function releaseLock(lockKey: string, lockValue: string): Promise { 19 | const currentLockValue = await kv.get(lockKey) 20 | if (currentLockValue === lockValue) { 21 | await kv.del(lockKey) 22 | } 23 | } 24 | 25 | export async function withLock( 26 | key: string, 27 | action: () => Promise, 28 | ): Promise { 29 | const lockValue = await aquireLock(key) 30 | if (!lockValue) { 31 | throw new Error(`Failed to acquire lock for key: ${key}`) 32 | } 33 | try { 34 | return await action() 35 | } finally { 36 | await releaseLock(`lock:${key}`, lockValue) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/sum.js: -------------------------------------------------------------------------------- 1 | export default function sum(arr) { 2 | return arr.reduce((a, b) => a + b, 0) 3 | } 4 | -------------------------------------------------------------------------------- /lib/transcript.js: -------------------------------------------------------------------------------- 1 | import transcriptData from './transcript.yml' 2 | 3 | function transcript(path, context = {}, skipArrays = true, depth = 0) { 4 | console.log({ path, depth }) 5 | if (depth > 10) { 6 | console.log('hit recursion depth 10!') 7 | return 8 | } 9 | try { 10 | const leaf = getDescendantProp(transcriptData, path, skipArrays) 11 | if (typeof leaf === 'string') { 12 | return evalInContext('`' + leaf + '`', { ...context, t: transcript }) 13 | } 14 | if (leaf === undefined) { 15 | return `transcript.${path}` 16 | } else { 17 | return leaf 18 | } 19 | } catch (e) { 20 | console.error(e) 21 | return path 22 | } 23 | } 24 | 25 | export { transcript } 26 | 27 | function evalInContext(js, context) { 28 | if (process.env.NODE_ENV === 'development') { 29 | console.log({ js, context }) 30 | } 31 | return function () { 32 | return eval(js) 33 | }.call(context) 34 | } 35 | 36 | function getDescendantProp(obj, desc, skipArrays) { 37 | const arr = desc.split('.') 38 | if (process.env.NODE_ENV === 'development') { 39 | console.log({ arr, obj }) 40 | } 41 | while (arr.length) { 42 | obj = obj[arr.shift()] 43 | if (Array.isArray(obj) && skipArrays) { 44 | obj = obj[Math.floor(Math.random() * obj.length)] 45 | } 46 | } 47 | return obj 48 | } 49 | -------------------------------------------------------------------------------- /lib/useEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | const globalEmitter = new EventTarget() 4 | 5 | export function useEventEmitter() { 6 | const emitterRef = useRef(globalEmitter) 7 | 8 | const emit = (eventName: string, detail?: any) => { 9 | emitterRef.current.dispatchEvent(new CustomEvent(eventName, { detail })) 10 | } 11 | 12 | // handy helper for shopkeeper interactions 13 | const emitYap = (interaction: string) => { 14 | emitterRef.current.dispatchEvent( 15 | new CustomEvent('shopkeeper', { detail: { interaction } }), 16 | ) 17 | } 18 | 19 | const on = (eventName: string, callback: (event: CustomEvent) => void) => { 20 | emitterRef.current.addEventListener(eventName, callback as EventListener) 21 | } 22 | 23 | const off = (eventName: string, callback: (event: CustomEvent) => void) => { 24 | emitterRef.current.removeEventListener(eventName, callback as EventListener) 25 | } 26 | 27 | return { emit, on, off, emitYap } 28 | } 29 | -------------------------------------------------------------------------------- /lib/useLocalStorageState.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | function getFromLocalStorage(key) { 6 | if (typeof localStorage === 'undefined') { 7 | return null 8 | } 9 | const item = localStorage.getItem(key) 10 | try { 11 | const obj = JSON.parse(item) 12 | 13 | if (obj) { 14 | if (obj.expiration && obj.expiration < Date.now()) { 15 | localStorage.removeItem(key) 16 | return null 17 | } 18 | 19 | if (obj.value) { 20 | return obj.value 21 | } 22 | 23 | return null 24 | } 25 | } catch (e) { 26 | console.error(e) 27 | return null 28 | } 29 | } 30 | 31 | function setToLocalStorage(key, value, expiration) { 32 | if (typeof localStorage === 'undefined') { 33 | return 34 | } 35 | const objToSave = { value } 36 | if (expiration) { 37 | timeToExpire = Date.now() + expiration 38 | objToSave.expiration = timeToExpire 39 | } 40 | localStorage.setItem(key, JSON.stringify(objToSave)) 41 | } 42 | 43 | function useLocalStorageState(key, initialValue) { 44 | const [stateValue, setStateValue] = useState( 45 | getFromLocalStorage(key) || initialValue, 46 | ) 47 | 48 | const setValue = (value) => { 49 | setToLocalStorage(key, value) 50 | setStateValue(value) 51 | } 52 | 53 | return [stateValue, setValue] 54 | } 55 | 56 | export default useLocalStorageState 57 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | import { withPlausibleProxy } from 'next-plausible' 4 | import withYaml from 'next-plugin-yaml' 5 | import { execSync } from 'child_process' 6 | const commitHash = execSync('git log --pretty=format:"%h" -n1') 7 | .toString() 8 | .trim() 9 | import { withSentryConfig } from '@sentry/nextjs' 10 | 11 | const nextConfig = { 12 | typescript: { 13 | // !! WARN !! 14 | // Dangerously allow production builds to successfully complete even if 15 | // your project has type errors. 16 | // !! WARN !! 17 | ignoreBuildErrors: true, 18 | }, 19 | reactStrictMode: false, 20 | env: { 21 | COMMIT_HASH: commitHash, 22 | }, 23 | images: { 24 | remotePatterns: [ 25 | { protocol: 'https', hostname: 'avatars.slack-edge.com' }, 26 | { protocol: 'https', hostname: '**' }, 27 | ], 28 | }, 29 | } 30 | 31 | const sentryContext = { 32 | org: 'malted', 33 | project: 'javascript-nextjs', 34 | silent: !process.env.CI, 35 | widenClientFileUpload: true, 36 | reactComponentAnnotation: { 37 | enabled: true, 38 | }, 39 | tunnelRoute: '/monitoring', 40 | hideSourceMaps: true, 41 | disableLogger: true, 42 | automaticVercelMonitors: true, 43 | } 44 | 45 | const plausibleConfig = withPlausibleProxy()(nextConfig) 46 | 47 | const yamlConfig = withYaml(plausibleConfig) 48 | 49 | export default withSentryConfig(yamlConfig, sentryContext) 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "high-seas", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "fmt": "prettier --write ." 11 | }, 12 | "dependencies": { 13 | "@ai-sdk/openai": "^1.0.11", 14 | "@hackclub/icons": "^0.0.15", 15 | "@radix-ui/react-dialog": "^1.1.2", 16 | "@radix-ui/react-popover": "^1.1.2", 17 | "@radix-ui/react-separator": "^1.1.0", 18 | "@radix-ui/react-slot": "^1.1.0", 19 | "@radix-ui/react-tabs": "^1.1.0", 20 | "@radix-ui/react-toast": "^1.2.2", 21 | "@sentry/nextjs": "^8", 22 | "@types/js-cookie": "^3.0.6", 23 | "@vercel/analytics": "^1.3.2", 24 | "@vercel/edge-config": "^1.4.0", 25 | "@vercel/kv": "^3.0.0", 26 | "@vercel/postgres": "^0.10.0", 27 | "@vercel/speed-insights": "^1.0.12", 28 | "ai": "^4.0.22", 29 | "airtable": "^0.12.2", 30 | "class-variance-authority": "^0.7.0", 31 | "clsx": "^2.1.1", 32 | "cmdk": "1.0.0", 33 | "framer-motion": "^11.5.6", 34 | "from": "^0.1.7", 35 | "howler": "^2.2.4", 36 | "import": "^0.0.6", 37 | "ioredis": "^5.4.1", 38 | "javascript-time-ago": "^2.5.11", 39 | "js-confetti": "^0.12.0", 40 | "js-cookie": "^3.0.5", 41 | "leaflet": "^1.9.4", 42 | "lucide-react": "^0.446.0", 43 | "next": "14.2.15", 44 | "next-plausible": "^3.12.2", 45 | "next-plugin-yaml": "^1.0.1", 46 | "openai": "^4.68.4", 47 | "react": "^18", 48 | "react-dom": "^18", 49 | "react-fast-marquee": "^1.6.5", 50 | "react-fullstory": "^1.4.0", 51 | "react-infinite-scroll-component": "^6.1.0", 52 | "react-leaflet": "^4.2.1", 53 | "react-markdown": "^9.0.1", 54 | "rehype-raw": "^7.0.0", 55 | "remark-gfm": "^4.0.0", 56 | "sharp": "^0.33.5", 57 | "shepherd.js": "^14.1.0", 58 | "tailwind-merge": "^2.5.2", 59 | "tailwindcss-animate": "^1.0.7", 60 | "uuid": "^11.0.3" 61 | }, 62 | "devDependencies": { 63 | "@types/leaflet": "^1.9.16", 64 | "@types/node": "^20", 65 | "@types/react": "^18", 66 | "@types/react-dom": "^18", 67 | "eslint": "^8", 68 | "eslint-config-next": "14.2.13", 69 | "postcss": "^8", 70 | "prettier": "3.3.3", 71 | "tailwindcss": "^3.4.13", 72 | "typescript": "^5" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /public/arrows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/audio/yapping/_.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/_.wav -------------------------------------------------------------------------------- /public/audio/yapping/__attribution.txt: -------------------------------------------------------------------------------- 1 | these are from https://github.com/equalo-official/animalese-generator! 2 | -------------------------------------------------------------------------------- /public/audio/yapping/a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/a.wav -------------------------------------------------------------------------------- /public/audio/yapping/b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/b.wav -------------------------------------------------------------------------------- /public/audio/yapping/c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/c.wav -------------------------------------------------------------------------------- /public/audio/yapping/d.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/d.wav -------------------------------------------------------------------------------- /public/audio/yapping/e.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/e.wav -------------------------------------------------------------------------------- /public/audio/yapping/f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/f.wav -------------------------------------------------------------------------------- /public/audio/yapping/g.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/g.wav -------------------------------------------------------------------------------- /public/audio/yapping/h.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/h.wav -------------------------------------------------------------------------------- /public/audio/yapping/i.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/i.wav -------------------------------------------------------------------------------- /public/audio/yapping/j.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/j.wav -------------------------------------------------------------------------------- /public/audio/yapping/k.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/k.wav -------------------------------------------------------------------------------- /public/audio/yapping/l.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/l.wav -------------------------------------------------------------------------------- /public/audio/yapping/m.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/m.wav -------------------------------------------------------------------------------- /public/audio/yapping/n.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/n.wav -------------------------------------------------------------------------------- /public/audio/yapping/o.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/o.wav -------------------------------------------------------------------------------- /public/audio/yapping/p.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/p.wav -------------------------------------------------------------------------------- /public/audio/yapping/q.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/q.wav -------------------------------------------------------------------------------- /public/audio/yapping/r.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/r.wav -------------------------------------------------------------------------------- /public/audio/yapping/s.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/s.wav -------------------------------------------------------------------------------- /public/audio/yapping/sh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/sh.wav -------------------------------------------------------------------------------- /public/audio/yapping/t.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/t.wav -------------------------------------------------------------------------------- /public/audio/yapping/th.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/th.wav -------------------------------------------------------------------------------- /public/audio/yapping/thonk1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/thonk1.wav -------------------------------------------------------------------------------- /public/audio/yapping/thonk2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/thonk2.wav -------------------------------------------------------------------------------- /public/audio/yapping/thonk3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/thonk3.wav -------------------------------------------------------------------------------- /public/audio/yapping/u.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/u.wav -------------------------------------------------------------------------------- /public/audio/yapping/v.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/v.wav -------------------------------------------------------------------------------- /public/audio/yapping/w.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/w.wav -------------------------------------------------------------------------------- /public/audio/yapping/x.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/x.wav -------------------------------------------------------------------------------- /public/audio/yapping/y.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/y.wav -------------------------------------------------------------------------------- /public/audio/yapping/z.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/audio/yapping/z.wav -------------------------------------------------------------------------------- /public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/background.png -------------------------------------------------------------------------------- /public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/bg.png -------------------------------------------------------------------------------- /public/bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/bg.webp -------------------------------------------------------------------------------- /public/chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/chest.png -------------------------------------------------------------------------------- /public/compass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/curly-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/divider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/dolphin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/dolphin.gif -------------------------------------------------------------------------------- /public/doubloon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "High Seas -- Hack Club", 3 | "short_name": "High Seas", 4 | "icons": [ 5 | { 6 | "src": "/favicons/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/favicons/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#3ea2ef", 19 | "background_color": "#42a4ef", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /public/favicons/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/favicons/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/favicons/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/favicons/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /public/floorboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/fonts/ADLaMDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/fonts/ADLaMDisplay-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/arialroundedmtbold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/fonts/arialroundedmtbold.ttf -------------------------------------------------------------------------------- /public/fonts/minecraft_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/fonts/minecraft_font.ttf -------------------------------------------------------------------------------- /public/footerbelow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/footerbelow.png -------------------------------------------------------------------------------- /public/gp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/gp.png -------------------------------------------------------------------------------- /public/handraise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/handraise.png -------------------------------------------------------------------------------- /public/highlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/highlogo.png -------------------------------------------------------------------------------- /public/hr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/idle.png -------------------------------------------------------------------------------- /public/imgbkgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/imgbkgr.png -------------------------------------------------------------------------------- /public/jagged-small-card.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/logo.png -------------------------------------------------------------------------------- /public/no-img-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/no-img-banner.png -------------------------------------------------------------------------------- /public/no-img-dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/no-img-dino.png -------------------------------------------------------------------------------- /public/ogcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/ogcard.png -------------------------------------------------------------------------------- /public/pictures/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic1.png -------------------------------------------------------------------------------- /public/pictures/pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic2.png -------------------------------------------------------------------------------- /public/pictures/pic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic3.png -------------------------------------------------------------------------------- /public/pictures/pic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic4.png -------------------------------------------------------------------------------- /public/pictures/pic5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic5.png -------------------------------------------------------------------------------- /public/pictures/pic6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic6.png -------------------------------------------------------------------------------- /public/pictures/pic7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/pictures/pic7.png -------------------------------------------------------------------------------- /public/readme-helper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/readme-helper.png -------------------------------------------------------------------------------- /public/scripts/hackatime-install.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Starting Hackatime setup..." 2 | 3 | # Check if BEARER_TOKEN is set 4 | if (-not $env:BEARER_TOKEN) { 5 | Write-Host "Error: BEARER_TOKEN environment variable is not set" 6 | return 7 | } 8 | 9 | # Step 1: Add settings to the WakaTime config file 10 | $bearerToken = $env:BEARER_TOKEN 11 | $configContent = @" 12 | [settings] 13 | api_url = https://waka.hackclub.com/api 14 | api_key = $bearerToken 15 | "@ 16 | $configPath = "$HOME\.wakatime.cfg" 17 | 18 | # Create or update the WakaTime config file 19 | Write-Host "Configuring WakaTime settings..." 20 | Set-Content -Path $configPath -Value $configContent 21 | Write-Host "$([char]9830) Wrote config to $configPath" 22 | Write-Host "" 23 | 24 | 25 | # Step 2: Check if VS Code is installed 26 | Write-Host "Checking for VS Code installation..." 27 | if (-not (Get-Command code -ErrorAction SilentlyContinue)) { 28 | Write-Host "Error: VS Code is not installed. You can install it at https://code.visualstudio.com/Download" 29 | Write-Host "(If you're certain it is, open vscode, hit Ctrl+Shift+P and type `"Shell Command: Install 'code' command in PATH`") then press enter. After doing that come back here, close and reopen this powershell window and rerun the setup command" 30 | return 31 | } 32 | 33 | # Install the WakaTime extension in VS Code 34 | Write-Host "Installing the WakaTime extension in VS Code..." 35 | try { 36 | & code --install-extension WakaTime.vscode-wakatime 37 | Write-Host "$([char]9830) VS Code extension installed successfully" 38 | } catch { 39 | Write-Host "Error: Failed to install WakaTime extension" 40 | return 41 | } 42 | Write-Host "" 43 | 44 | Write-Host "Sending test heartbeats to verify setup..." 45 | # Step 3: Run the curl command twice with a conditional sleep 46 | for ($i = 1; $i -le 2; $i++) { 47 | Write-Host "Sending heartbeat $i/2..." 48 | 49 | $time = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() 50 | $jsonData = @{ 51 | branch = "master" 52 | category = "coding" 53 | cursorpos = 1 54 | entity = "welcome.txt" 55 | type = "file" 56 | lineno = 1 57 | lines = 1 58 | project = "welcome" 59 | time = $time # Removed quotes to keep it as a number 60 | user_agent = "wakatime/v1.102.1 (windows) go1.22.5 vscode/1.94.2 vscode-wakatime/24.6.2" 61 | } | ConvertTo-Json -Compress 62 | 63 | try { 64 | $response = Invoke-RestMethod -Uri "https://waka.hackclub.com/api/heartbeat" ` 65 | -Method Post ` 66 | -Headers @{ 67 | Authorization = "Bearer $bearerToken" 68 | "Content-Type" = "application/json" 69 | } ` 70 | -Body $jsonData 71 | Write-Host "$([char]9830) Heartbeat sent successfully" 72 | } catch { 73 | Write-Host "Error: Heartbeat failed with HTTP status $($_.Exception.Response.StatusCode)" 74 | return 75 | } 76 | 77 | # Sleep for 1 second only if this is not the last iteration 78 | if ($i -lt 2) { 79 | Start-Sleep -Seconds 1 80 | } 81 | } 82 | 83 | Write-Host "" 84 | Write-Host "$([char]8730) Hackatime setup completed successfully!" 85 | Write-Host "You can now return to the setup page for further instructions." -------------------------------------------------------------------------------- /public/ship.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/signpost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/signpost.png -------------------------------------------------------------------------------- /public/slack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/slidebkgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/slidebkgr.png -------------------------------------------------------------------------------- /public/special1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/special1.png -------------------------------------------------------------------------------- /public/special2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/special2.png -------------------------------------------------------------------------------- /public/sticky-holidays/d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/sticky-holidays/d1.png -------------------------------------------------------------------------------- /public/sticky-holidays/d1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/sticky-holidays/d1b.png -------------------------------------------------------------------------------- /public/sticky-holidays/d2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/sticky-holidays/d2b.png -------------------------------------------------------------------------------- /public/talking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/talking.png -------------------------------------------------------------------------------- /public/tavern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/tavern.png -------------------------------------------------------------------------------- /public/textures/cardboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/textures/cardboard.png -------------------------------------------------------------------------------- /public/textures/paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/textures/paper.png -------------------------------------------------------------------------------- /public/thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/thinking.png -------------------------------------------------------------------------------- /public/trashbeard_pfp_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/trashbeard_pfp_1.png -------------------------------------------------------------------------------- /public/videos/WakaSetupScriptLinux.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/videos/WakaSetupScriptLinux.mp4 -------------------------------------------------------------------------------- /public/videos/WakaSetupScriptMacOS.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/videos/WakaSetupScriptMacOS.mp4 -------------------------------------------------------------------------------- /public/videos/WakaSetupScriptWindows.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/videos/WakaSetupScriptWindows.mp4 -------------------------------------------------------------------------------- /public/videos/Wakatime Install.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/public/videos/Wakatime Install.mp4 -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs' 6 | 7 | Sentry.init({ 8 | dsn: 'https://a50b849edd5242bba9e91047fd36711d@o4508422526795776.ingest.us.sentry.io/4508422529548288', 9 | 10 | // Add optional integrations for additional features 11 | integrations: [Sentry.replayIntegration()], 12 | 13 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 14 | tracesSampleRate: 1, 15 | 16 | // Define how likely Replay events are sampled. 17 | // This sets the sample rate to be 10%. You may want this to be 100% while 18 | // in development and sample at a lower rate in production 19 | replaysSessionSampleRate: 0.1, 20 | 21 | // Define how likely Replay events are sampled when an error occurs. 22 | replaysOnErrorSampleRate: 1.0, 23 | 24 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 25 | debug: false, 26 | }) 27 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from '@sentry/nextjs' 7 | 8 | Sentry.init({ 9 | dsn: 'https://a50b849edd5242bba9e91047fd36711d@o4508422526795776.ingest.us.sentry.io/4508422529548288', 10 | 11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }) 17 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs' 6 | 7 | Sentry.init({ 8 | dsn: 'https://a50b849edd5242bba9e91047fd36711d@o4508422526795776.ingest.us.sentry.io/4508422529548288', 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }) 16 | -------------------------------------------------------------------------------- /src/app/[tab]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { notFound } from 'next/navigation' 4 | import Harbor from '../harbor/tabs/tabs' 5 | import { createMagicSession, getSession } from '../utils/auth' 6 | import { Card } from '@/components/ui/card' 7 | import { SoundButton } from '../../components/sound-button.js' 8 | import { useEffect } from 'react' 9 | import useLocalStorageState from '../../../lib/useLocalStorageState' 10 | 11 | export default function Page({ 12 | params, 13 | searchParams, 14 | }: { 15 | params: { tab: string } 16 | searchParams: any 17 | }) { 18 | const [session, setSession] = useLocalStorageState('cache.session', {}) 19 | 20 | useEffect(() => { 21 | getSession().then((s) => { 22 | if (s) { 23 | setSession(s) 24 | } else { 25 | window.location.pathname = '/' 26 | } 27 | }) 28 | }, []) 29 | 30 | const { tab } = params 31 | const validTabs = ['signpost', 'shipyard', 'wonderdome', 'shop', 'tavern'] 32 | if (!validTabs.includes(tab)) return notFound() 33 | 34 | const { magic_auth_token } = searchParams 35 | 36 | if (magic_auth_token) { 37 | console.info('maigc auth token:', magic_auth_token) 38 | // First check for is_full_user, if so, redirect to slack auth 39 | // const person = 40 | 41 | createMagicSession(magic_auth_token).then( 42 | () => (window.location.href = window.location.pathname), 43 | ) 44 | } 45 | 46 | return ( 47 | <> 48 |
57 | 58 | 62 | {session?.slackId ? ( 63 | 64 | ) : ( 65 |

Session is loading...

66 | )} 67 |
68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/app/api/admin/impersonate/[slackId]/route.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | import { NextRequest } from 'next/server' 3 | 4 | import { impersonate } from '@/app/utils/auth' 5 | 6 | export async function GET( 7 | _request: NextRequest, 8 | { params }: { params: { slackId: string } }, 9 | ) { 10 | if (process.env.NODE_ENV === 'development') { 11 | await impersonate(params.slackId) 12 | } 13 | 14 | redirect('/signpost') 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/battles/matchups/get-cached-projects.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv' 2 | import { getAllProjects } from '../../../../../lib/battles/airtable' 3 | 4 | // 1mb limit on each redis entry 5 | const PROJECT_CHUNK_SIZE = 1400 6 | const PROJECT_CACHE_TTL = 60 * 10 7 | 8 | async function pullFromRedis() { 9 | const chunkCount = await kv.get('projects.size') 10 | 11 | if (!chunkCount) { 12 | return null 13 | } 14 | if (typeof chunkCount !== 'number') { 15 | return null 16 | } 17 | 18 | const chunks = await Promise.all( 19 | Array.from({ length: chunkCount }, (_, i) => kv.get(`projects.${i}`)), 20 | ) 21 | return chunks.flat() 22 | } 23 | async function setToRedis(projectsArr: Ships[]) { 24 | console.log('Setting projects to Redis') 25 | const chunkCount = Math.ceil(projectsArr.length / PROJECT_CHUNK_SIZE) 26 | await kv.set('projects.size', chunkCount, { ex: PROJECT_CACHE_TTL }) 27 | for (let i = 0; i < projectsArr.length; i += PROJECT_CHUNK_SIZE) { 28 | await kv.set( 29 | `projects.${i / PROJECT_CHUNK_SIZE}`, 30 | projectsArr.slice(i, i + PROJECT_CHUNK_SIZE), 31 | { ex: PROJECT_CACHE_TTL }, 32 | ) 33 | } 34 | } 35 | 36 | export async function tryUpdateProjectCache() { 37 | const chunkCount = await kv.get('projects.size') 38 | const currentlyCached = Boolean(chunkCount) 39 | const every2Minutes = new Date().getMinutes() % 2 === 0 40 | 41 | if (!currentlyCached || every2Minutes) { 42 | await updateProjectCache() 43 | } 44 | } 45 | 46 | export async function updateProjectCache() { 47 | const projects = await getAllProjects() 48 | await setToRedis(projects) 49 | } 50 | 51 | export async function getCachedProjects() { 52 | const alreadyCached = await pullFromRedis() 53 | if (alreadyCached) { 54 | return alreadyCached 55 | } 56 | const projects = await getAllProjects() 57 | await setToRedis(projects) 58 | return projects 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/battles/matchups/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { 3 | generateMatchup, 4 | generateTutorialMatchup, 5 | } from '../../../../../lib/battles/matchupGenerator' 6 | import { getSession } from '@/app/utils/auth' 7 | import { getCachedProjects } from './get-cached-projects' 8 | import { NextRequest } from 'next/server' 9 | 10 | export const dynamic = 'force-dynamic' 11 | 12 | export async function GET(request: NextRequest) { 13 | const isTutorial = request.nextUrl.searchParams.get('tutorial') 14 | 15 | const session = await getSession() 16 | if (!session) { 17 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 18 | } 19 | 20 | try { 21 | let matchup 22 | if (isTutorial) { 23 | matchup = generateTutorialMatchup() 24 | } else { 25 | const projects = await getCachedProjects() 26 | const userSlackId = session.slackId 27 | 28 | // TODO: this filtering could happen in the generateMatchup function! 29 | const votableProjects = projects.filter( 30 | (project) => project?.['entrant__slack_id']?.[0] !== userSlackId, 31 | ) 32 | matchup = await generateMatchup(votableProjects, userSlackId) 33 | } 34 | 35 | if (!matchup) { 36 | return NextResponse.json( 37 | { error: 'No valid matchup found' }, 38 | { status: 404 }, 39 | ) 40 | } 41 | 42 | const rMatchup = { 43 | project1: { 44 | id: matchup.project1.id, 45 | title: matchup.project1.title, 46 | screenshot_url: matchup.project1.screenshot_url, 47 | readme_url: matchup.project1.readme_url, 48 | repo_url: matchup.project1.repo_url, 49 | deploy_url: matchup.project1.deploy_url, 50 | rating: matchup.project1.rating, 51 | ship_type: matchup.project1.ship_type, 52 | update_description: matchup.project1.update_description, 53 | entrant__slack_id: matchup.project1.entrant__slack_id[0], 54 | }, 55 | project2: { 56 | id: matchup.project2.id, 57 | title: matchup.project2.title, 58 | screenshot_url: matchup.project2.screenshot_url, 59 | readme_url: matchup.project2.readme_url, 60 | repo_url: matchup.project2.repo_url, 61 | deploy_url: matchup.project2.deploy_url, 62 | rating: matchup.project2.rating, 63 | ship_type: matchup.project2.ship_type, 64 | update_description: matchup.project2.update_description, 65 | entrant__slack_id: matchup.project2.entrant__slack_id[0], 66 | }, 67 | signature: matchup.signature, 68 | ts: matchup.ts, 69 | } 70 | 71 | return NextResponse.json(rMatchup) 72 | } catch (error) { 73 | console.error('Error generating matchup:', error) 74 | return NextResponse.json( 75 | { error: 'Failed to generate matchup' }, 76 | { status: 500 }, 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/api/battles/vote/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { 3 | ensureUniqueVote, 4 | submitVote, 5 | } from '../../../../../lib/battles/airtable' 6 | import { getSession } from '@/app/utils/auth' 7 | import { verifyMatchup } from '../../../../../lib/battles/matchupGenerator' 8 | 9 | export async function POST(request: Request) { 10 | const session = await getSession() 11 | if (!session) { 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 13 | } 14 | 15 | const personId = session.personId 16 | 17 | try { 18 | const voteData = await request.json() 19 | 20 | const winnerAnalytics = voteData.analytics.projectResources[ 21 | voteData.winner 22 | ] || { 23 | readmeOpened: false, 24 | repoOpened: false, 25 | demoOpened: false, 26 | } 27 | 28 | const loserAnalytics = voteData.analytics.projectResources[ 29 | voteData.loser 30 | ] || { 31 | readmeOpened: false, 32 | repoOpened: false, 33 | demoOpened: false, 34 | } 35 | 36 | const matchup = { 37 | winner: voteData.winner, 38 | loser: voteData.loser, 39 | signature: voteData.signature, 40 | ts: voteData.ts, 41 | } 42 | // @ts-expect-error because i don't understand typescript 43 | const isVerified = verifyMatchup(matchup, session.slackId) 44 | if (!isVerified) { 45 | return NextResponse.json( 46 | { error: 'Invalid matchup signature' }, 47 | { status: 400 }, 48 | ) 49 | } 50 | 51 | voteData.winner_readme_opened = winnerAnalytics.readmeOpened 52 | voteData.winner_repo_opened = winnerAnalytics.repoOpened 53 | voteData.winner_demo_opened = winnerAnalytics.demoOpened 54 | voteData.loser_readme_opened = loserAnalytics.readmeOpened 55 | voteData.loser_repo_opened = loserAnalytics.repoOpened 56 | voteData.loser_demo_opened = loserAnalytics.demoOpened 57 | voteData.skips_before_vote = voteData.analytics.skipsBeforeVote 58 | 59 | const _result = await submitVote(voteData, personId, session.slackId) 60 | 61 | return NextResponse.json({ ok: true /*, reload: isBot */ }) 62 | } catch (error) { 63 | console.error('Error submitting vote:', error) 64 | return NextResponse.json( 65 | { error: 'Failed to submit vote' }, 66 | { status: 500 }, 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/api/buy/[item]/route.js: -------------------------------------------------------------------------------- 1 | import { getSession } from '@/app/utils/auth' 2 | import { redirect } from 'next/navigation' 3 | import { NextResponse } from 'next/server' 4 | import { getSelfPerson } from '@/app/utils/server/airtable' 5 | import { base } from 'airtable' 6 | 7 | export async function GET(request, { params }) { 8 | const session = await getSession() 9 | const person = await getSelfPerson(session.slackId) 10 | if (!person) { 11 | return NextResponse.json( 12 | { error: "i don't even know who you are" }, 13 | { status: 418 }, 14 | ) 15 | } 16 | const b = await base(process.env.BASE_ID) 17 | const items = await b('shop_items') 18 | 19 | const recs = await items 20 | .select({ 21 | filterByFormula: `{identifier} = '${params.item}'`, 22 | maxRecords: 1, 23 | }) 24 | .firstPage() 25 | if (recs.length < 1) { 26 | return NextResponse.json({ error: 'what do you want?!' }, { status: 418 }) 27 | } 28 | const item = recs[0] 29 | 30 | const people = await b('people') 31 | const otp = Math.random().toString(16).slice(2) 32 | 33 | if ( 34 | !person.fields.verification_status || 35 | !['Eligible L1', 'Eligible L2'].includes( 36 | person.fields.verification_status[0], 37 | ) 38 | ) { 39 | await people.update(person.id, { 40 | shop_otp: otp, 41 | shop_otp_expires_at: new Date( 42 | new Date().getTime() + 60 * 60 * 1000, 43 | ).toISOString(), 44 | }) 45 | return redirect( 46 | `https://forms.hackclub.com/eligibility?slack_id=${session.slackId}&program=High Seas&continue=${encodeURIComponent(item.fields.fillout_base_url.replace('shop-order', 'hs-order') + otp)}`, 47 | ) 48 | } 49 | await people.update(person.id, { 50 | shop_otp: otp, 51 | shop_otp_expires_at: new Date( 52 | new Date().getTime() + 5 * 60 * 1000, 53 | ).toISOString(), 54 | }) 55 | return redirect( 56 | `${item.fields.fillout_base_url.replace('shop-order', 'hs-order')}${otp}`, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/app/api/cron/create-background-job.ts: -------------------------------------------------------------------------------- 1 | import { sql } from '@vercel/postgres' 2 | 3 | export default async function createBackgroundJob( 4 | type: 'run_lottery' | 'create_person' | 'invite' | 'submit_vote', 5 | args: {}, 6 | status: 'pending' | 'completed' | 'failed' = 'pending', 7 | ) { 8 | console.log(JSON.stringify(args)) 9 | return await sql`INSERT INTO background_job (type, args, status) VALUES (${type}, ${JSON.stringify(args)}, ${status})` 10 | } 11 | 12 | export const fetchCache = 'force-no-store' 13 | -------------------------------------------------------------------------------- /src/app/api/cron/every-day/route.ts: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic' 2 | export const fetchCache = 'force-no-store' 3 | 4 | import 'server-only' 5 | import Airtable from 'airtable' 6 | 7 | async function triggerShirtJob() { 8 | Airtable.configure({ 9 | apiKey: process.env.AIRTABLE_API_KEY, 10 | endpointUrl: process.env.AIRTABLE_ENDPOINT_URL, 11 | }) 12 | 13 | const base = Airtable.base(process.env.BASE_ID) 14 | console.log('Getting people to regenerate') 15 | const peopleToRegenerate = await base('people') 16 | .select({ 17 | filterByFormula: ` 18 | AND( 19 | NOT({action_generate_shirt_design} = TRUE()), 20 | NOT({ysws_submission} = BLANK()) 21 | )`, 22 | fields: [], 23 | }) 24 | .all() 25 | 26 | const peopleIds = peopleToRegenerate.map((person) => person.id) 27 | console.log('People to regenerate:', peopleIds.length) 28 | const chunkSize = 10 29 | for (let i = 0; i < peopleIds.length; i += chunkSize) { 30 | console.log(`Processing chunk ${i} to ${i + chunkSize}`) 31 | const chunk = peopleIds.slice(i, i + chunkSize) 32 | await base('people').update( 33 | chunk.map((id) => ({ 34 | id, 35 | fields: { 36 | action_generate_shirt_design: true, 37 | }, 38 | })), 39 | ) 40 | } 41 | } 42 | 43 | async function processDailyJobs() { 44 | console.log('Processing daily jobs') 45 | // await triggerShirtJob() 46 | } 47 | 48 | export async function GET() { 49 | await processDailyJobs() 50 | return Response.json({ success: true }) 51 | } 52 | -------------------------------------------------------------------------------- /src/app/api/cron/every-minute/route.ts: -------------------------------------------------------------------------------- 1 | import { tryUpdateProjectCache } from '../../battles/matchups/get-cached-projects' 2 | import { processBackgroundJobs } from '../process-background-jobs' 3 | import yswsUpdates from '../ysws-updates' 4 | 5 | export const dynamic = 'force-dynamic' 6 | export const fetchCache = 'force-no-store' 7 | export const maxDuration = 59 8 | 9 | export async function GET() { 10 | await Promise.all([ 11 | processBackgroundJobs(), 12 | tryUpdateProjectCache(), 13 | yswsUpdates(), 14 | ]) 15 | 16 | return Response.json({ success: true }) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/api/ideagen/route.js: -------------------------------------------------------------------------------- 1 | // // Enable streaming responses 2 | // export const runtime = 'edge' 3 | 4 | // export async function POST(req) { 5 | // try { 6 | // const { prompt } = await req.json() 7 | 8 | // if (!prompt) { 9 | // return NextResponse.json({ error: 'Prompt is required' }, { status: 400 }) 10 | // } 11 | 12 | // const apiKey = process.env.OPENAI_API_KEY 13 | // if (!apiKey) { 14 | // throw new Error('Missing OpenAI API key in environment variables') 15 | // } 16 | 17 | // const response = await fetch('https://api.openai.com/v1/chat/completions', { 18 | // method: 'POST', 19 | // headers: { 20 | // 'Content-Type': 'application/json', 21 | // Authorization: `Bearer ${apiKey}`, 22 | // }, 23 | // body: JSON.stringify({ 24 | // model: 'gpt-4-turbo', 25 | // messages: [ 26 | // { 27 | // role: 'system', 28 | // content: 29 | // 'You are an embedded model that takes a project idea and outputs an action plan of how one could go about building it to a creative developer, preferably in about 4 bullet points. Do not preface your answer. There is no option to converse - your first answer will be displayed on the website. Do not wafffle. Keep it concise and technical.', 30 | // }, 31 | // { 32 | // role: 'user', 33 | // content: prompt, 34 | // }, 35 | // ], 36 | // stream: true, 37 | // }), 38 | // }) 39 | 40 | // if (!response.ok) { 41 | // throw new Error( 42 | // `OpenAI API error: ${response.status} ${response.statusText}`, 43 | // ) 44 | // } 45 | 46 | // // Create a TransformStream for handling the response 47 | // console.log({ stream }) 48 | 49 | // // Return the stream with the appropriate header 50 | // return new Response(stream, { 51 | // headers: { 52 | // 'Content-Type': 'text/event-stream', 53 | // 'Cache-Control': 'no-cache', 54 | // Connection: 'keep-alive', 55 | // }, 56 | // }) 57 | // } catch (error) { 58 | // console.error('Error:', error) 59 | // return NextResponse.json({ error: error.message }, { status: 500 }) 60 | // } 61 | // } 62 | 63 | import { streamText } from 'ai' 64 | import { openai } from '@ai-sdk/openai' 65 | 66 | // This method must be named GET 67 | export async function POST(request) { 68 | const response = await streamText({ 69 | model: openai('gpt-4o'), 70 | messages: [ 71 | { 72 | role: 'system', 73 | content: 74 | 'You are an embedded model that takes a project idea and outputs an action plan of how one could go about building it to a creative developer, preferably in about 4 bullet points. Your explaination should not include dumb stuff like "testing" but should describe the subsystems and how they will fit together. Do not preface your answer. There is no option to converse - your first answer will be displayed on the website. Do not wafffle. Use plaintext only. No markdown. Keep it extremely concise and technical. Remember to number the bullet points!', 75 | }, 76 | { 77 | role: 'user', 78 | content: (await request.json()).msg, 79 | }, 80 | ], 81 | stream: true, 82 | }) 83 | // Respond with the stream 84 | return response.toTextStreamResponse({ 85 | headers: { 86 | 'Content-Type': 'text/event-stream', 87 | }, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /src/app/api/marketing/route.ts: -------------------------------------------------------------------------------- 1 | // run every minute, trigger a job to invite new users 2 | 3 | import { processPendingInviteJobs } from '@/app/marketing/invite-job' 4 | import type { NextRequest } from 'next/server' 5 | 6 | async function processJob() { 7 | await Promise.all([ 8 | processPendingInviteJobs(), 9 | new Promise((resolve) => setTimeout(resolve, 1000 * 18)), 10 | ]) 11 | } 12 | 13 | export async function GET(request: NextRequest) { 14 | const authHeader = request.headers.get('authorization') 15 | if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { 16 | return new Response('Unauthorized', { 17 | status: 401, 18 | }) 19 | } 20 | 21 | await processJob() 22 | await processJob() 23 | await processJob() 24 | 25 | return Response.json({ success: true }) 26 | } 27 | 28 | export const maxDuration = 60 29 | export const fetchCache = 'force-no-store' 30 | -------------------------------------------------------------------------------- /src/app/api/referral/[autonum]/route.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getPersonByAuto } from '@/app/utils/server/airtable' 4 | import { redirect } from 'next/navigation' 5 | import { NextRequest } from 'next/server' 6 | 7 | export async function GET( 8 | _request: NextRequest, 9 | { params }: { params: { autonum: string } }, 10 | ) { 11 | const slackId = (await getPersonByAuto(params.autonum))?.slackId 12 | console.log({ autonum: params.autonum, slackId }) 13 | if (slackId) { 14 | redirect('/?ref=' + slackId) 15 | } else { 16 | redirect('/') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/api/slack_redirect/route.ts: -------------------------------------------------------------------------------- 1 | import { getRedirectUri, createSlackSession } from '@/app/utils/server/auth' 2 | import { redirect } from 'next/navigation' 3 | import { NextRequest } from 'next/server' 4 | 5 | const errRedir = (err: any) => redirect('/slack-error?err=' + err.toString()) 6 | 7 | export async function GET(request: NextRequest) { 8 | const code = request.nextUrl.searchParams.get('code') 9 | 10 | const redirectUri = await getRedirectUri() 11 | 12 | const exchangeUrl = `https://slack.com/api/openid.connect.token?code=${code}&client_id=${process.env.SLACK_CLIENT_ID}&client_secret=${process.env.SLACK_CLIENT_SECRET}&redirect_uri=${redirectUri}` 13 | 14 | console.log('exchanging by posting to', exchangeUrl) 15 | 16 | const res = await fetch(exchangeUrl, { method: 'POST' }) 17 | 18 | if (res.status !== 200) return errRedir('Bad Slack OpenId response status') 19 | 20 | let data 21 | 22 | try { 23 | data = await res.json() 24 | } catch (e) { 25 | console.error(e, await res.text()) 26 | 27 | throw e 28 | } 29 | 30 | if (!data || !data.ok) { 31 | console.error(data) 32 | 33 | return errRedir('Bad Slack OpenID response') 34 | } 35 | 36 | try { 37 | await createSlackSession(data.id_token) 38 | 39 | console.log('cretaed slack session!! :)))))') 40 | } catch (e) { 41 | return errRedir(e) 42 | } 43 | 44 | // const userInfoUrl = `https://slack.com/api/openid.connect.userInfo`; 45 | 46 | // const userInfo = await fetch(userInfoUrl, { 47 | 48 | // headers: { Authorization: `Bearer ${data.access_token}` }, 49 | 50 | // }).then((d) => d.json()); 51 | 52 | redirect('/signpost') 53 | } 54 | -------------------------------------------------------------------------------- /src/app/api/stats/route.tsx: -------------------------------------------------------------------------------- 1 | import Airtable from 'airtable' 2 | import { ImageResponse } from 'next/og' 3 | import { kv } from '@vercel/kv' 4 | 5 | const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( 6 | process.env.BASE_ID!, 7 | ) 8 | 9 | export async function GET({ url }) { 10 | return null 11 | let shipCount = await kv.get('ship-count') 12 | 13 | if (!shipCount) { 14 | console.log('Refetching ships') 15 | const allShips = await base('ships').select({}).all() 16 | shipCount = allShips.length.toString() 17 | await kv.set('ship-count', shipCount, { ex: 60_000, nx: true }) 18 | } 19 | 20 | const darkTheme = new URL(url).searchParams.get('theme') === 'dark' 21 | 22 | return new ImageResponse( 23 | ( 24 |
38 | 🚢 {shipCount} projects shipped (and counting!) 39 |
40 | ), 41 | { 42 | width: 1_200, 43 | height: 100, 44 | }, 45 | ) 46 | } 47 | 48 | /* 49 | 50 | import chromium from '@sparticuz/chromium' 51 | import puppeteer from 'puppeteer-core' 52 | 53 | let browser: puppeteer.Browser 54 | 55 | 56 | 57 | 58 | const html = (shipCount: number) => ` 59 | 60 | 61 | 62 | 67 | 68 | 69 |
70 |

${shipCount} projects shipped

71 |
72 | 73 | ` 74 | 75 | 76 | 77 | const CHROME_EXECUTABLE_PATH = 78 | '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary' 79 | 80 | const isLocal = false // Set this variable as required - @sparticuz/chromium does not work on ARM, so we use a standard Chrome executable locally - see issue https://github.com/Sparticuz/chromium/issues/186 81 | if (!browser?.isConnected()) { 82 | chromium.setHeadlessMode = true 83 | chromium.setGraphicsMode = false 84 | 85 | browser = await puppeteer.launch({ 86 | args: chromium.args, 87 | defaultViewport: chromium.defaultViewport, 88 | executablePath: await chromium.executablePath(), 89 | headless: chromium.headless, 90 | ignoreHTTPSErrors: true, 91 | }) 92 | } 93 | 94 | try { 95 | const page = await browser.newPage() 96 | 97 | await page.setViewport({ 98 | width: 800, 99 | height: 100, 100 | deviceScaleFactor: 3, 101 | }) 102 | 103 | await page.setContent(html(123)) 104 | 105 | const screenshot = await page.screenshot({ 106 | type: 'png', 107 | clip: { 108 | x: 0, 109 | y: 0, 110 | width: 800, 111 | height: 100, 112 | }, 113 | }) 114 | 115 | await page.close() 116 | await browser.close() 117 | 118 | // Return the screenshot as a PNG 119 | return new Response(screenshot, { 120 | headers: { 121 | 'Content-Type': 'image/png', 122 | // "Cache-Control": "public, max-age=3600" 123 | }, 124 | }) 125 | } catch (error) { 126 | await browser.close() 127 | return new Response(JSON.stringify({ error: error.message }), { 128 | status: 500, 129 | headers: { 130 | 'Content-Type': 'application/json', 131 | }, 132 | }) 133 | } 134 | } 135 | */ 136 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as Sentry from '@sentry/nextjs' 4 | import NextError from 'next/error' 5 | import { useEffect } from 'react' 6 | 7 | export default function GlobalError({ 8 | error, 9 | }: { 10 | error: Error & { digest?: string } 11 | }) { 12 | useEffect(() => { 13 | Sentry.captureException(error) 14 | }, [error]) 15 | 16 | return ( 17 | 18 | 19 | {/* `NextError` is the default Next.js error page component. Its type 20 | definition requires a `statusCode` prop. However, since the App Router 21 | does not expose status codes for errors, we simply pass 0 to render a 22 | generic error message. */} 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | /* font-family: Arial, Helvetica, sans-serif; */ 7 | font-family: var(--font-main), serif; 8 | } 9 | 10 | html, 11 | body, 12 | main { 13 | min-height: 100%; 14 | } 15 | 16 | @layer utilities { 17 | .text-balance { 18 | text-wrap: balance; 19 | } 20 | } 21 | 22 | .font-main { 23 | font-family: var(--font-main); 24 | } 25 | 26 | input, 27 | textarea { 28 | color: black; 29 | } 30 | 31 | @layer base { 32 | :root { 33 | --background: 0 0% 100%; 34 | --foreground: 0 0% 3.9%; 35 | --card: 0 0% 100%; 36 | --card-foreground: 0 0% 3.9%; 37 | --popover: 0 0% 100%; 38 | --popover-foreground: 0 0% 3.9%; 39 | --primary: 0 0% 9%; 40 | --primary-foreground: 0 0% 98%; 41 | --secondary: 0 0% 96.1%; 42 | --secondary-foreground: 0 0% 9%; 43 | --muted: 0 0% 96.1%; 44 | --muted-foreground: 0 0% 45.1%; 45 | --accent: 0 0% 96.1%; 46 | --accent-foreground: 0 0% 9%; 47 | --destructive: 0 84.2% 60.2%; 48 | --destructive-foreground: 0 0% 98%; 49 | --border: 0 0% 89.8%; 50 | --input: 0 0% 89.8%; 51 | --ring: 0 0% 3.9%; 52 | --chart-1: 12 76% 61%; 53 | --chart-2: 173 58% 39%; 54 | --chart-3: 197 37% 24%; 55 | --chart-4: 43 74% 66%; 56 | --chart-5: 27 87% 67%; 57 | --radius: 0.5rem; 58 | } 59 | .dark { 60 | --background: 0 0% 3.9%; 61 | --foreground: 0 0% 98%; 62 | --card: 0 0% 3.9%; 63 | --card-foreground: 0 0% 98%; 64 | --popover: 0 0% 3.9%; 65 | --popover-foreground: 0 0% 98%; 66 | --primary: 0 0% 98%; 67 | --primary-foreground: 0 0% 9%; 68 | --secondary: 0 0% 14.9%; 69 | --secondary-foreground: 0 0% 98%; 70 | --muted: 0 0% 14.9%; 71 | --muted-foreground: 0 0% 63.9%; 72 | --accent: 0 0% 14.9%; 73 | --accent-foreground: 0 0% 98%; 74 | --destructive: 0 62.8% 30.6%; 75 | --destructive-foreground: 0 0% 98%; 76 | --border: 0 0% 14.9%; 77 | --input: 0 0% 14.9%; 78 | --ring: 0 0% 83.1%; 79 | --chart-1: 220 70% 50%; 80 | --chart-2: 160 60% 45%; 81 | --chart-3: 30 80% 55%; 82 | --chart-4: 280 65% 60%; 83 | --chart-5: 340 75% 55%; 84 | } 85 | } 86 | 87 | @layer base { 88 | * { 89 | @apply border-border; 90 | } 91 | body { 92 | @apply bg-background text-foreground; 93 | } 94 | } 95 | 96 | :marker { 97 | content: none; 98 | } 99 | 100 | @font-face { 101 | font-family: '0Enchanted_Land'; 102 | src: url('https://cloud-dv1senge8-hack-club-bot.vercel.app/0enchanted_land.otf') 103 | format('truetype'); 104 | } 105 | 106 | .enchanted { 107 | font-family: '0Enchanted_Land', cursive; 108 | } 109 | 110 | @font-face { 111 | font-family: 'Minecraft'; 112 | src: url('/fonts/minecraft_font.ttf') format('truetype'); 113 | } 114 | 115 | .minecraft { 116 | font-family: 'Minecraft', monospace; 117 | } 118 | -------------------------------------------------------------------------------- /src/app/harbor/battles/blessed.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import Pill from '@/components/ui/pill' 6 | import { 7 | Popover, 8 | PopoverTrigger, 9 | PopoverContent, 10 | } from '@/components/ui/popover' 11 | import Icon from '@hackclub/icons' 12 | 13 | export default function Blessed() { 14 | const [open, setOpen] = useState(false) 15 | 16 | return ( 17 | 18 | setOpen(true)} 21 | onMouseLeave={() => setOpen(false)} 22 | > 23 |
24 | 25 |
26 |
27 | 28 |
29 |

What's a blessing?

30 |
    31 |
  • 32 | {' '} 37 | Be thoughtful with your voting. 38 |
  • 39 |
  • 40 | {' '} 45 | Write good descriptions. 46 |
  • 47 |
  • 48 | {' '} 49 | Keep voting regularly. 50 |
  • 51 |
  • 52 | doubloons{' '} 58 | While you keep your voting streak, you'll earn 20% more doubloons! 59 |
  • 60 |
61 |
62 |
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/app/harbor/battles/cursed.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import Pill from '@/components/ui/pill' 6 | import { 7 | Popover, 8 | PopoverTrigger, 9 | PopoverContent, 10 | } from '@/components/ui/popover' 11 | import Icon from '@hackclub/icons' 12 | 13 | export default function Cursed() { 14 | const [open, setOpen] = useState(false) 15 | 16 | return ( 17 | 18 | setOpen(true)} 21 | onMouseLeave={() => setOpen(false)} 22 | > 23 |
24 | 25 |
26 |
27 | 28 |
29 |

Your votes have been flagged

30 |
    31 |
  • 32 | {' '} 37 | Be more thoughtful with your voting. 38 |
  • 39 |
  • 40 | {' '} 41 | Write better descriptions for your choices. 42 |
  • 43 |
  • 44 | doubloons{' '} 50 | Until you lift the curse, your payouts are halved. 51 |
  • 52 |
53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/app/harbor/battles/fraud-utils.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getSession } from '@/app/utils/auth' 4 | import { Ship } from '@/app/utils/server/data' 5 | 6 | export async function sendFraudReport( 7 | project: Ship, 8 | type: string, 9 | reason: string, 10 | ) { 11 | const session = await getSession() 12 | 13 | const res = await fetch( 14 | 'https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/flagged_projects', 15 | { 16 | method: 'POST', 17 | headers: { 18 | Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`, 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | records: [ 23 | { 24 | fields: { 25 | project: [project.id], 26 | reporter: [session?.personId], 27 | reason: type, 28 | details: reason, 29 | }, 30 | }, 31 | ], 32 | }), 33 | }, 34 | ) 35 | .then((data) => data.json()) 36 | .then(console.log) 37 | 38 | console.log(project, type, reason) 39 | } 40 | -------------------------------------------------------------------------------- /src/app/harbor/battles/mdc.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { Components } from 'react-markdown' 3 | 4 | export const markdownComponents: Components = { 5 | h1: ({ ...props }) => ( 6 |

10 | ), 11 | h2: ({ ...props }) => ( 12 |

16 | ), 17 | h3: ({ ...props }) => ( 18 |

22 | ), 23 | p: ({ ...props }) =>

, 24 | a: ({ ...props }) => ( 25 | 29 | ), 30 | ul: ({ ...props }) =>