├── .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 |
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 |
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 | {' '}
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 |
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 | {' '}
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 }) => ,
31 | ol: ({ ...props }) => ,
32 | li: ({ ...props }) => ,
33 | img: ({ src, alt, ...props }) => (
34 |
35 |
41 |
42 | ),
43 | code: ({ className, children, ...props }) => {
44 | const match = /language-(\w+)/.exec(className || '')
45 | return match ? (
46 |
47 |
48 | {children}
49 |
50 |
51 | ) : (
52 |
56 | {children}
57 |
58 | )
59 | },
60 | blockquote: ({ ...props }) => (
61 |
65 | ),
66 | table: ({ ...props }) => (
67 |
73 | ),
74 | th: ({ ...props }) => (
75 |
79 | ),
80 | td: ({ ...props }) => (
81 |
85 | ),
86 | }
87 |
--------------------------------------------------------------------------------
/src/app/harbor/events/events.tsx:
--------------------------------------------------------------------------------
1 | export default function Events() {
2 | return Events
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/harbor/gallery/gallery.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState, useCallback } from 'react'
4 | import InfiniteScroll from 'react-infinite-scroll-component'
5 | import { getShips } from './utils'
6 | import { Ship } from '../shipyard/ship-utils'
7 | import Ships from '../shipyard/ships'
8 |
9 | export type ShipsObject = Record
10 |
11 | export default function Gallery({ ships, setShips }: any) {
12 | const [nextOffset, setNextOffset] = useState(undefined)
13 | const [hasMore, setHasMore] = useState(true)
14 | const [isLoading, setIsLoading] = useState(false)
15 |
16 | const fetchShips = useCallback(async () => {
17 | if (isLoading || !hasMore) return
18 | setIsLoading(true)
19 |
20 | try {
21 | const newShipInfo = await getShips(nextOffset)
22 | setNextOffset(newShipInfo.offset)
23 | setHasMore(!!newShipInfo.offset)
24 |
25 | setShips((prev: any) => {
26 | const newShips = newShipInfo.ships.reduce(
27 | (acc: ShipsObject, ship: Ship) => {
28 | acc[ship.id] = ship
29 | return acc
30 | },
31 | {},
32 | )
33 | return { ...prev, ...newShips }
34 | })
35 | } catch (error) {
36 | console.error('Error fetching ships:', error)
37 | setHasMore(false)
38 | setIsLoading(false)
39 | } finally {
40 | setIsLoading(false)
41 | }
42 | }, [isLoading, hasMore, nextOffset])
43 |
44 | useEffect(() => {
45 | if (Object.keys(ships).length === 0) {
46 | fetchShips()
47 | }
48 | }, [])
49 |
50 | if (!ships) return Loading all ships...
51 |
52 | const shipsArray: Ship[] = Object.values(ships)
53 |
54 | if (shipsArray.length === 0)
55 | return Loading all ships...
56 |
57 | return (
58 |
64 | Loading
65 |
66 | }
67 | endMessage={
68 |
69 | Yay! You have seen all {shipsArray.length} ships.
70 |
71 | }
72 | scrollableTarget="harbor-tab-scroll-element"
73 | >
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/harbor/gallery/utils.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { Ship } from '../shipyard/ship-utils'
4 |
5 | interface AirtableShipRow {
6 | id: string
7 | fields: {
8 | title: string
9 | readme_url: string
10 | repo_url: string
11 | screenshot_url: string
12 | hours: number
13 | rating: number
14 | }
15 | }
16 |
17 | export async function getShips(offset: string | undefined): Promise<{
18 | ships: Ship[]
19 | offset: string | undefined
20 | }> {
21 | const res = await fetch(
22 | `https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/ships?view=Grid%20view${offset ? `&offset=${offset}` : ''}`,
23 | {
24 | headers: {
25 | Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
26 | 'User-Agent': 'highseas.hackclub.com (getShips)',
27 | },
28 | },
29 | )
30 | let data
31 | try {
32 | data = await res.json()
33 | } catch (e) {
34 | console.error(e, await res.text())
35 | throw e
36 | }
37 |
38 | // TODO: Error checking
39 | const ships = data.records.map((r: AirtableShipRow) => {
40 | return {
41 | id: r.id,
42 | title: r.fields.title,
43 | readmeUrl: r.fields.readme_url,
44 | hours: r.fields.hours,
45 | repoUrl: r.fields.repo_url,
46 | screenshotUrl: r.fields.screenshot_url,
47 | rating: r.fields.rating,
48 | }
49 | })
50 |
51 | return {
52 | ships,
53 | offset: data.offset,
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/harbor/shipyard/shipyard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Ships from './ships'
4 | import useLocalStorageState from '../../../../lib/useLocalStorageState'
5 | import { useEffect } from 'react'
6 | import { getVotesRemainingForNextPendingShip } from '@/app/utils/airtable'
7 | import Pill from '@/components/ui/pill'
8 | import { fetchShips, Ship } from '@/app/utils/data'
9 | import { IdeaGenerator } from './idea-generator/impl'
10 |
11 | const tutorialShips: Ship[] = [
12 | {
13 | id: 'hack-club-site',
14 | title: 'Hack Club Site',
15 | repoUrl: 'https://github.com/hackclub/site',
16 | deploymentUrl: 'https://hackclub.com',
17 | screenshotUrl:
18 | 'https://cloud-lezyvcdxr-hack-club-bot.vercel.app/0image.png',
19 | readmeUrl:
20 | 'https://raw.githubusercontent.com/hackclub/site/refs/heads/main/README.md',
21 | credited_hours: 123,
22 | voteRequirementMet: false,
23 | doubloonPayout: 421,
24 | shipType: 'project',
25 | shipStatus: 'staged',
26 | wakatimeProjectNames: ['hack-club-site'],
27 | matchups_count: 0,
28 | hours: null,
29 | total_hours: null,
30 | createdTime: '',
31 | updateDescription: null,
32 | reshippedFromId: null,
33 | reshippedToId: null,
34 | },
35 | ]
36 |
37 | export default function Shipyard({ session }: any) {
38 | const [ships, setShips] = useLocalStorageState('cache.ships', [])
39 | const [voteBalance, setVoteBalance] = useLocalStorageState(
40 | 'cache.voteBalance',
41 | 0,
42 | )
43 |
44 | useEffect(() => {
45 | fetchShips(session.slackId).then((ships) => setShips(ships))
46 |
47 | getVotesRemainingForNextPendingShip(session.slackId).then((balance) =>
48 | setVoteBalance(balance),
49 | )
50 | }, [])
51 |
52 | const isTutorial = sessionStorage.getItem('tutorial') === 'true'
53 |
54 | if (!ships) return
55 |
56 | return (
57 | <>
58 |
59 |
60 |
61 | The Shipyard
62 |
66 | Manage yer ships!
67 |
68 |
69 |
70 | {voteBalance > 0 && (
71 |
79 | )}
80 |
81 | {/* */}
82 |
87 |
88 |
89 | >
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/src/app/harbor/shipyard/test.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const FullScreenOverlay = () => {
4 | const [isOpen, setIsOpen] = useState(false)
5 |
6 | const toggleOverlay = () => {
7 | setIsOpen(!isOpen)
8 | // Toggle scrolling on body
9 | document.body.style.overflow = isOpen ? 'auto' : 'hidden'
10 | }
11 |
12 | return (
13 |
14 |
18 | Toggle Overlay
19 |
20 |
21 | {isOpen && (
22 |
23 |
24 |
Overlay Content
25 |
26 | This is a full-screen overlay that prevents scrolling of the main
27 | content.
28 |
29 |
33 | Close Overlay
34 |
35 |
36 |
37 | )}
38 |
39 | {/* Example content to demonstrate scrolling being blocked */}
40 |
41 |
Main Content
42 | {[...Array(20)].map((_, i) => (
43 |
44 | This is paragraph {i + 1} to demonstrate scrolling content.
45 |
46 | ))}
47 |
48 |
49 | )
50 | }
51 |
52 | export default FullScreenOverlay
53 |
--------------------------------------------------------------------------------
/src/app/harbor/shop/progress.tsx:
--------------------------------------------------------------------------------
1 | // harbor/shop/progress.tsx
2 |
3 | import React, { useEffect, useState } from 'react'
4 | import Cookies from 'js-cookie'
5 |
6 | export default function Progress({ val, items }) {
7 | const currentTix = Number(Cookies.get('tickets') ?? 0)
8 | const favItems = items.filter((item) => val.includes(item.id))
9 | console.log(favItems)
10 | const max = favItems.sort(
11 | (a: ShopItem, b: ShopItem) => b.priceGlobal - a.priceGlobal,
12 | )[0]
13 |
14 | useEffect(() => {
15 | localStorage.setItem('favouriteItems', JSON.stringify(val))
16 | }, [val])
17 |
18 | if (!max) {
19 | return null
20 | }
21 |
22 | return (
23 | <>
24 |
64 | >
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/best-ships.tsx:
--------------------------------------------------------------------------------
1 | import { getBestShips, Ship } from '@/app/utils/data'
2 | import JaggedCard from '@/components/jagged-card'
3 | import Pill from '@/components/ui/pill'
4 | import { useEffect, useState } from 'react'
5 | import DoubloonsImage from '/public/doubloon.svg'
6 | import Image from 'next/image'
7 |
8 | export default function BestShips() {
9 | const [bestShips, setBestShips] = useState()
10 |
11 | useEffect(() => {
12 | getBestShips().then(setBestShips)
13 | }, [])
14 |
15 | return (
16 |
17 |
18 | Best ships this week
19 |
20 |
21 | {bestShips ? (
22 |
23 | {bestShips.map((partialShip: any, idx: number) => {
24 | return (
25 |
30 | {partialShip.title}
31 |
36 | }
37 | />
38 |
64 | {partialShip.entrantSlackId ? (
65 |
70 |
76 |
77 | ) : null}
78 |
87 |
88 | )
89 | })}
90 |
91 | ) : (
92 |
Loading...
93 | )}
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/countdown.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import JaggedCardSmall from '@/components/jagged-card-small'
3 | import { Button } from '@/components/ui/button'
4 | import Link from 'next/link'
5 |
6 | const dateEnd = new Date('2025-02-03T05:00:00Z').getTime()
7 | const formatTime = (distance: number) => {
8 | const hours = Math.floor(distance / (1000 * 60 * 60))
9 | const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
10 | const seconds = Math.floor((distance % (1000 * 60)) / 1000)
11 |
12 | if (hours > 0) {
13 | return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
14 | } else if (minutes > 0) {
15 | return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
16 | } else {
17 | return `${String(seconds).padStart(2, '0')}`
18 | }
19 | }
20 |
21 | export default function Countdown() {
22 | const [timeLeft, setTimeLeft] = useState('00:00:00')
23 |
24 | useEffect(() => {
25 | const interval = setInterval(() => {
26 | const now = new Date().getTime()
27 | const distance = dateEnd - now
28 |
29 | if (distance < 0) {
30 | clearInterval(interval)
31 | setTimeLeft('00:00:00')
32 | return
33 | }
34 |
35 | setTimeLeft(formatTime(distance))
36 | }, 1000)
37 |
38 | return () => clearInterval(interval)
39 | }, [])
40 |
41 | return dateEnd - new Date().getTime() > 100 * 60 * 60 * 1000 ? (
42 | <>>
43 | ) : (
44 |
45 |
46 |
47 | {dateEnd - new Date().getTime() > 0 ? (
48 | <>Time Remaining>
49 | ) : (
50 | <>Ended.>
51 | )}
52 |
53 |
56 | {dateEnd - new Date().getTime() > 0 ? (
57 | <>{timeLeft}>
58 | ) : (
59 | <>High Seas is over!>
60 | )}
61 |
62 |
63 |
64 | {dateEnd - new Date().getTime() > 0 ? (
65 | <>
66 | Arrrrr, you'd better{' '}
67 |
68 |
72 | ship all your ships
73 |
74 | {' '}
75 | before the time runs out!
76 | >
77 | ) : (
78 | <>Arrr, thank you all for competing, you were worthy pirates!>
79 | )}
80 |
81 |
82 |
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/feed-items.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { SignpostFeedItem } from '@/app/utils/server/data'
4 | import JaggedCardSmall from '@/components/jagged-card-small'
5 | import Cookies from 'js-cookie'
6 | import Markdown from 'react-markdown'
7 | import TimeAgo from 'javascript-time-ago'
8 | import en from 'javascript-time-ago/locale/en'
9 |
10 | TimeAgo.addDefaultLocale(en)
11 | const timeAgo = new TimeAgo('en-US')
12 |
13 | export default function FeedItems() {
14 | if (process.env.NEXT_PUBLIC_LOW_RATE_LIMIT) return null
15 |
16 | const cookie = Cookies.get('signpost-feed')
17 | if (!cookie) return null
18 |
19 | let feedItems: SignpostFeedItem[]
20 | try {
21 | feedItems = JSON.parse(cookie).sort((a, b) => a?.autonumber < b?.autonumber)
22 | } catch (e) {
23 | console.error("Could't parse signpost feed cookie into JSON:", e)
24 | return null
25 | }
26 |
27 | if (!feedItems || feedItems.length === 0) {
28 | return No changelog posts yet! Check back soon.
29 | }
30 |
31 | return (
32 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/help.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | import Airtable from 'airtable'
3 |
4 | const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(
5 | process.env.BASE_ID!,
6 | )
7 |
8 | //TODO: Delete this file it's weird
9 | import { getWakaSessions } from '@/app/utils/waka'
10 |
11 | export async function wakaSessions() {
12 | return await getWakaSessions()
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/leaderboard.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import Icon from '@hackclub/icons'
3 | import { useEffect, useState } from 'react'
4 | import {
5 | getLeaderboardParticipating,
6 | reportLeaderboardParticipating,
7 | } from '@/app/utils/airtable'
8 |
9 | export default function LeaderboardOptIn() {
10 | const [optedIn, setOptedIn] = useState(false)
11 | const [inProgress, setInProgress] = useState(false)
12 |
13 | useEffect(() => {
14 | async function fetchParticipating() {
15 | setInProgress(true)
16 | const participating = await getLeaderboardParticipating()
17 | setOptedIn(participating ?? false)
18 | setInProgress(false)
19 | }
20 | fetchParticipating()
21 | }, [])
22 |
23 | return (
24 |
25 |
26 | Want to be on the leaderboard?
27 |
28 |
29 |
30 | If you would like to be on the leaderboard then please click the button
31 | below to opt in!
32 |
33 |
34 |
35 | By opting in you agree to have your name, slackid, and doubloon count
36 | displayed on the leaderboard. You can view the leaderboard at any time
37 | by going to{' '}
38 |
44 | this link
45 |
46 | . You can opt out at any time by clicking the button again.
47 |
48 |
49 |
{
53 | setInProgress(true)
54 | await reportLeaderboardParticipating(!optedIn)
55 | setOptedIn(!optedIn)
56 | setInProgress(false)
57 | }}
58 | >
59 | {inProgress ? (
60 | optedIn ? (
61 | <>
62 |
63 | Destroying Records
64 | >
65 | ) : (
66 | <>
67 |
68 | Scribbling
69 | >
70 | )
71 | ) : optedIn ? (
72 | 'Remove me plz'
73 | ) : (
74 | "I'm In!"
75 | )}
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/referral.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect } from 'react'
4 |
5 | import { getSession } from '@/app/utils/auth'
6 | import Pill from '@/components/ui/pill'
7 | import {
8 | Popover,
9 | PopoverTrigger,
10 | PopoverContent,
11 | } from '@/components/ui/popover'
12 | import Icon from '@hackclub/icons'
13 | import { safePerson } from '@/app/utils/airtable'
14 |
15 | export default function Referral() {
16 | const [shareLink, setShareLink] = useState(null)
17 | const [open, setOpen] = useState(false)
18 |
19 | useEffect(() => {
20 | safePerson().then((sp) => {
21 | if (sp?.referralLink) {
22 | setShareLink(sp.referralLink)
23 | }
24 | })
25 | }, [])
26 |
27 | const handleClick = (e) => {
28 | e.preventDefault()
29 | if (navigator.share) {
30 | navigator
31 | .share({
32 | url: shareLink || '',
33 | })
34 | .then(() => console.log('Successful share'))
35 | .catch((error) => console.log('Error sharing', error))
36 | } else {
37 | // copy to clipboard
38 | navigator.clipboard.writeText(shareLink || '')
39 | alert('Copied to clipboard!')
40 | }
41 | }
42 |
43 | if (!shareLink) return null
44 |
45 | return (
46 |
47 |
48 | setOpen(true)}
51 | onMouseLeave={() => setOpen(false)}
52 | >
53 |
54 |
55 |
56 |
57 |
58 |
59 |
Your referral link!
60 |
61 |
62 | {' '}
67 | Get your friends to sign up at this link!
68 |
69 |
70 | {' '}
76 | Once they ship you'll earn 4 doubloons!
77 |
78 |
79 | 🦈
80 | You'll also be entered into a raffle to win a smolhaj!
81 |
82 |
83 |
84 |
85 |
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/verification.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import JaggedCard from '@/components/jagged-card'
4 | import JaggedCardSmall from '@/components/jagged-card-small'
5 | import Cookies from 'js-cookie'
6 | import Link from 'next/link'
7 |
8 | const getVerificationMessage = (status: string, reason: string | undefined) => {
9 | switch (status) {
10 | case 'Unknown':
11 | return {
12 | color: 'yellow',
13 | message:
14 | "Hang tight, we're still reviewing your verification documents! Don't worry, it usually takes less than day. In the meantime, get hacking! Your hours still count as long as you've installed Hackatime.",
15 | }
16 | case 'Insufficient':
17 | return {
18 | color: '#FFA500',
19 | message: (
20 | <>
21 | Blimey! We weren't able to verify you with the proof you submitted
22 | {reason ? (
23 | <>
24 | . According to the reviewer, {reason}
25 | >
26 | ) : null}
27 |
28 |
29 | Don't feed the fish though!{' '}
30 |
31 |
35 | Try again here
36 |
37 |
38 | . Email{' '}
39 |
40 | verifications@hackclub.com
41 | {' '}
42 | if you have any questions.
43 | >
44 | ),
45 | }
46 | case 'Ineligible':
47 | return {
48 | color: '#ff0000',
49 | message: (
50 | <>
51 | Uh-oh, seems like you're an adult… unfortunately, High Seas is only
52 | for teenagers 18 and under. Email{' '}
53 |
54 | verifications@hackclub.com
55 | {' '}
56 | if you think this is a misunderstanding.
57 | >
58 | ),
59 | }
60 | default:
61 | return {
62 | color: '#FFA500',
63 | message:
64 | "Oh no, you haven't filled out a verification form yet! But… how did you even get to this page then?? That's not supposed to be possible… please make a post to #high-seas-help 🤔",
65 | redirect: true,
66 | }
67 | }
68 | }
69 |
70 | export default function Verification() {
71 | const verificationCookie = Cookies.get('verification')
72 | if (!verificationCookie) return null
73 |
74 | let status: string, reason: string | undefined
75 | try {
76 | const parsed = JSON.parse(verificationCookie)
77 |
78 | status = parsed.status
79 | reason = parsed.reason
80 | } catch (e) {
81 | console.error("Could't parse verification feed cookie into JSON:", e)
82 | return null
83 | }
84 |
85 | if (!status || status.startsWith('Eligible')) return null
86 |
87 | const { message, redirect } = getVerificationMessage(status, reason)
88 |
89 | return (
90 |
91 | {message}
92 | {redirect ? (
93 |
97 | Verify Yourself!
98 |
99 | ) : null}
100 |
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/harbor/signpost/wakatime-config-tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect } from 'react'
4 | import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
5 | import { Card } from '@/components/ui/card'
6 | import { Button } from '@/components/ui/button'
7 | import Icon from '@hackclub/icons'
8 | import { useToast } from '@/hooks/use-toast'
9 |
10 | const CopyButton = ({ textToCopy }: { textToCopy: string }) => {
11 | const { toast } = useToast()
12 |
13 | const [isCopied, setIsCopied] = useState(false)
14 |
15 | useEffect(() => {
16 | if (isCopied) {
17 | const timer = setTimeout(() => setIsCopied(false), 1000)
18 | return () => clearTimeout(timer)
19 | }
20 | }, [isCopied])
21 |
22 | const handleCopy = async () => {
23 | // await navigator.clipboard.writeText(textToCopy); // TODO: UNCOMMENT & fix navigator issue
24 | setIsCopied(true)
25 | toast({ title: 'Copied WakaTime setup script' })
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | const WakaTimeConfigTabs = ({ wakaToken }: { wakaToken: string }) => {
36 | const unixCommand = `echo -e "[settings]\napi_url = https://waka.hackclub.com/api\napi_key = ${wakaToken}" > ~/.wakatime.cfg && echo "WakaTime configuration file has been created/updated at ~/.wakatime.cfg"`
37 | const windowsCommand = `echo "[settings]\`napi_url = https://waka.hackclub.com/api\`napi_key = ${wakaToken}" | Out-File -FilePath "$env:USERPROFILE\\.wakatime.cfg" -Encoding ASCII; Write-Host "WakaTime configuration file has been created/updated at $env:USERPROFILE\\.wakatime.cfg"`
38 |
39 | return (
40 |
41 |
42 | MacOS & Linux
43 | Windows
44 |
45 |
46 |
47 |
48 | {unixCommand}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {windowsCommand}
57 |
58 |
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export default WakaTimeConfigTabs
66 |
--------------------------------------------------------------------------------
/src/app/harbor/tavern/tavern-utils.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getSession } from '@/app/utils/auth'
4 | import {
5 | setTavernRsvpStatus,
6 | submitMyTavernLocation,
7 | submitShirtSize,
8 | } from '@/app/utils/tavern'
9 | import Airtable from 'airtable'
10 |
11 | Airtable.configure({
12 | apiKey: process.env.AIRTABLE_API_KEY,
13 | endpointUrl: process.env.AIRTABLE_ENDPOINT_URL,
14 | })
15 |
16 | export type RsvpStatus = 'none' | 'organizer' | 'participant'
17 | export type TavernPersonItem = {
18 | status: RsvpStatus
19 | coordinates: string
20 | }
21 | export type TavernEventItem = {
22 | id: string
23 | city: string
24 | geocode: string
25 | locality: string
26 | attendeeCount: number
27 | organizers: string[]
28 | channel: string
29 | eventDate?: string
30 | }
31 |
32 | let cachedPeople: TavernPersonItem[] | null,
33 | cachedEvents: TavernEventItem[] | null
34 | let lastPeopleFetch = 0,
35 | lastEventsFetch = 0
36 | const TTL = 30 * 60 * 1000
37 |
38 | export const getTavernPeople = async () => {
39 | const session = await getSession()
40 | if (!session) {
41 | const error = new Error('Tried to get tavern people without a session')
42 | console.log(error)
43 | throw error
44 | }
45 |
46 | if (Date.now() - lastPeopleFetch < TTL) return cachedPeople
47 |
48 | const base = Airtable.base(process.env.BASE_ID!)
49 | const records = await base('people')
50 | .select({
51 | fields: ['tavern_rsvp_status', 'tavern_map_coordinates'],
52 | filterByFormula:
53 | 'AND({tavern_map_coordinates} != "", OR(tavern_rsvp_status != "", shipped_ship_count >= 1))',
54 | })
55 | .all()
56 |
57 | const items = records.map((r) => ({
58 | status: r.get('tavern_rsvp_status'),
59 | coordinates: r.get('tavern_map_coordinates'),
60 | })) as TavernPersonItem[]
61 |
62 | cachedPeople = items
63 | lastPeopleFetch = Date.now()
64 |
65 | return items
66 | }
67 |
68 | export const getTavernEvents = async () => {
69 | const session = await getSession()
70 | if (!session) {
71 | const error = new Error('Tried to get tavern locations without a session')
72 | console.log(error)
73 | throw error
74 | }
75 | if (Date.now() - lastEventsFetch < TTL) return cachedEvents
76 |
77 | console.log('Fetching tavern events')
78 | const base = Airtable.base(process.env.BASE_ID!)
79 | const records = await base('taverns')
80 | .select({
81 | fields: [
82 | 'city',
83 | 'map_geocode',
84 | 'organizers',
85 | 'locality',
86 | 'attendees_count',
87 | 'channel',
88 | 'event_date',
89 | 'hide',
90 | ],
91 | filterByFormula: '{hide} = FALSE()',
92 | })
93 | .all()
94 |
95 | const items = records.map((r) => ({
96 | id: r.id,
97 | city: r.get('city'),
98 | geocode: r.get('map_geocode'),
99 | locality: r.get('locality'),
100 | organizers: r.get('organizers') ?? [],
101 | attendeeCount: r.get('attendees_count'),
102 | channel: r.get('channel'),
103 | eventDate: r.get('event_date'),
104 | })) as TavernEventItem[]
105 |
106 | cachedEvents = items
107 | lastEventsFetch = Date.now()
108 | return items
109 | }
110 |
111 | export async function rspvForTavern(formData: FormData) {
112 | console.log('Saving rspv for tavern seliction...')
113 | let res = { success: true, error: null }
114 |
115 | await Promise.all([
116 | setTavernRsvpStatus(formData.get('rsvp') as RsvpStatus),
117 | submitMyTavernLocation(formData.get('tavern') as string),
118 | submitShirtSize(formData.get('shirt') as string),
119 | ]).catch((error) => {
120 | console.error('Error submitting tavern RSVP', error)
121 | res = { success: false, error: error.toString() }
122 | })
123 |
124 | console.log('Successfully saved tavern RSVP')
125 | return JSON.stringify(res)
126 | }
127 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import { SpeedInsights } from '@vercel/speed-insights/next'
3 | import localFont from 'next/font/local'
4 | import './globals.css'
5 | import Nav from '@/components/nav'
6 | import { Toaster } from '@/components/ui/toaster'
7 |
8 | import PlausibleProvider from 'next-plausible'
9 | import { Analytics } from '@vercel/analytics/react'
10 | import Fullstory from '@/components/fullstory'
11 |
12 | const mainFont = localFont({
13 | src: '../../public/fonts/arialroundedmtbold.ttf',
14 | variable: '--font-main',
15 | })
16 |
17 | export const metadata: Metadata = {
18 | title: 'High Seas | Hack Club',
19 | description: 'Build cool projects. Get cool stuff.',
20 | openGraph: {
21 | images: [
22 | {
23 | url: '/ogcard.png',
24 | width: 1200,
25 | height: 630,
26 | alt: 'High Seas OG Image',
27 | },
28 | ],
29 | },
30 | }
31 |
32 | export default function RootLayout({
33 | children,
34 | }: Readonly<{
35 | children: React.ReactNode
36 | }>) {
37 | return (
38 |
39 |
40 |
44 |
50 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 | {children}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 | }
72 |
73 | export const dynamic = 'force-dynamic'
74 |
--------------------------------------------------------------------------------
/src/app/marketing/art/divider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/divider.png
--------------------------------------------------------------------------------
/src/app/marketing/art/divider2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/divider2.png
--------------------------------------------------------------------------------
/src/app/marketing/art/how1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/how1.png
--------------------------------------------------------------------------------
/src/app/marketing/art/how2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/how2.png
--------------------------------------------------------------------------------
/src/app/marketing/art/how3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/how3.png
--------------------------------------------------------------------------------
/src/app/marketing/art/orphwoah.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/orphwoah.png
--------------------------------------------------------------------------------
/src/app/marketing/art/paper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/paper.png
--------------------------------------------------------------------------------
/src/app/marketing/art/scales.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/scales.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop1.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop2.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop3.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop4.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop5.png
--------------------------------------------------------------------------------
/src/app/marketing/art/shop/shop6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/art/shop/shop6.png
--------------------------------------------------------------------------------
/src/app/marketing/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @font-face {
6 | font-family: 'Arial MTRoundedBold';
7 | src: url('/fonts/arialroundedmtbold.ttf') format('truetype');
8 | font-weight: bold;
9 | font-style: normal;
10 | }
11 |
12 | body {
13 | background: white;
14 | font-family: 'Arial MTRoundedBold', sans-serif;
15 | }
16 |
17 | html {
18 | scroll-behavior: smooth;
19 | }
20 |
21 | .bodycss {
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | scroll-behavior: smooth;
25 | background: #ff73f5;
26 | background: linear-gradient(to bottom, #ff73f5, #f6b163, #ec4899);
27 | color: white;
28 | }
29 |
30 | .watergradient {
31 | background: linear-gradient(to bottom, #49a8f1, #36349c, #000000);
32 | color: white;
33 | }
34 |
35 | .paper {
36 | background-image: url(./art/paper.png);
37 | background-repeat: repeat;
38 | }
39 |
40 | .paper::before {
41 | content: '';
42 | position: absolute;
43 | top: 0;
44 | left: 0;
45 | right: 0;
46 | bottom: 0;
47 | background-image: url(./art/paper.png);
48 | background-repeat: repeat;
49 | opacity: 0.2;
50 | z-index: -1;
51 | pointer-events: none;
52 | }
53 |
54 | .landing {
55 | display: flex;
56 | padding: 0 10vw;
57 | height: 100vh;
58 | background-color: #46c1fe;
59 | }
60 |
61 | .landing-left {
62 | width: 55vw;
63 | display: flex;
64 | flex-direction: column;
65 | justify-content: center;
66 | text-align: left;
67 | align-items: left;
68 | }
69 |
70 | .landing-right {
71 | width: 45%;
72 | display: flex;
73 | flex-direction: column;
74 | justify-content: center;
75 | text-align: left;
76 | align-items: left;
77 | }
78 |
79 | @media screen and (max-width: 800px) {
80 | .landing {
81 | flex-direction: column;
82 | padding-top: 100px;
83 | padding-bottom: 0;
84 | }
85 |
86 | .landing-left {
87 | width: 100vw;
88 | text-align: center;
89 | align-items: center;
90 | margin-bottom: 30px;
91 | }
92 |
93 | .landing-right {
94 | width: 80vw;
95 | text-align: center;
96 | align-items: center;
97 | }
98 | }
99 |
100 | .faqLink {
101 | color: lightpink !important;
102 | text-decoration: underline !important;
103 | }
104 |
105 | .aboutLink {
106 | color: lightcyan !important;
107 | text-decoration: underline !important;
108 | }
109 |
110 | .pop {
111 | transition: transform 0.1s ease-in-out;
112 | }
113 |
114 | .pop:hover {
115 | transform: scale(1.05);
116 | }
117 |
118 | .linkPop {
119 | transition: transform 0.08s ease-in-out;
120 | }
121 |
122 | .linkPop:hover {
123 | transform: scale(1.2) rotate(1deg);
124 | }
125 |
126 | .footLink {
127 | color: rgb(104, 10, 246);
128 | text-decoration: underline;
129 | }
130 |
131 | .buildLink {
132 | color: lightpink;
133 | text-decoration: underline;
134 | }
135 |
136 | .bobble {
137 | animation: bobble 2s infinite;
138 | }
139 |
140 | @keyframes bobble {
141 | 0%,
142 | 100% {
143 | transform: translateY(0);
144 | }
145 | 50% {
146 | transform: translateY(-10px);
147 | }
148 | }
149 |
150 | @keyframes scroll {
151 | 0% {
152 | transform: translateX(0);
153 | }
154 | 100% {
155 | transform: translateX(-100%);
156 | }
157 | }
158 |
159 | .images-ani {
160 | display: flex;
161 | overflow: hidden;
162 | white-space: nowrap;
163 | position: relative;
164 | width: 200%; /* Double the width to accommodate two sets of images */
165 | }
166 |
167 | .images-slide {
168 | display: flex;
169 | width: 50%; /* Each set of images takes up half the width */
170 | animation: scroll 40s linear infinite;
171 | }
172 |
173 | .images-slide img {
174 | width: 500px;
175 | height: auto;
176 | margin: 0px;
177 | }
178 |
179 | progress::-moz-progress-bar {
180 | background: blue;
181 | }
182 | progress::-webkit-progress-value {
183 | background: blue;
184 | }
185 | progress {
186 | color: blue;
187 | }
188 |
--------------------------------------------------------------------------------
/src/app/marketing/invite-job.js:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { sql } from '@vercel/postgres'
4 | import { headers } from 'next/headers'
5 |
6 | async function sendInviteJob({ email, userAgent }) {
7 | const headersList = headers()
8 | const ipAddress = headersList.get('x-forwarded-for')
9 |
10 | const result =
11 | await sql`INSERT INTO invite_job (email, ip_address, user_agent) VALUES (${email}, ${ipAddress}, ${userAgent});`
12 |
13 | // return result;
14 | }
15 |
16 | async function processPendingInviteJobs() {
17 | const { rows } =
18 | await sql`SELECT * FROM invite_job WHERE airtable_invite_record_id IS NULL LIMIT 10`
19 |
20 | if (rows.length === 0) {
21 | return
22 | }
23 |
24 | console.log(
25 | `Processing ${rows.length} pending invite jobs for ${rows.map((row) => row.email).join(', ')}`,
26 | )
27 |
28 | const fields = rows.map((row) => ({
29 | fields: {
30 | Email: row.email,
31 | 'Form Submission IP': row.ip_address,
32 | 'User Agent': row.user_agent,
33 | },
34 | }))
35 |
36 | console.log(
37 | `Creating ${fields.length} records in Airtable for ${fields.map((field) => field.fields.Email).join(', ')}`,
38 | )
39 |
40 | const createdRecords = await fetch(
41 | 'https://middleman.hackclub.com/airtable/v0/appaqcJtn33vb59Au/High Seas',
42 | {
43 | cache: 'no-cache',
44 | method: 'POST',
45 | headers: {
46 | Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
47 | 'Content-Type': 'application/json',
48 | 'User-Agent': 'highseas.hackclub.com (processPendingInviteJobs)',
49 | },
50 | body: JSON.stringify({ records: fields }),
51 | },
52 | ).then((r) => r.json())
53 |
54 | console.log(
55 | `Created ${createdRecords.records.length} records in Airtable for ${createdRecords.records.map((record) => record.fields.Email).join(', ')}`,
56 | )
57 |
58 | for (const record of createdRecords.records) {
59 | const { id, fields } = record
60 | const email = fields['Email']
61 | await sql`UPDATE invite_job SET airtable_invite_record_id = ${id} WHERE email = ${email} AND airtable_invite_record_id IS NULL;`
62 | }
63 | }
64 |
65 | export { sendInviteJob, processPendingInviteJobs }
66 |
--------------------------------------------------------------------------------
/src/app/marketing/marketing-utils.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | import { headers } from 'next/headers'
3 | import Airtable from 'airtable'
4 | import { createWaka } from '../utils/waka'
5 | import { getSession } from '../utils/auth'
6 |
7 | import createBackgroundJob from '../api/cron/create-background-job'
8 |
9 | const highSeasPeopleTable = () => {
10 | const highSeasBaseId = process.env.BASE_ID
11 | if (!highSeasBaseId) throw new Error('No Base ID env var set')
12 | return Airtable.base(highSeasBaseId)('tblfTzYVqvDJlIYUB')
13 | }
14 |
15 | export async function handleEmailSubmission(
16 | email: string,
17 | isMobile: boolean,
18 | userAgent: string,
19 | urlParams: string,
20 | ): Promise<{
21 | username: string
22 | key: string
23 | } | null> {
24 | throw new Error('High Seas has ended! Sign-ups are disabled.')
25 |
26 | /*
27 | if (!email) throw new Error('No email supplied to handleEmailSubmission')
28 | if (!userAgent)
29 | throw new Error('No user agent supplied to handleEmailSubmission')
30 |
31 | const ipAddress = headers().get('x-forwarded-for')
32 |
33 | const [session, _backgroundJob] = await Promise.all([
34 | getSession(),
35 | createBackgroundJob('invite', { email, ipAddress, userAgent, isMobile }),
36 | ])
37 |
38 | const slackId = session?.slackId
39 |
40 | const signup = await createWaka(email, null, slackId)
41 | const { username, key } = signup
42 |
43 | if (username) {
44 | await createBackgroundJob('create_person', {
45 | email,
46 | ipAddress,
47 | isMobile,
48 | username,
49 | urlParams,
50 | })
51 | }
52 |
53 | return {
54 | username,
55 | key,
56 | }
57 | */
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/marketing/marquee/1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/1.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/10.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/10.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/12.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/12.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/13.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/13.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/14.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/14.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/15.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/15.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/16.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/16.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/17.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/17.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/18.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/18.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/19.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/19.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/2.png
--------------------------------------------------------------------------------
/src/app/marketing/marquee/20.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/20.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/21.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/21.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/3.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/4.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/5.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/5.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/6.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/6.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/7.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/7.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/8.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/8.jpeg
--------------------------------------------------------------------------------
/src/app/marketing/marquee/9.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackclub/high-seas/0185c716b5c4fab71cc8ac1bfa58ad314e1be3ed/src/app/marketing/marquee/9.jpeg
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import Marketing from './marketing/page'
4 |
5 | export default async function Page() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/signout/route.ts:
--------------------------------------------------------------------------------
1 | export const dynamic = 'force-dynamic'
2 |
3 | import { redirect } from 'next/navigation'
4 | import { deleteSession } from '../utils/auth'
5 |
6 | export async function GET() {
7 | console.log('SIGNING OUT!!!!!!')
8 | await deleteSession()
9 | return redirect('/')
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/slack-error/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { AlertCircle } from 'lucide-react'
4 | import { buttonVariants } from '@/components/ui/button'
5 | import Link from 'next/link'
6 | import { useMemo } from 'react'
7 | import { reportError } from './report-error'
8 |
9 | export default function SlackAuthErrorPage({
10 | searchParams,
11 | }: {
12 | searchParams: { err?: string }
13 | }) {
14 | const { err } = searchParams
15 | useMemo(() => {
16 | if (err) {
17 | reportError(err)
18 | }
19 | }, [])
20 |
21 | const isSignupDisabled = err?.includes('Sign-ups are currently disabled')
22 |
23 | return (
24 |
25 |
26 |
30 |
31 | {isSignupDisabled ? 'Sign-ups are closed!' : 'Going overboard!'}
32 |
33 |
34 | {isSignupDisabled
35 | ? 'High Seas has ended! Sign-ups are disabled.'
36 | : "We Arrrr over capacity right now, but we got your request to join the crew... we'll reach out once we figure out how to keep this ship from capsizing."}
37 | {!isSignupDisabled &&
38 | (err || 'An error occurred during Slack authentication.')}
39 |
40 |
41 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/slack-error/report-error.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import Airtable from 'airtable'
4 |
5 | export const reportError = async (e: string) => {
6 | console.log('reporting error')
7 | const base = new Airtable({
8 | apiKey: process.env.AIRTABLE_API_KEY,
9 | endpointUrl: process.env.AIRTABLE_ENDPOINT_URL,
10 | }).base('appTeNFYcUiYfGcR6')
11 |
12 | base('non_user_in_slack').create(
13 | [
14 | {
15 | fields: {
16 | error: e,
17 | },
18 | },
19 | ],
20 | function (err, records) {
21 | if (err) {
22 | console.error(err)
23 | return
24 | }
25 | },
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/utils/auth.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | /* Functions exported from this module will be exposed
3 | * as HTTP endpoints. Dragons be here.
4 | */
5 |
6 | import { cookies } from 'next/headers'
7 | import { getPersonByMagicToken } from './server/airtable'
8 | import { getSelfPerson } from './server/airtable'
9 | import {
10 | HsSession,
11 | sessionCookieName,
12 | signAndSet,
13 | verifySession,
14 | } from './server/auth'
15 |
16 | export async function getSession(): Promise {
17 | try {
18 | const sessionCookie = cookies().get(sessionCookieName)
19 | if (!sessionCookie) return null
20 |
21 | const unsafeSession = JSON.parse(sessionCookie.value)
22 | return verifySession(unsafeSession)
23 | } catch (error) {
24 | console.error('Error verifying session:', error)
25 | return null
26 | }
27 | }
28 |
29 | export async function deleteSession() {
30 | const cookieKeys =
31 | 'academy-completed ships signpost-feed tickets verification waka'
32 | .split(' ')
33 | .forEach((key) => cookies().delete(key))
34 | cookies().delete(sessionCookieName)
35 | }
36 |
37 | export async function createMagicSession(magicCode: string) {
38 | try {
39 | const partialPersonData = await getPersonByMagicToken(magicCode)
40 | if (!partialPersonData)
41 | throw new Error('High Seas has ended! Sign-ups are disabled.')
42 |
43 | const { id, email, slackId } = partialPersonData
44 |
45 | console.log('SOTNRESTNSREINTS', { id, email, slackId })
46 |
47 | const session: HsSession = {
48 | personId: id,
49 | authType: 'magic-link',
50 | slackId,
51 | email,
52 | }
53 |
54 | await signAndSet(session)
55 | } catch (error) {
56 | console.error('Error creating Magic session:', error)
57 | throw error
58 | }
59 | }
60 |
61 | export async function impersonate(slackId: string) {
62 | // only allow impersonation in development while testing
63 | if (process.env.NODE_ENV !== 'development') {
64 | return
65 | }
66 |
67 | // look for airtable user with this record
68 | const person = await getSelfPerson(slackId)
69 | const id = person.id
70 | const email = person.fields.email
71 |
72 | const session: HsSession = {
73 | personId: id,
74 | authType: 'impersonation',
75 | slackId,
76 | email,
77 | }
78 |
79 | await signAndSet(session)
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/utils/server/airtable.ts:
--------------------------------------------------------------------------------
1 | import 'server-only'
2 |
3 | export const getSelfPerson = async (slackId: string) => {
4 | if (!/^[UW][A-Z0-9]{8,11}$/.test(slackId)) {
5 | const err = new Error(
6 | `Invalid Slack ID passed to getSelfPerson: ${slackId}`,
7 | )
8 | console.error(err)
9 | throw err
10 | }
11 |
12 | const url = `https://middleman.hackclub.com/airtable/v0/${process.env.BASE_ID}/people`
13 | const filterByFormula = encodeURIComponent(`{slack_id} = '${slackId}'`)
14 | const response = await fetch(`${url}?filterByFormula=${filterByFormula}`, {
15 | method: 'GET',
16 | headers: {
17 | Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
18 | 'Content-Type': 'application/json',
19 | 'User-Agent': 'highseas.hackclub.com (getSelfPerson)',
20 | },
21 | })
22 |
23 | if (!response.ok) {
24 | throw new Error(`HTTP error! status: ${response.status}`)
25 | }
26 |
27 | let data
28 | try {
29 | data = await response.json()
30 | } catch (e) {
31 | console.error(e, await response.text())
32 | throw e
33 | }
34 | return data.records[0]
35 | }
36 |
37 | export async function getPersonByAuto(num: string): Promise<{
38 | slackId: string
39 | } | null> {
40 | const baseId = process.env.BASE_ID
41 | const apiKey = process.env.AIRTABLE_API_KEY
42 | const table = 'people'
43 |
44 | if (!Number(num)) {
45 | const err = new Error(
46 | `Non-numeric getPersonByAuto parameter passed: ${num}`,
47 | )
48 | console.error(err)
49 | throw err
50 | }
51 |
52 | const url = `https://middleman.hackclub.com/airtable/v0/${baseId}/${table}?filterByFormula={autonumber}='${encodeURIComponent(num)}'`
53 |
54 | const response = await fetch(url, {
55 | headers: {
56 | Authorization: `Bearer ${apiKey}`,
57 | 'Content-Type': 'application/json',
58 | 'User-Agent': 'highseas.hackclub.com (getPersonByAuto)',
59 | },
60 | })
61 |
62 | if (!response.ok) {
63 | const err = new Error(`Airtable API error: ${await response.text()}`)
64 | console.error(err)
65 | throw err
66 | }
67 |
68 | const data = await response.json()
69 | if (!data.records || data.records.length === 0) return null
70 |
71 | const id = data.records[0].id
72 | const email = data.records[0].fields.email
73 | const slackId = data.records[0].fields.slack_id
74 |
75 | if (!id || !email || !slackId) return null
76 |
77 | return { slackId }
78 | }
79 |
80 | export async function getPersonByMagicToken(token: string): Promise<{
81 | id: string
82 | email: string
83 | slackId: string
84 | } | null> {
85 | const uuidRegex =
86 | /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
87 | if (!uuidRegex.test(token)) {
88 | const err = new Error(
89 | `Invalid magic token passed to getPersonByMagicToken: ${token}`,
90 | )
91 | console.error(err)
92 | throw err
93 | }
94 |
95 | const baseId = process.env.BASE_ID
96 | const apiKey = process.env.AIRTABLE_API_KEY
97 | const table = 'people'
98 |
99 | const url = `https://middleman.hackclub.com/airtable/v0/${baseId}/${table}?filterByFormula={magic_auth_token}='${encodeURIComponent(token)}'`
100 |
101 | const response = await fetch(url, {
102 | headers: {
103 | Authorization: `Bearer ${apiKey}`,
104 | 'Content-Type': 'application/json',
105 | 'User-Agent': 'highseas.hackclub.com (getPersonByMagicToken)',
106 | },
107 | })
108 |
109 | if (!response.ok) {
110 | const err = new Error(`Airtable API error: ${await response.text()}`)
111 | console.error(err)
112 | throw err
113 | }
114 |
115 | const data = await response.json()
116 | if (!data.records || data.records.length === 0) return null
117 |
118 | const id = data.records[0].id
119 | const email = data.records[0].fields.email
120 | const slackId = data.records[0].fields.slack_id
121 |
122 | if (!id || !email || !slackId) return null
123 |
124 | return { id, email, slackId }
125 | }
126 |
--------------------------------------------------------------------------------
/src/app/utils/tavern.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import Airtable from 'airtable'
4 | import { getSession } from './auth'
5 | import { TavernEventItem } from '../harbor/tavern/tavern-utils'
6 |
7 | Airtable.configure({
8 | apiKey: process.env.AIRTABLE_API_KEY,
9 | endpointUrl: process.env.AIRTABLE_ENDPOINT_URL,
10 | })
11 |
12 | export type RsvpStatus = 'none' | 'organizer' | 'participant'
13 | export const setTavernRsvpStatus = async (rsvpStatus: RsvpStatus) => {
14 | // check auth
15 | const session = await getSession()
16 | if (!session) {
17 | return
18 | }
19 | if (!session.personId) {
20 | return
21 | }
22 |
23 | // update status
24 | const base = Airtable.base(process.env.BASE_ID)
25 | const result = await base('people').update(session.personId, {
26 | tavern_rsvp_status: rsvpStatus,
27 | })
28 |
29 | return result.get('tavern_rsvp_status')
30 | }
31 |
32 | export const getTavernRsvpStatus = async () => {
33 | // check auth
34 | const session = await getSession()
35 | if (!session) {
36 | return
37 | }
38 | if (!session.personId) {
39 | return
40 | }
41 |
42 | // get status
43 | const base = Airtable.base(process.env.BASE_ID)
44 | const record = await base('people').find(session.personId)
45 | return record.get('tavern_rsvp_status') as RsvpStatus
46 | }
47 |
48 | export const submitMyTavernLocation = async (tavernId: string) => {
49 | // check auth
50 | const session = await getSession()
51 | if (!session) {
52 | return
53 | }
54 | if (!session.personId) {
55 | return
56 | }
57 |
58 | // update status
59 | const base = Airtable.base(process.env.BASE_ID)
60 |
61 | await base('people').update(session.personId, {
62 | taverns_attendee: tavernId ? [tavernId] : [],
63 | })
64 | }
65 |
66 | export const getMyTavernLocation: Promise<
67 | TavernEventItem | null
68 | > = async () => {
69 | // check auth
70 | const session = await getSession()
71 | if (!session) {
72 | return
73 | }
74 | if (!session.personId) {
75 | return
76 | }
77 |
78 | // update status
79 | const base = Airtable.base(process.env.BASE_ID)
80 |
81 | const foundTavern = await base('taverns')
82 | .select({
83 | filterByFormula: `FIND('${session.personId}', {attendee_record_ids} & '')`,
84 | })
85 | .firstPage()
86 | .then((r) => r[0])
87 |
88 | console.log('getMyTavernLocation: ', foundTavern)
89 |
90 | if (!foundTavern) return null
91 |
92 | return {
93 | id: foundTavern.id,
94 | city: foundTavern.get('city'),
95 | geocode: foundTavern.get('map_geocode'),
96 | locality: foundTavern.get('locality'),
97 | attendeeCount: foundTavern.get('attendees_count'),
98 | organizers: foundTavern.get('organizers'),
99 | channel: foundTavern.get('channel'),
100 | eventDate: foundTavern.get('event_date'),
101 | }
102 | }
103 |
104 | export async function submitShirtSize(size: string) {
105 | // check auth
106 | const session = await getSession()
107 | if (!session) {
108 | return
109 | }
110 | if (!session.personId) {
111 | return
112 | }
113 |
114 | if (!['Small', 'Medium', 'Large', 'XL', 'XXL'].includes(size)) {
115 | return
116 | }
117 |
118 | // update status
119 | const base = Airtable.base(process.env.BASE_ID)
120 |
121 | await base('people').update(session.personId, {
122 | shirt_size: size,
123 | })
124 | }
125 |
126 | export async function getShirtSize() {
127 | // check auth
128 | const session = await getSession()
129 | if (!session) {
130 | return
131 | }
132 | if (!session.personId) {
133 | return
134 | }
135 |
136 | // update status
137 | const base = Airtable.base(process.env.BASE_ID)
138 |
139 | return await base('people')
140 | .find(session.personId)
141 | .then((r) => r.get('shirt_size'))
142 | }
143 |
--------------------------------------------------------------------------------
/src/app/utils/wakatime-setup/setup-modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import Platforms from './platforms'
3 | import Modal from '../../../components/ui/modal'
4 | import { fetchWaka } from '../server/data'
5 | import useLocalStorageState from '../../../../lib/useLocalStorageState'
6 |
7 | function SetupModal({
8 | isOpen,
9 | close,
10 | onHbDetect,
11 | }: {
12 | isOpen: boolean
13 | close: () => void
14 | onHbDetect?: () => void
15 | }) {
16 | const [wakaKey, setWakaKey] = useLocalStorageState('cache.wakaKey', '')
17 | const [hasHb, setHasHb] = useLocalStorageState('cache.hasHb', false)
18 | const [wakaUsername, setWakaUsername] = useLocalStorageState(
19 | 'cache.wakaUsername',
20 | '',
21 | )
22 |
23 | useEffect(() => {
24 | fetchWaka().then(({ key, hasHb, username }) => {
25 | setWakaKey(key)
26 | setHasHb(hasHb)
27 | setWakaUsername(username)
28 | })
29 |
30 | let mounted = true
31 | let timeoutId: number
32 |
33 | async function checkHeartbeat() {
34 | if (!onHbDetect || !mounted || hasHb) return
35 |
36 | if (hasHb && mounted) {
37 | console.log('Heartbeat data detected')
38 | onHbDetect()
39 | hasHb(true)
40 | } else if (mounted) {
41 | timeoutId = window.setTimeout(checkHeartbeat, 5000)
42 | }
43 | }
44 |
45 | checkHeartbeat()
46 | return () => {
47 | mounted = false
48 | window.clearTimeout(timeoutId)
49 | }
50 | }, [])
51 |
52 | return (
53 |
54 | {wakaKey && }
55 | {onHbDetect && (
56 |
57 | {hasHb ? 'Installed!' : 'Waiting for install...'}
58 |
59 | )}
60 |
61 | )
62 | }
63 |
64 | export default SetupModal
65 |
--------------------------------------------------------------------------------
/src/app/utils/wakatime-setup/tutorial-utils.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getSession } from '../../utils/auth'
4 |
5 | // export async function hasHb(username: string, key: string): Promise {
6 | // if (!username)
7 | // throw new Error("Username is undefined while checking waka hasData");
8 |
9 | // const res = await fetch(
10 | // `https://waka.hackclub.com/api/special/hasData?user=${username}`,
11 | // { headers: { Authorization: `Bearer ${process.env.WAKA_API_KEY}` } },
12 | // );
13 | // if (!res.ok) {
14 | // const txt = await res.text();
15 | // const err = new Error(
16 | // `Error while checking ${username}'s waka hasData: ${txt}`,
17 | // );
18 | // console.error(err);
19 | // throw err;
20 | // }
21 |
22 | // const resJson = await res.json();
23 | // return resJson.hasData === true;
24 | // }
25 |
--------------------------------------------------------------------------------
/src/components/fullstory.js:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import FullStory from 'react-fullstory'
4 |
5 | export default function Fullstory() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/idea-generator.css:
--------------------------------------------------------------------------------
1 | /* .idea-generator {} */
2 | .idea-generator .idle {
3 | cursor: pointer;
4 | animation: idle 2s infinite alternate;
5 | filter: contrast(60%);
6 | }
7 | .idea-generator .thinking {
8 | cursor: wait;
9 | animation: idle 2s infinite alternate;
10 | }
11 | .idea-generator .typing {
12 | cursor: wait;
13 | animation: talking 0.5s infinite alternate;
14 | }
15 |
16 | @keyframes idle {
17 | from {
18 | transform: translateY(0%);
19 | }
20 | to {
21 | transform: translateY(2%);
22 | }
23 | }
24 |
25 | @keyframes talking {
26 | from {
27 | transform: scale(1.01, 0.99) translateY(2%);
28 | }
29 | to {
30 | transform: scale(0.99, 1.01) translateY(0%);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/jagged-card-small.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Svg = ({ color }: { color: string }) =>
4 | color ? (
5 |
14 |
19 |
24 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
43 | ) : null
44 |
45 | const JaggedCardSmall = ({
46 | children,
47 | className = '',
48 | bgColor,
49 | shadow = true,
50 | ...props
51 | }) => {
52 | return (
53 |
60 |
61 |
{children}
62 |
63 | )
64 | }
65 |
66 | export default JaggedCardSmall
67 |
--------------------------------------------------------------------------------
/src/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { Components } from 'react-markdown'
3 |
4 | export const markdownComponents: Components = {
5 | h1: ({ ...props }) => (
6 |
7 | ),
8 | h2: ({ ...props }) => (
9 |
13 | ),
14 | h3: ({ ...props }) => (
15 |
19 | ),
20 | p: ({ ...props }) =>
,
21 | a: ({ ...props }) => (
22 |
27 | ),
28 | ul: ({ ...props }) => ,
29 | ol: ({ ...props }) => ,
30 | li: ({ ...props }) => ,
31 | img: ({ src, alt, ...props }) => (
32 |
33 |
39 |
40 | ),
41 | code: ({ className, children, ...props }) => {
42 | const match = /language-(\w+)/.exec(className || '')
43 | return match ? (
44 |
45 |
46 | {children}
47 |
48 |
49 | ) : (
50 |
54 | {children}
55 |
56 | )
57 | },
58 | blockquote: ({ ...props }) => (
59 |
63 | ),
64 | table: ({ ...props }) => (
65 |
71 | ),
72 | th: ({ ...props }) => (
73 |
77 | ),
78 | td: ({ ...props }) => (
79 |
83 | ),
84 | hr: ({ ...props }) => (
85 |
92 | ),
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/nav.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getSession } from '@/app/utils/auth'
4 | import SignOut from './sign_out'
5 | import SignIn from './sign_in'
6 | import Image from 'next/image'
7 | import Logo from '/public/logo.png'
8 |
9 | export default async function Nav() {
10 | const session = await getSession()
11 |
12 | return (
13 |
14 |
24 |
25 |
26 |
27 |
28 | {session?.picture && session.givenName ? (
29 |
30 |
37 |
Hey, {session.givenName}!
{' '}
38 |
39 | ) : null}
40 | {session ? (
41 | <>
42 |
43 | >
44 | ) : (
45 |
46 | )}
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/sign_in.tsx:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { buttonVariants } from '@/components/ui/button'
4 | import { headers } from 'next/headers'
5 |
6 | export default async function SignIn({
7 | variant = 'default',
8 | session,
9 | }: {
10 | variant: 'small' | 'default'
11 | session: any
12 | }) {
13 | const headersList = headers()
14 | const host = headersList.get('host') || ''
15 | const proto = headersList.get('x-forwarded-proto') || 'http'
16 | const origin = encodeURIComponent(`${proto}://${host}`)
17 |
18 | const slackAuthUrl = `https://hackclub.slack.com/oauth/v2/authorize?scope=&user_scope=openid%2Cprofile%2Cemail&redirect_uri=${origin}/api/slack_redirect&client_id=${process.env.SLACK_CLIENT_ID}`
19 |
20 | const textSize = variant === 'small' ? 'text-base' : 'text-2xl'
21 | return (
22 |
26 | {session ? 'Enter the Harbor' : "Already in Hack Club's Slack?"}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/sign_out.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button, buttonVariants } from './ui/button'
4 |
5 | export default function SignOut() {
6 | const handleOnClick = () => {
7 | Object.keys(localStorage).forEach((key) => {
8 | if (key.startsWith('cache.')) {
9 | localStorage.removeItem(key)
10 | }
11 | })
12 | sessionStorage.clear()
13 | }
14 |
15 | return (
16 |
17 |
18 | Sign out
19 | of High Seas
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/speech-to-text.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useRef, useEffect } from 'react'
4 | import { Button } from '@/components/ui/button'
5 | import { Mic, CircleStop } from 'lucide-react'
6 | import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
7 |
8 | export default function SpeechToText({
9 | handleResults,
10 | }: {
11 | handleResults: (transcript: string) => void
12 | }) {
13 | const [isListening, setIsListening] = useState(false)
14 | const [transcript, setTranscript] = useState('')
15 |
16 | const [open, setOpen] = useState(false)
17 |
18 | const [supportsSpeechRecognition, setSupportsSpeechRecognition] =
19 | useState(false)
20 |
21 | const recognitionRef = useRef(null)
22 |
23 | useEffect(() => {
24 | const ua = navigator?.userAgent || ''
25 |
26 | const hasSupport =
27 | typeof window !== 'undefined' &&
28 | ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)
29 | const isMobile =
30 | typeof window !== 'undefined' &&
31 | window.matchMedia('(pointer: coarse)').matches
32 |
33 | // unfortunately most chrome forks misreport support for this API
34 | const isArc = ua.includes('Arc')
35 | const isChromium = ua.includes('Chromium')
36 | const isBrave = navigator?.brave // good on brave for supporting this!
37 |
38 | // mobile keyboards already support STT, so we don't need to render in that case
39 | setSupportsSpeechRecognition(
40 | hasSupport && !isMobile && !isArc && !isChromium && !isBrave,
41 | )
42 | }, [])
43 |
44 | const startListening = async () => {
45 | setTranscript('')
46 |
47 | await initLocalRecording()
48 |
49 | await recognitionRef.current.start()
50 | setIsListening(true)
51 | }
52 |
53 | const stopListening = () => {
54 | recognitionRef.current.stop()
55 | handleResults(transcript)
56 | setIsListening(false)
57 | }
58 |
59 | const initLocalRecording = async () => {
60 | if (!window) {
61 | return
62 | }
63 | const SpeechRecognition =
64 | window?.SpeechRecognition || window?.webkitSpeechRecognition
65 | if (!SpeechRecognition) {
66 | return
67 | }
68 | recognitionRef.current = new SpeechRecognition()
69 | recognitionRef.current.interimResults = true
70 |
71 | recognitionRef.current.onresult = (event) => {
72 | for (const result of event.results) {
73 | setTranscript(result[0].transcript)
74 | }
75 | }
76 |
77 | recognitionRef.current.onerror = (event) => {
78 | console.error({ event })
79 | setIsListening(false)
80 | }
81 |
82 | recognitionRef.current.onend = (event) => {
83 | console.log({ end: event })
84 | setIsListening(false)
85 | }
86 | }
87 |
88 | if (supportsSpeechRecognition) {
89 | return (
90 | <>
91 |
92 |
93 | {isListening ? 'Finish and paste' : 'Click to begin dictation'}
94 |
95 | setOpen(true)}
98 | onMouseLeave={() => setOpen(false)}
99 | >
100 |
105 | {isListening ? (
106 |
107 | ) : (
108 |
109 | )}
110 |
111 |
112 |
113 | >
114 | )
115 | } else {
116 | return null
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/steps.tsx:
--------------------------------------------------------------------------------
1 | import Pill from './ui/pill'
2 |
3 | const steps = [
4 | { name: 'Hackatime', done: true },
5 | { name: 'Ship', done: true },
6 | { name: 'Vote', done: false },
7 | { name: 'Prizes', done: false },
8 | ]
9 |
10 | export default function Steps({}) {
11 | return (
12 |
13 | {steps.map((step, idx) => (
14 |
25 | ))}
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
14 | },
15 | },
16 | defaultVariants: {
17 | variant: 'default',
18 | },
19 | },
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = 'Alert'
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = 'AlertTitle'
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = 'AlertDescription'
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | },
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 transition duration-150 active:scale-90',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-[#9AD9EE] text-black',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'bg-[#ffffffaa] text-secondary-foreground border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | },
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
53 | )
54 | },
55 | )
56 | Button.displayName = 'Button'
57 |
58 | export { Button, buttonVariants }
59 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => {
9 | let backgroundColor,
10 | backgroundImage,
11 | backgroundSize,
12 | backgroundRepeat,
13 | backdropFilter
14 |
15 | switch (props?.type) {
16 | case 'cardboard':
17 | backgroundColor = '#140726c0'
18 | backgroundSize = '50rem auto'
19 | backgroundRepeat = 'repeat'
20 | backdropFilter = 'blur(1px)'
21 | break
22 | default: // paper
23 | backgroundColor = '#fffffff0'
24 | // backgroundImage = "url(/textures/paper.png)";
25 | backgroundSize = '10rem 100%'
26 | backgroundRepeat = 'repeat-x'
27 | }
28 |
29 | return (
30 |
45 | )
46 | })
47 | Card.displayName = 'Card'
48 |
49 | const CardHeader = React.forwardRef<
50 | HTMLDivElement,
51 | React.HTMLAttributes
52 | >(({ className, ...props }, ref) => (
53 |
58 | ))
59 | CardHeader.displayName = 'CardHeader'
60 |
61 | const CardTitle = React.forwardRef<
62 | HTMLParagraphElement,
63 | React.HTMLAttributes
64 | >(({ className, ...props }, ref) => (
65 |
73 | ))
74 | CardTitle.displayName = 'CardTitle'
75 |
76 | const CardDescription = React.forwardRef<
77 | HTMLParagraphElement,
78 | React.HTMLAttributes
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | CardDescription.displayName = 'CardDescription'
87 |
88 | const CardContent = React.forwardRef<
89 | HTMLDivElement,
90 | React.HTMLAttributes
91 | >(({ className, ...props }, ref) => (
92 |
93 | ))
94 | CardContent.displayName = 'CardContent'
95 |
96 | const CardFooter = React.forwardRef<
97 | HTMLDivElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | CardFooter.displayName = 'CardFooter'
107 |
108 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
109 |
--------------------------------------------------------------------------------
/src/components/ui/loading_spinner.js:
--------------------------------------------------------------------------------
1 | import { useMemo, useEffect, useState } from 'react'
2 | import { loadingSpinners, sample } from '../../../lib/flavor'
3 |
4 | const LoadingSpinner = () => {
5 | const [src, setSrc] = useState('')
6 |
7 | useEffect(() => {
8 | setSrc(sample(loadingSpinners))
9 | }, [])
10 |
11 | return useMemo(
12 | () => (
13 |
14 | {src && (
15 |
24 | )}
25 |
26 | ),
27 | [src],
28 | )
29 | }
30 |
31 | export { LoadingSpinner }
32 |
--------------------------------------------------------------------------------
/src/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { AnimatePresence, motion } from 'framer-motion'
3 | import Icon from '@hackclub/icons'
4 | import JaggedCard from '../jagged-card'
5 | import { createPortal } from 'react-dom'
6 |
7 | export default function Modal({
8 | isOpen,
9 | close,
10 | hideCloseButton = false,
11 | children,
12 | ...props
13 | }: {
14 | isOpen: boolean
15 | close: (_: any) => void
16 | hideCloseButton?: boolean
17 | children: React.ReactNode
18 | props?: any
19 | }) {
20 | useEffect(() => {
21 | const handleKeyDown = (event: KeyboardEvent) => {
22 | if (event.key === 'Escape' && !hideCloseButton) {
23 | close(event)
24 | }
25 | }
26 | document.addEventListener('keydown', handleKeyDown)
27 | return () => {
28 | document.removeEventListener('keydown', handleKeyDown)
29 | }
30 | }, [hideCloseButton, close])
31 |
32 | return (
33 |
34 | {isOpen ? (
35 | <>
36 | {createPortal(
37 |
49 |
53 |
54 |
60 | e.stopPropagation()}
63 | >
64 | {hideCloseButton ? null : (
65 |
77 |
78 |
79 | )}
80 | {children}
81 |
82 |
83 | ,
84 | document.body,
85 | )}
86 | >
87 | ) : null}
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/ui/pill.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Icon from '@hackclub/icons'
3 |
4 | export default function Pill({
5 | msg,
6 | color = 'gray',
7 | classes = '',
8 | glyph,
9 | glyphImage,
10 | glyphSize = 24,
11 | glyphStyles = {},
12 | percentage,
13 | id = '',
14 | }: {
15 | msg: string
16 | color?: 'red' | 'yellow' | 'green' | 'blue' | 'purple' | 'gray'
17 | classes?: string
18 | glyph?: any
19 | glyphImage?: React.ReactNode
20 | glyphSize?: number
21 | glyphStyles?: React.CSSProperties
22 | percentage?: number
23 | id?: string
24 | }) {
25 | const colorClasses = {
26 | red: 'text-red-600 bg-red-50 border-red-500/10',
27 | yellow: 'text-yellow-600 bg-yellow-50 border-yellow-500/10',
28 | green: 'text-green-600 bg-green-50 border-green-500/10',
29 | blue: 'text-blue-600 bg-blue-50 border-blue-500/10',
30 | purple: 'text-purple-600 bg-purple-50 border-purple-500/10',
31 | gray: 'text-gray-600 bg-gray-50 border-gray-500/10',
32 | }
33 |
34 | const progressBarStyle = percentage
35 | ? {
36 | background: `linear-gradient(to right, #C8DBFF, ${percentage}%, transparent ${percentage}%)`,
37 | }
38 | : {}
39 |
40 | return (
41 |
46 | {glyphImage}
47 | {glyph && (
48 |
57 | )}
58 | {msg}
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as PopoverPrimitive from '@radix-ui/react-popover'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/src/components/ui/repo_link.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import Icon from '@hackclub/icons'
3 | import { buttonVariants } from './button'
4 |
5 | const RepoLink = ({ repo }: { repo: string }) => {
6 | if (!repo) return null
7 | let repoHost = 'Git Repo'
8 | let repoIcon = 'external'
9 | if (repo.includes('github.com')) {
10 | repoHost = 'GitHub Repo'
11 | repoIcon = 'github'
12 | } else if (repo.includes('gitlab.com')) {
13 | repoHost = 'GitLab Repo'
14 | } else if (repo.includes('codeberg')) {
15 | repoHost = 'Codeberg Repo'
16 | } else if (repo.includes('git.hackclub.app')) {
17 | // the HC hosted forgejo instance https://hackclub.slack.com/archives/C056WDR3MQR/p1732500961325289?thread_ts=1732500940.778049&cid=C056WDR3MQR
18 | repoHost = 'Nest Repo'
19 | }
20 | return (
21 |
30 | {repoHost}
31 |
32 | )
33 | }
34 |
35 | export default RepoLink
36 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref,
15 | ) => (
16 |
27 | ),
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TabsPrimitive from '@radix-ui/react-tabs'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | // Use React.memo to memoize the TabsList component
11 | const TabsList = React.memo(
12 | React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
27 | )),
28 | )
29 | TabsList.displayName = TabsPrimitive.List.displayName
30 |
31 | const TabsTrigger = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef
34 | >(({ className, ...props }, ref) => (
35 |
43 | ))
44 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
45 |
46 | const TabsContent = React.forwardRef<
47 | React.ElementRef,
48 | React.ComponentPropsWithoutRef
49 | >(({ className, ...props }, ref) => (
50 |
58 | ))
59 | TabsContent.displayName = TabsPrimitive.Content.displayName
60 |
61 | export { Tabs, TabsList, TabsTrigger, TabsContent }
62 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useToast } from '@/hooks/use-toast'
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport as OriginalToastViewport,
11 | } from '@/components/ui/toast'
12 | import React from 'react'
13 |
14 | // Memoize the ToastViewport component to prevent unnecessary re-renders
15 | const ToastViewport = React.memo(OriginalToastViewport)
16 |
17 | export function Toaster() {
18 | const { toasts } = useToast()
19 |
20 | return (
21 |
22 | {toasts.map(function ({ id, title, description, action, ...props }) {
23 | return (
24 |
25 |
26 | {title && {title} }
27 | {description && (
28 | {description}
29 | )}
30 |
31 | {action}
32 |
33 |
34 | )
35 | })}
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ui/waka-lock.tsx:
--------------------------------------------------------------------------------
1 | import Icon from '@hackclub/icons'
2 | import React, { useEffect, useState } from 'react'
3 | import {
4 | Os,
5 | osFromAgent,
6 | SinglePlatform,
7 | } from '@/app/utils/wakatime-setup/tutorial-utils.client'
8 | import { Button, buttonVariants } from './button'
9 |
10 | const WakaLock = ({ wakaOverride, wakaToken, tabName }) => {
11 | const [userOs, setUserOs] = useState('unknown')
12 | const [showAllPlatforms, setShowAllPlatforms] = useState(false)
13 |
14 | useEffect(() => {
15 | const os = osFromAgent()
16 | setUserOs(os)
17 | setShowAllPlatforms(os === 'unknown')
18 | }, [])
19 |
20 | return (
21 |
22 |
23 |
24 |
Waiting for Hackatime install...
25 |
26 | {showAllPlatforms ? (
27 | <>
28 |
29 |
30 |
31 |
setShowAllPlatforms(false)}
34 | >
35 | Hide other platforms
36 |
37 | >
38 | ) : (
39 | <>
40 |
41 |
setShowAllPlatforms(true)}
44 | >
45 | Not using {userOs}? View instructions for other platforms.
46 |
47 | >
48 | )}
49 |
50 |
57 |
wakaOverride()}
60 | >
61 | Skip for now
62 |
63 |
64 | )
65 | }
66 | export { WakaLock }
67 |
--------------------------------------------------------------------------------
/src/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/nextjs'
2 |
3 | export async function register() {
4 | if (process.env.NEXT_RUNTIME === 'nodejs') {
5 | await import('../sentry.server.config')
6 | }
7 |
8 | if (process.env.NEXT_RUNTIME === 'edge') {
9 | await import('../sentry.edge.config')
10 | }
11 | }
12 |
13 | export const onRequestError = Sentry.captureRequestError
14 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/_error.jsx:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/nextjs'
2 | import Error from 'next/error'
3 |
4 | const CustomErrorComponent = (props) => {
5 | return
6 | }
7 |
8 | CustomErrorComponent.getInitialProps = async (contextData) => {
9 | // In case this is running in a serverless function, await this in order to give Sentry
10 | // time to send the error before the lambda exits
11 | await Sentry.captureUnderscoreErrorException(contextData)
12 |
13 | // This will contain the status code of the response
14 | return Error.getInitialProps(contextData)
15 | }
16 |
17 | export default CustomErrorComponent
18 |
--------------------------------------------------------------------------------
/test.ts:
--------------------------------------------------------------------------------
1 | export const getSelfPerson = async (slackId: string) => {
2 | const url = `https://middleman.hackclub.com/airtable/v0/${process.env.BASE_ID}/people`
3 | const filterByFormula = encodeURIComponent(`{slack_id} = '${slackId}'`)
4 | const response = await fetch(`${url}?filterByFormula=${filterByFormula}`, {
5 | method: 'GET',
6 | headers: {
7 | Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
8 | 'Content-Type': 'application/json',
9 | 'User-Agent': 'highseas.hackclub.com (tests)',
10 | },
11 | })
12 |
13 | if (!response.ok) {
14 | throw new Error(`HTTP error! status: ${response.status}`)
15 | }
16 |
17 | const data = await response.json()
18 | return data.records[0]
19 | }
20 |
21 | const r = await getSelfPerson('U07TETATJE7')
22 | console.log(r)
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "target": "ES2017"
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules", "src/app/harbor/shop/shop.tsx"]
27 | }
28 |
--------------------------------------------------------------------------------
/types/battles/airtable.ts:
--------------------------------------------------------------------------------
1 | import { FieldSet } from 'airtable'
2 |
3 | export interface Ships extends FieldSet {
4 | id?: string
5 | identifier?: string
6 | title: string
7 | credited_hours: number
8 | total_hours: number
9 | rating?: number
10 | entrant: string[]
11 | contest?: string[]
12 | repo_url: string
13 | readme_url: string
14 | deploy_url: string
15 | matchups?: string[]
16 | matchups_count?: number
17 | wins?: string[]
18 | wins_adjustments?: number
19 | losses?: string[]
20 | losses_adjustments?: number
21 | autonumber?: number
22 | screenshot_url: string
23 | entrant__slack_id: string[]
24 | ship_type?: string
25 | update_description?: string
26 | }
27 |
28 | export interface Battles extends FieldSet {
29 | identifier: string
30 | contest: string[]
31 | voter: string[]
32 | ships: string[]
33 | explanation: string
34 | winner: string[]
35 | winner_rating: number
36 | winner_adjustment: number
37 | loser: string[]
38 | loser_rating: number
39 | loser_adjustment: number
40 | }
41 |
42 | export interface Person extends FieldSet {
43 | id?: string
44 | identifier?: string
45 | first_name: string
46 | last_name: string
47 | email: string
48 | slack_id: string
49 | verification_status?: string
50 | all_battle_ship_autonumbers?: string[]
51 | }
52 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "crons": [
3 | {
4 | "path": "/api/cron/every-minute",
5 | "schedule": "*/3 * * * *"
6 | },
7 | {
8 | "path": "/api/cron/every-day",
9 | "schedule": "0 18 * * *"
10 | }
11 | ],
12 | "rewrites": [
13 | {
14 | "source": "/:path*",
15 | "has": [
16 | {
17 | "type": "host",
18 | "value": "ahoy.hack.club"
19 | }
20 | ],
21 | "destination": "https://highseas.hackclub.com/api/referral/:path*"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------