diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..029c2039 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This is a **pnpm + Turborepo monorepo** (`pnpm@10.33.2`, Node 20–23; the VM has Node 22 + pnpm via `corepack`). It contains three deployable products plus shared packages: + +- `sites/mainweb` (workspace name `web`) — the main Next.js full‑stack app: public marketing site **and** an authenticated member Portal (auth, tRPC, Drizzle/Postgres, Stripe). Dev server on **port 3001**. +- `sites/hacklytics2027` — static Next.js hackathon marketing site. Dev server on **port 3000**. +- `sites/discordBot` (`@query/discord-bot`) — standalone Discord.js bot (no HTTP port). + +Standard scripts live in the root `package.json` (`dev`, `build`, `lint`, `typecheck`, `test`) and per‑package `package.json` files. `pnpm dev` runs every app in parallel via Turbo; use `pnpm --filter web dev` / `pnpm --filter hacklytics2027 dev` to run one. Tests: `pnpm test` (Vitest, `packages/api`). Lint: `pnpm lint`. + +### Dependencies are auto-installed +The startup/update script runs `pnpm install`. You normally do **not** need to reinstall. + +### Postgres runs in Docker, started manually (no systemd on the VM) +The DB is **required** for the mainweb portal and any DB/auth flows. Docker is pre-installed but the daemon is **not** started automatically: + +1. Start the Docker daemon (background): `sudo dockerd > /tmp/dockerd.log 2>&1 &` then wait a few seconds. The daemon is configured for `fuse-overlayfs` with the containerd snapshotter disabled (see `/etc/docker/daemon.json`) — required in this VM; don't change it. +2. Start Postgres: `sudo docker compose up -d db` (root `docker-compose.yml`; Postgres 15 on host port **5433**, db `app_db`, user/pass `postgres`/`postgres`). +3. Apply the schema: `cd packages/db && pnpm drizzle-kit push --force`. See gotcha below — `pnpm --filter @query/db migrate:push` is interactive and **silently no-ops in a non-interactive shell**, so always pass `--force`. It is idempotent. + +### CRITICAL: injected Secrets point at PRODUCTION — override to local before running dev servers +The user's Cursor Secrets are injected into the VM environment and set `DATABASE_URL`, `EMAIL_SERVER_*` (real Gmail SMTP), `STRIPE_*` (LIVE keys), `NEXTAUTH_URL`, etc. to **production/live** values. **Next.js does NOT override already-defined `process.env` values with `.env`/`.env.local`**, so a dev server started with the ambient environment will talk to the **production database, send real emails, and use live Stripe keys**. + +To develop safely against local services, launch the mainweb dev server with explicit local overrides so they take precedence over the injected env, e.g.: + +```bash +DATABASE_URL='postgresql://postgres:postgres@localhost:5433/app_db' \ +AUTH_URL='http://localhost:3001' NEXTAUTH_URL='http://localhost:3001' \ +EMAIL_SERVER_HOST='127.0.0.1' EMAIL_SERVER_PORT='1025' EMAIL_SERVER_USER='' EMAIL_SERVER_PASSWORD='' \ +EMAIL_FROM='noreply@datasciencegt.org' \ +STRIPE_SECRET_KEY='' NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY='' STRIPE_WEBHOOK_SECRET='' \ +pnpm --filter web dev +``` + +`AUTH_SECRET`/`NEXTAUTH_SECRET` from the injected env are fine to reuse. `sites/mainweb/.env.local` and root `.env` also hold local values, but remember they are ignored for any variable already present in `process.env`, which is why the inline overrides above are needed. `packages/db` tooling (drizzle-kit) reads the root `.env` directly, so keep `DATABASE_URL` local there too. + +### Email (6-digit code) login without real email +The portal login is a 6-digit code flow. The code is written to the Postgres `verificationToken` table as `custom:` before the email is sent. For local dev you can either (a) run a local SMTP catcher on `127.0.0.1:1025` (`python3 -m aiosmtpd -n -l 127.0.0.1:1025`, pre-installed) and point `EMAIL_SERVER_HOST/PORT` at it, or (b) simply read the latest code from the DB: +`sudo docker exec monorepo-postgres psql -U postgres -d app_db -tAc "SELECT replace(token,'custom:','') FROM \"verificationToken\" WHERE token LIKE 'custom:%' ORDER BY expires DESC LIMIT 1;"` + +### Discord bot needs live credentials +`sites/discordBot` validates its env with zod and hard-fails unless real `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_ID`, `GUILD_ID`, role IDs, and `NOTION_TOKEN`/`NOTION_GUIDE_PAGE_ID` are set. It cannot run locally without those and is optional for portal/site work. + +### Stale docs +`GCP_SETUP.md` references `pnpm dev:full` and `scripts/sync-secrets.sh`, **neither of which exists**. Use `pnpm dev` / per-filter commands instead. diff --git a/sites/hacklytics2027/app/fonts/Archivo-100900.woff2 b/sites/hacklytics2027/app/fonts/Archivo-100900.woff2 new file mode 100644 index 00000000..716d4abd Binary files /dev/null and b/sites/hacklytics2027/app/fonts/Archivo-100900.woff2 differ diff --git a/sites/hacklytics2027/app/fonts/IBMPlexMono-400.woff2 b/sites/hacklytics2027/app/fonts/IBMPlexMono-400.woff2 new file mode 100644 index 00000000..0804aaff Binary files /dev/null and b/sites/hacklytics2027/app/fonts/IBMPlexMono-400.woff2 differ diff --git a/sites/hacklytics2027/app/fonts/IBMPlexMono-500.woff2 b/sites/hacklytics2027/app/fonts/IBMPlexMono-500.woff2 new file mode 100644 index 00000000..090f82f7 Binary files /dev/null and b/sites/hacklytics2027/app/fonts/IBMPlexMono-500.woff2 differ diff --git a/sites/hacklytics2027/app/fonts/IBMPlexMono-600.woff2 b/sites/hacklytics2027/app/fonts/IBMPlexMono-600.woff2 new file mode 100644 index 00000000..67aeeb01 Binary files /dev/null and b/sites/hacklytics2027/app/fonts/IBMPlexMono-600.woff2 differ diff --git a/sites/hacklytics2027/app/globals.css b/sites/hacklytics2027/app/globals.css index 45511348..97101630 100644 --- a/sites/hacklytics2027/app/globals.css +++ b/sites/hacklytics2027/app/globals.css @@ -1,146 +1,185 @@ -/* app/globals.css — Digital Bloom Theme */ +/* app/globals.css — Hacklytics 2027 · Swiss editorial system + Paper stock, ink type, one gold rule. No glass, no glow, no blur. */ @import "tailwindcss"; @theme inline { - --color-bloom-pink: var(--bloom-pink); - --color-bloom-cyan: var(--bloom-cyan); - --color-bloom-lime: var(--bloom-lime); - --color-bloom-purple: var(--bloom-purple); - --color-gridline: var(--color-gridline); + --color-paper: var(--paper); + --color-paper-2: var(--paper-2); + --color-ink: var(--ink); + --color-ink-soft: var(--ink-soft); + --color-navy: var(--navy); + --color-gold: var(--gold); + --color-gold-hot: var(--gold-hot); + --color-gold-deep: var(--gold-deep); + --color-rule: var(--rule); + + /* legacy tokens — kept so unmounted components don't render invisible */ + --color-bloom-pink: var(--gold-hot); + --color-bloom-cyan: var(--navy); + --color-bloom-lime: var(--gold); + --color-bloom-purple: var(--navy); } :root { - --color-bg: #030305; - --color-text: #f0f0f2; - --color-gridline: rgba(255, 255, 255, 0.05); + --paper: #f2efe9; + --paper-2: #e6e1d6; + --ink: #101010; + --ink-soft: #55524b; + --navy: #003057; + --gold: #b3a369; + --gold-hot: #eaaa00; /* on ink only — 1.8:1 on paper */ + --gold-deep: #7a5c00; /* gold for type on paper — 5.4:1 */ + --rule: rgba(16, 16, 16, 0.16); + --rule-strong: rgba(16, 16, 16, 0.85); - --bloom-pink: #ff2d78; - --bloom-cyan: #00e5ff; - --bloom-lime: #c8ff00; - --bloom-purple: #9b00ff; + --font-family-mono: var(--font-mono), ui-monospace, monospace; + --font-family-sans: var(--font-sans), "Helvetica Neue", Arial, sans-serif; - --font-family-mono: "Roboto Mono", monospace; - --font-family-sans: "Space Grotesk", sans-serif; + --navbar-height: 58px; + --gutter: 1.25rem; +} - --navbar-height: 72px; +@media (min-width: 768px) { + :root { --gutter: 2.5rem; --navbar-height: 64px; } } -/* ─── Base ─── */ -html { scroll-behavior: smooth; } +/* ─── Base ─────────────────────────────────────────────────────────────── */ +html { + scroll-behavior: smooth; + background: var(--paper); +} body { - background-color: var(--color-bg); - color: var(--color-text); + background-color: var(--paper); + color: var(--ink); font-family: var(--font-family-sans); overflow-x: hidden; -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } ::selection { - background-color: var(--bloom-cyan); - color: var(--color-bg); + background-color: var(--ink); + color: var(--paper); } -/* ─── Typography ─── */ -.font-display { - font-family: var(--font-family-sans); - font-weight: 700; - letter-spacing: -0.04em; - line-height: 0.9; +/* Ink ring + paper halo, so focus stays visible on paper AND on ink surfaces */ +:focus-visible { + outline: 2px solid var(--ink); + outline-offset: 2px; + box-shadow: 0 0 0 4px var(--paper); } -/* ─── Glass Surfaces — used sparingly ─── */ -.glass { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); +/* ─── Type ─────────────────────────────────────────────────────────────── */ +.display { + font-family: var(--font-family-sans); + font-weight: 800; + text-transform: uppercase; + letter-spacing: -0.045em; + line-height: 0.82; + font-feature-settings: "ss01"; } -/* Navbar glass — slightly more opaque */ -.glass-nav { - background: rgba(5, 5, 8, 0.85); - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); +.mono-label { + font-family: var(--font-family-mono); + font-weight: 500; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.12em; + line-height: 1; } -/* ─── Glow utilities — used intentionally, not everywhere ─── */ -.glow-pink { box-shadow: 0 0 32px rgba(255, 45, 120, 0.35); } -.glow-cyan { box-shadow: 0 0 32px rgba(0, 229, 255, 0.35); } -.glow-lime { box-shadow: 0 0 32px rgba(200, 255, 0, 0.3); } -.glow-purple { box-shadow: 0 0 32px rgba(155, 0, 255, 0.3); } +.tabular { font-variant-numeric: tabular-nums; } -.text-glow-cyan { - text-shadow: 0 0 20px rgba(0, 229, 255, 0.5); -} -.text-glow-pink { - text-shadow: 0 0 20px rgba(255, 45, 120, 0.5); +.lede { + font-size: clamp(1.125rem, 1.6vw, 1.5rem); + line-height: 1.45; + letter-spacing: -0.012em; } -/* ─── Layout ─── */ -.section-wrap { - padding-left: 1.5rem; - padding-right: 1.5rem; +.dropcap::first-letter { + float: left; + font-weight: 800; + font-size: 4.6em; + line-height: 0.74; + padding: 0.06em 0.1em 0 0; + color: var(--ink); } -@media (min-width: 768px) { - .section-wrap { padding-left: 3rem; padding-right: 3rem; } -} -@media (min-width: 1280px) { - .section-wrap { padding-left: 5rem; padding-right: 5rem; } -} - +/* ─── Rules & structure ────────────────────────────────────────────────── */ +.rule-t { border-top: 1px solid var(--rule); } +.rule-b { border-bottom: 1px solid var(--rule); } +.rule-heavy-t { border-top: 2px solid var(--ink); } +.rule-heavy-b { border-bottom: 2px solid var(--ink); } -/* ─── Backward-compat shims for unrewritten components ─── */ -.glass-panel { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-radius: 1rem; +.wrap { + width: 100%; + margin-inline: auto; + max-width: 88rem; + padding-inline: var(--gutter); +} + +/* Visible column rules — the page shows its own grid */ +.gridlines { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: repeating-linear-gradient( + to right, + var(--rule) 0 1px, + transparent 1px calc(100% / 6) + ); + opacity: 0.5; } -.glass-card { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 0.75rem; +@media (max-width: 767px) { .gridlines { display: none; } } + +.section-anchor { + scroll-margin-top: calc(var(--navbar-height) + 1px); + content-visibility: auto; + contain-intrinsic-size: auto 700px; } -.hover-bloom-glow:hover { - border-color: rgba(0, 229, 255, 0.4); - box-shadow: 0 0 20px rgba(0, 229, 255, 0.2); + +/* ─── Invert-on-hover — the one interaction motif ──────────────────────── */ +.invert-hover { + transition: background-color 140ms linear, color 140ms linear; } -.bloom-text-glow { - text-shadow: 0 0 20px rgba(0, 229, 255, 0.5); +.invert-hover:hover { + background-color: var(--ink); + color: var(--paper); } - -/* ─── Accent line — thin colored rule used for section punctuation ─── */ -.accent-line { - display: block; - width: 2.5rem; - height: 2px; - border-radius: 1px; +/* ─── Ticker ───────────────────────────────────────────────────────────── */ +@keyframes ticker-scroll { + from { transform: translate3d(0, 0, 0); } + to { transform: translate3d(-50%, 0, 0); } } - -/* ─── Section anchor offset for fixed navbar ─── */ -.section-anchor { - scroll-margin-top: calc(var(--navbar-height) + 1.5rem); - content-visibility: auto; - contain-intrinsic-size: auto 600px; +.ticker-track { + display: flex; + width: max-content; + animation: ticker-scroll 38s linear infinite; } -/* ─── Divider ─── */ -.bloom-divider { - width: 100%; - height: 1px; - background: linear-gradient( - to right, - transparent, - rgba(255, 255, 255, 0.08) 20%, - rgba(255, 255, 255, 0.08) 80%, - transparent - ); - margin: 0; +/* ─── Scroll reveal — progressive, no JS, no layout shift ──────────────── */ +@supports (animation-timeline: view()) { + @media (prefers-reduced-motion: no-preference) { + .reveal { + animation: reveal-in linear both; + animation-timeline: view(); + animation-range: entry 5% cover 22%; + } + @keyframes reveal-in { + from { opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: none; } + } + } } - +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .ticker-track { animation: none; } + *, *::before, *::after { + animation-duration: 0.001ms !important; + transition-duration: 0.001ms !important; + } +} diff --git a/sites/hacklytics2027/app/icon.svg b/sites/hacklytics2027/app/icon.svg new file mode 100644 index 00000000..b85c88fe --- /dev/null +++ b/sites/hacklytics2027/app/icon.svg @@ -0,0 +1,5 @@ + + + + H + diff --git a/sites/hacklytics2027/app/layout.tsx b/sites/hacklytics2027/app/layout.tsx index 025ee18b..fdd99d76 100644 --- a/sites/hacklytics2027/app/layout.tsx +++ b/sites/hacklytics2027/app/layout.tsx @@ -1,43 +1,51 @@ import "./globals.css"; import type { Metadata, Viewport } from "next"; -import { Roboto_Mono, Space_Grotesk } from "next/font/google"; +import localFont from "next/font/local"; import Navbar from "../components/Navbar"; import ServiceWorkerRegistrar from "../components/ServiceWorkerRegistrar"; import Footer from "../components/Footer"; -const robotoMono = Roboto_Mono({ - subsets: ["latin"], - weight: ["400", "700"], - variable: "--font-mono", +// Self-hosted so the build never depends on reaching Google Fonts. +// Latin woff2 subsets pulled from Google Fonts; both faces are OFL. +const archivo = localFont({ + src: [{ path: "./fonts/Archivo-100900.woff2", weight: "100 900", style: "normal" }], + variable: "--font-sans", + display: "swap", + fallback: ["Helvetica Neue", "Arial", "sans-serif"], }); -const spaceGrotesk = Space_Grotesk({ - subsets: ["latin"], - weight: ["400", "500", "700"], - variable: "--font-sans", +const plexMono = localFont({ + src: [ + { path: "./fonts/IBMPlexMono-400.woff2", weight: "400", style: "normal" }, + { path: "./fonts/IBMPlexMono-500.woff2", weight: "500", style: "normal" }, + { path: "./fonts/IBMPlexMono-600.woff2", weight: "600", style: "normal" }, + ], + variable: "--font-mono", + display: "swap", + fallback: ["ui-monospace", "monospace"], }); export const viewport: Viewport = { - themeColor: "#050508", + themeColor: "#f2efe9", }; export const metadata: Metadata = { - title: "Hacklytics 2027: Digital Bloom | Premier Data Science Hackathon", + title: "Hacklytics 2027 | Data Science @ Georgia Tech", description: - "Join Data Science @ GT for Hacklytics 2027, the premier data science and AI hackathon in the Southeast. 36 hours of coding, prizes, and networking in Atlanta.", + "Hacklytics 2027 — 36 hours of data science and AI at Georgia Tech. Feb 26–28, 2027, Klaus Advanced Computing Building, Atlanta. Free to enter, 1,000+ hackers.", keywords: ["hackathon", "data science", "machine learning", "AI", "Georgia Tech", "Atlanta", "coding", "competition"], authors: [{ name: "Data Science @ GT" }], openGraph: { - title: "Hacklytics 2027: Digital Bloom", - description: "The premier data science hackathon. 36 hours. Join 1,000+ hackers in Atlanta.", + title: "Hacklytics 2027", + description: "36 hours. 1,000 hackers. Feb 26–28, 2027 at Georgia Tech.", url: "https://hacklytics.io", siteName: "Hacklytics", images: [ { - url: "/og-image.jpg", + url: "/og-image.png", width: 1200, height: 630, - alt: "Hacklytics 2027 Digital Bloom", + alt: "Hacklytics 2027 — Feb 26–28, Georgia Tech", }, ], type: "website", @@ -45,12 +53,11 @@ export const metadata: Metadata = { }, twitter: { card: "summary_large_image", - title: "Hacklytics 2027: Digital Bloom", + title: "Hacklytics 2027", description: "The premier data science hackathon at Georgia Tech.", - images: ["/og-image.jpg"], + images: ["/og-image.png"], creator: "@datasciencegt", }, - icons: { icon: "/favicon.ico" }, metadataBase: new URL("https://hacklytics.io"), }; @@ -59,7 +66,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) const jsonLd = { "@context": "https://schema.org", "@type": "Event", - name: "Hacklytics 2027: Digital Bloom", + name: "Hacklytics 2027", startDate: "2027-02-26T17:00:00-05:00", endDate: "2027-02-28T16:00:00-05:00", eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", @@ -77,7 +84,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } }, image: [ - "https://hacklytics.io/og-image.jpg" + "https://hacklytics.io/og-image.png" ], description: "Data Science @ GT — The premier data science hackathon in the Southeast. 36 hours of coding, data science, and AI.", offers: { @@ -104,9 +111,15 @@ export default function RootLayout({ children }: { children: React.ReactNode }) /> + + Skip to content + {children}