diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfe7793..b3f7420 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ name: exploretech.la Continuous Integration -# Verifies every pull request targeting master. Runs the same install, test, -# image-manifest check (via prebuild) and production build that deployment uses, -# without any access to deployment secrets. +# Verifies every pull request targeting master. Runs the same install, checks, +# image-manifest check (via prebuild) and production build that deployment +# uses, plus the browser smoke suite, without any access to deployment secrets. on: pull_request: branches: [master] @@ -20,7 +20,7 @@ concurrency: jobs: verify: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 25 steps: - name: Check out the repository @@ -38,6 +38,20 @@ jobs: - name: Install packages run: npm ci + - name: Check types + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Check formatting + run: npm run format:check + + # References between people, teams and event content, and the assets + # they point at. Loads the content modules; makes no network requests. + - name: Check content + run: npm run content:check + - name: Run the tests run: npm test @@ -45,3 +59,25 @@ jobs: # verified against scripts/image-sources.json before bundling. - name: Build the site run: npm run build + + # Only Chromium: the smoke suite gates behaviour, not cross-engine + # rendering. Browsers are not in the npm cache, so this is a real + # download on every run. + - name: Install the Chromium test browser + run: npx playwright install --with-deps chromium + + # Serves the build through scripts/serve-built-site.cjs, which reproduces + # GitHub Pages' 404-to-root restoration. + - name: Run the browser smoke suite + run: npm run test:browser + + - name: Upload the browser smoke report + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-report + path: | + playwright-report + test-results + retention-days: 7 + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 8b31b89..e5086d2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ # testing /coverage +/playwright-report +/test-results # production /build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6852267 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules/ +build/ +handoffs/ +playwright-report/ +test-results/ +src/static/ +src/constants/optimizedImages.ts +src/image.png +package-lock.json diff --git a/README.md b/README.md index 0eddf7c..50fe8ec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # exploretech.la -The React website for [exploretech.la](https://www.exploretech.la/), built with Vite and Dart Sass and hosted on GitHub Pages. +The website for [exploretech.la](https://www.exploretech.la/): a React single-page app written in TypeScript, built with Vite and Tailwind, and hosted on GitHub Pages. ## Development @@ -13,43 +13,85 @@ npm ci npm start ``` -The development server runs at `http://127.0.0.1:3000`. It fails clearly if that port is already occupied. JSX and stylesheet edits update through Vite's development server. +The development server runs at `http://127.0.0.1:3000` and fails clearly if that port is occupied. -React 16.13 uses classic JSX in both application transforms and dependency scanning. The development config removes the plugin's automatic-runtime pre-bundles, which do not exist in that React version. +## Where things live -## Checks and production builds +| Path | Holds | Edit it when | +| --- | --- | --- | +| `src/content/people.ts` | One profile per person: name, portrait, profile link | Someone joins, leaves, or changes their photo or link | +| `src/content/teams.ts` | Ordered team sections and the role each person holds in them | A roster or job title changes | +| `src/content/speakers.ts` | Ordered historical speaking roles referencing the people registry | A past speaker's displayed title or order changes | +| `src/content/events/` | One module per event year, plus `index.ts` listing them | A schedule, workshop, map, waiver or FAQ changes | +| `src/content/sections.ts` | Home page section ids used by header hash links | A home page anchor is added or renamed | +| `src/features/` | Page rendering and feature-specific home or Ignite copy | A page layout or its local copy changes | +| `src/components/` | Shared UI and media only | Something is genuinely used by more than one feature | +| `src/styles/` | `theme.css` entry, tokens, reset, and per-area layers | The design changes | +| `src/constants/optimizedImages.ts` | Generated image map — never edit by hand | Never; run `npm run images` | +| `src/static/` | Original photographs and documents, and generated WebP output | You add a source image or document | +| `scripts/` | Image pipeline, content check, browser smoke, test server, parity tools | Tooling changes | +| `src/app/` | Entry, routes, analytics and scroll effects | A route is added or removed | -```sh -npm test # Run the regression suite once -npm run test:watch # Watch tests while developing -npm run images:check # Check generated image integrity and budgets -npm run build # Check images, then build into build/ -npm run preview # Serve the production build locally +Content is central on purpose: authors change `src/content` without knowing which component renders it, and features read content without owning it. + +Home carousel and sponsor records live in `src/features/home/content/`. Team and speaker identities always live in the shared people registry. + +## Contributor tasks + +### Update a person + +Edit the one entry in `src/content/people.ts`. Every team that lists them picks the change up. + +```ts +"sandra-pan": { + name: "Sandra Pan", + image: images["team/leadership/sandra-pan.jpg"].src, + link: "https://www.linkedin.com/in/sandra-pan-b994802aa", +}, ``` -Tests use Vitest and jsdom. Analytics tests use a dummy ID and make no provider requests. +A person with no portrait simply omits `image`; their card still renders with name and role. + +### Change a roster or a role -The build/test tooling is modernized independently of the UI libraries. React 16, React Router 5, and Bootstrap 4 remain pinned to their previously deployed versions. Dart Sass currently warns about legacy imports and Bootstrap APIs; those warnings remain visible and belong to a subsequent stylesheet/UI-library upgrade. +Edit `src/content/teams.ts`. Each member is a reference plus the title held *in that section*, so the same person can appear in several teams with different titles. Card order follows the array; tab order follows the section order. + +```ts +{ personId: "benjamin-garcia", title: "Web Dev Lead" }, +``` -## Updating images +`personId` is checked against the registry at compile time and again by `npm run content:check`. -Optimized images are checked in. Normal installs, tests, CI, and builds do not need native image-generation tools. +### Change an event -To regenerate images, install `cwebp`, `ffmpeg`, and `ffprobe`. On macOS: +Edit the year module under `src/content/events/` (`2022.ts` is served at `/resources`, `2023.ts` at `/resources2023`, `2026.ts` at `/resources2026`, and `2021.ts` is the registration page at `/register`). Schedules, workshop cards, maps, waivers, FAQ entries and the feedback link are all data there. Archived workshop videos hold a bare YouTube id, never a watch URL, and always render behind the click-to-load facade. + +### Add an event year + +1. Add `src/content/events/.ts` exporting an `EventContent`. +2. Register it in `src/content/events/index.ts`: the module in `EVENTS`, and its path, year and menu label in `EVENT_ROUTES`. + +That is the whole change. The router and the Resources menu are generated from `EVENT_ROUTES`, which is also why the menu label lives there: `/resources` serves the 2022 content under the label "exploretech 2021" it shipped with. + +Old years stay published. They are history, not dead code. + +### Add or replace an image + +Optimized images are checked in, so normal installs, tests, CI and builds need no native tools. Regeneration needs `cwebp`, `ffmpeg` and `ffprobe`: ```sh brew install webp ffmpeg ``` -1. Keep the original image under `src/static/`. -2. Add its path and profile to `scripts/image-sources.json`. Portraits use a square crop up to 320px for the 160px team cards. Content images get responsive widths without enlargement. Use a `focus` override when the default crop cuts a face. -3. Run `npm run images`. It applies EXIF orientation, generates WebP files, and updates the manifest and `src/constants/optimizedImages.js`. -4. Import the generated map rather than an original photograph: +1. Put the original under `src/static/`. +2. Add its path and profile to `scripts/image-sources.json`. Portraits are cropped square up to 320px for the 160px team cards; content images get responsive widths without enlargement. Add a `focus` override when the default crop cuts a face. +3. Run `npm run images`. It applies EXIF orientation, writes the WebP variants, and regenerates the manifest and `src/constants/optimizedImages.ts`. +4. Use the generated map rather than the original file: -```jsx -import images from "constants/optimizedImages"; +```tsx +import images from "../../constants/optimizedImages"; // relative to your module -// Team roster entries retain a plain image URL. +// A roster portrait needs only the URL. const portrait = images["team/leadership/sandra-pan.jpg"].src; // Content images carry responsive sources and intrinsic dimensions. @@ -62,30 +104,78 @@ const portrait = images["team/leadership/sandra-pan.jpg"].src; />; ``` -5. Run `npm run images:check`, then inspect the affected pages at phone and desktop widths. Check faces, transparent logos, and images revealed by scrolling. +5. Run `npm run images:check`, then look at the affected pages at phone and desktop widths: faces, transparent logos, and images revealed by scrolling. + +The check validates source and output hashes, recipe freshness, dimensions, byte budgets, generated-map consistency, and raw raster imports in the configured sources. Keep the originals; they are the regeneration inputs. Use `npm run images -- --only ` for a narrow update, `npm run images -- --force` after changing tools, and `npm run images -- --help` for profile budgets. + +## Checks + +```sh +npm run typecheck # TypeScript, strict, no emit +npm run lint # ESLint +npm run format:check # Prettier (npm run format writes) +npm test # Vitest unit and component regressions +npm run content:check # References between people, teams, events and their assets +npm run images:check # Generated image integrity and budgets +npm run build # Checks images, then builds into build/ +npm run preview # Serve the production build locally +``` + +`npm run content:check` loads the content modules through Vite, so imported images and documents are resolved the way the app resolves them. It reports missing people, duplicate profiles or roles, shared profile links, missing assets, malformed URLs and video ids, and duplicate routes or workshop titles. It makes no network requests: a syntactically valid but dead external link still passes. + +### Browser smoke + +```sh +npx playwright install chromium # once per machine +npm run build +npm run test:browser +``` + +The suite (`scripts/browser-smoke.spec.ts`, configured in `playwright.config.ts`) runs Chromium against the **built** site served by `scripts/serve-built-site.cjs`, which answers unknown paths with `404.html` exactly as GitHub Pages does. Point it at another build with `SMOKE_ROOT=/path/to/build`, or move it off port 4390 with `SMOKE_PORT`. -The check validates source/output hashes, recipe freshness, dimensions, byte budgets, generated-map consistency, and raw raster imports in the configured JavaScript sources. Do not edit generated files by hand. +Every cross-origin request is aborted and recorded, so the suite is offline and reaches no analytics or video vendor. It covers: -Use `npm run images -- --only ` for a narrow update, or `npm run images -- --force` after changing encoder behavior or tools. Run `npm run images -- --help` for profile budgets and crop settings. Original files remain regeneration inputs; PDF and map links are excluded from this pipeline. +- all 13 routes, the `/our_team` alias, its trailing slash, and the not-found page and title; +- deep links surviving the Pages 404-to-root restore with their query and hash intact; +- the phone menu opening, navigating, closing, and still landing on a hash section; +- a repeated same-hash click scrolling back; +- a pushed route starting at the top while Back leaves scrolling to the browser; +- all seven team sections, eager-then-lazy portraits, and cards with no portrait; +- archived videos requesting nothing before activation, keyboard activation, and unchanged geometry after it; +- every shared PDF and map link answering 200; +- the carousel holding the current slide until a slow one loads, and skipping a broken one without locking up. + +Add a case when you fix a bug a user could see; keep it in this one file. + +`npm run visual:parity` captures the screenshot and geometry matrix used to compare a change against a saved baseline build. It is a review tool, not part of CI. + +Use the same browser version for both captures. Serve the saved baseline and candidate builds with `scripts/serve-built-site.cjs` on separate ports, then run: + +```sh +SITE_URL=http://127.0.0.1:4391 OUTPUT_DIR=/tmp/site-before npm run visual:parity +SITE_URL=http://127.0.0.1:4392 OUTPUT_DIR=/tmp/site-after BASELINE_DIR=/tmp/site-before npm run visual:parity +node scripts/asset-parity.cjs /path/to/baseline/build build +``` + +`WIDTHS=390,768,1440` narrows a capture. The default includes every Bootstrap-era breakpoint and its adjacent pixels. Read [the UI canon](docs/ui-canon.md) before changing shared controls or styling. ## CI and deployment -- Pull requests targeting **`master`** run `.github/workflows/ci.yml`: `npm ci`, `npm test`, and `npm run build`, including its image checks. This job has read-only repository access, receives no deployment secrets, and never publishes the site. -- Pushes to **`master`** run `.github/workflows/deploy.yml`: install, test, build, then publish the verified `build/` directory to **`gh-pages`**. The deploy token is available only to the publishing step, not to installation, tests, or build tools. -- Both workflows use the Node version in `.nvmrc`, the committed lockfile, npm caching, and SHA-pinned official checkout/setup actions. -- The existing `DEPLOY_ACCESS_TOKEN` and `GOOGLE_ANALYTICS_TRACKING_ID` repository secret names are unchanged. Only the build-time environment variable is renamed to `VITE_GOOGLE_ANALYTICS_TRACKING_ID`. -- Branch protection and review requirements are unchanged. The PR check is named `verify`; an administrator can make it required in branch protection if desired. +- Pull requests targeting **`master`** run `.github/workflows/ci.yml` as the `verify` job: `npm ci`, typecheck, lint, formatting, content check, tests, `npm run build` including its image checks, then Chromium smoke against that build. On failure it uploads the Playwright traces and screenshots. The job has read-only repository access, receives no deployment secrets, and never publishes the site. +- Pushes to **`master`** run `.github/workflows/deploy.yml`: install, test, build, then publish the verified `build/` directory to **`gh-pages`**. The deploy token is available only to the publishing step. +- Both workflows use the Node version in `.nvmrc`, the committed lockfile, npm caching, and SHA-pinned official actions. +- The `DEPLOY_ACCESS_TOKEN` and `GOOGLE_ANALYTICS_TRACKING_ID` secret names are unchanged; the build-time client variable is `VITE_GOOGLE_ANALYTICS_TRACKING_ID`. +- Branch protection and review requirements are unchanged. An administrator can make `verify` a required check. -`npm run deploy` remains available for an authorized manual release and builds before publishing. Do not run it just to preview changes. +`npm run deploy` remains available for an authorized manual release and builds before publishing. Do not run it to preview changes. ## Runtime and hosting conventions -- Root `index.html` is the Vite entry. It retains the GitHub Pages query-to-route restoration script; `public/404.html` and `public/CNAME` are copied unchanged. -- Public media retain the existing `static/media/..` URLs so shared PDF/map links and cached images survive the bundler migration. JavaScript and CSS filenames may change. -- `components/`, `constants/`, `static/`, and `util/` imports resolve from `src/`; `jsconfig.json` keeps the same editor lookup root. -- `VITE_GOOGLE_ANALYTICS_TRACKING_ID` is optional and is used only in production builds. Vite-prefixed variables are public client configuration, not a place for private credentials. Analytics keeps the existing tracker and connected GA4 behavior, queues early commands, and defers vendor loading until idle, interaction, or its deadline. -- Fonts load from an HTML stylesheet link rather than a nested CSS import, preventing an unstyled startup transition in WebKit. -- Initially visible images stay eager; offscreen content uses lazy loading with reserved dimensions. The carousel waits for selected images and skips failures. Archived videos load only after activation. -- GitHub Pages' cache headers and the hosting configuration are unchanged. Longer immutable caching would require a separate hosting/CDN decision. - -npm install-script decisions are version-scoped in `package.json`. The optional watcher source-build hooks are denied; supported platforms use their prebuilt packages. Review `npm install-scripts ls` before changing these decisions for a dependency update. +- Root `index.html` is the Vite entry and keeps the GitHub Pages query-to-route restoration script; `public/404.html` and `public/CNAME` are copied unchanged. +- Public media keep their `static/media/..` URLs so shared PDF, map and image links survive a rebuild. JavaScript and CSS filenames may change. +- Styling is Tailwind utilities plus the layered stylesheets in `src/styles`, entered from `theme.css`. Tailwind's Preflight is deliberately not imported: the site's geometry depends on the reset it shipped with, which `src/styles/reset.css` carries explicitly. Do not add `@import "tailwindcss"` back. +- Fonts load from a stylesheet link in the HTML rather than a nested CSS import, which prevents an unstyled startup flash in WebKit. +- Initially visible images stay eager; offscreen content is lazy with reserved dimensions. The carousel waits for a selected image and skips failures. Archived videos load only after activation. +- `VITE_GOOGLE_ANALYTICS_TRACKING_ID` is optional and used only in production builds. Vite-prefixed variables are public client configuration, not a place for credentials. Analytics keeps the existing tracker, queues early commands, and defers vendor loading until idle, interaction, or its deadline. +- GitHub Pages' cache headers and hosting configuration are unchanged. Longer immutable caching would need a separate hosting decision. +- npm install-script decisions are version-scoped in `package.json`; the optional watcher source builds are denied and supported platforms use prebuilt packages. Review `npm install-scripts ls` before changing them. diff --git a/docs/refactor-plan.md b/docs/refactor-plan.md new file mode 100644 index 0000000..083c255 --- /dev/null +++ b/docs/refactor-plan.md @@ -0,0 +1,70 @@ +# Contributor refactor plan and acceptance ledger + +## Mission + +Centralize people, role assignments, and annual event content; organize typed feature renderers; replace the old runtime and styling dependencies without redesigning the site. No CMS, backend, database, hosting migration, deployment, merge, branch-protection change, or secret change is included. + +Baseline: `origin/master` at `f435aca007fbaf3bc2726c8239bdd784d56b8d6d`, the independently verified PR #107 merge. Work started on `refactor-contributor-architecture`, not the already-merged modernization branch. The Node/Vite/Vitest modernization was retained. + +## Implementation sequence and decisions + +1. Fetch and verify master, preserve the user-owned image and local handoffs, build an immutable baseline, then capture screenshots and geometry before changing CSS. +2. Establish shared profile/role/event types, content ownership, brand tokens, and the observed breakpoint/reset contract. Parallel feature writers owned disjoint files; integration owned runtime, navigation, analytics, and configuration. +3. Centralize all 35 people in `content/people.ts`. The seven team sections contain 45 contextual assignments; `content/speakers.ts` contains eight historical speaking roles. Names, portraits, links, titles, order, and missing-photo slots remain unchanged. +4. Consolidate annual resources into `content/events/.ts` plus shared renderers. `/resources` deliberately retains its 2022 page content and "exploretech 2021" menu label. Registration remains the distinct 2021 two-day event. `EVENT_ROUTES` drives both routing and navigation; its keys determine valid event years. +5. Upgrade to stable React/DOM 19.3.0, Router 7.18.3, and Tailwind 4.3.3. TypeScript 6.0.3 is the latest stable compatible with typescript-eslint 8.70.0; TypeScript 7 is outside that linter's peer range. No forced peer resolution is used. +6. Replace Bootstrap's generic class API with the small `action`, `site-nav`, `site-menu`, `content-card`, and `resource-list` canon. Layout and dimensions use exact-value Tailwind utilities. Motion and state effects remain explicit CSS. Preflight is excluded. The former Sass cascade's missing tablet rules and width overrides were preserved rather than corrected into a redesign. +7. Remove old JS/JSX, Sass, copied annual components, unused content/dependencies, classic-JSX compatibility configuration, and unused scaffold files. Retain every image-generation input and public document. +8. Extend the existing CI job, add content checks and durable browser smoke, document contributor tasks and the [UI canon](ui-canon.md), then perform independent review and final verification. + +## Acceptance ledger + +| Requirement | State | Evidence | +| --- | --- | --- | +| Latest master and fresh branch | done | Fetch, branch creation, GitHub PR #107 merge verification | +| Baseline screenshots and geometry | done | 210 baseline cells at 14 routes and 15 widths, including breakpoint-adjacent pixels | +| One person profile and contextual assignments | done | `content/people.ts`, `teams.ts`, `speakers.ts`; 35 profiles, 53 total roles; content and browser checks | +| Shared annual data/rendering and archives | done | `content/events`, `features/events`, `features/registration`; preserved route text, image order, links and schedules | +| Feature and content ownership | done | README ownership table and edit-person/roster/event/image instructions | +| Strict TS and compatible stable runtime | done | No application JS/JSX; strict typecheck, lint, cold Vite startup and live JSX HMR without document reload | +| Tailwind cutover and small shared UI canon | done | No Bootstrap/React Bootstrap/Sass dependencies or application imports; semantic selectors, explicit reset/tokens, shared ActionLink/OutboundLink/Collapse | +| Proven dead code removed | done | Unreachable annual copies, inactive components, old constants, all Sass files and unused scaffold removed; original assets retained | +| Shipped image loading and regressions preserved | done | 12 Vitest tests, including the original failed/stale carousel and analytics assertions; browser slow/failing-image cases | +| Public binary URLs and bytes retained | done | `asset-parity.cjs`: all 102 public image/PDF/icon URLs and SHA-256 hashes unchanged; `404.html` and `CNAME` byte-identical | +| CI content checks and browser smoke | done | Existing read-only `verify` extended with types, lint, formatting, content, image/build and 34 Chromium cases; no deployment secrets | +| Contributor documentation | done | README, UI canon, this ledger, committed parity summary and comparison images | +| Clean install/types/lint/format/tests/images/build | done | `npm ci`, all listed checks, production build; npm audit reports zero vulnerabilities at verification time | +| Responsive and interaction parity | done | 210 final geometry/style/content cells; 34 WebKit plus 34 Firefox scenarios; 25 hover/focus/video/carousel states; autoplay, pause/resume, interrupted menu and keyboard probes | +| Analytics semantics retained | done | Dummy-ID build with real retained `analytics.js`: one tracker/script, resolved-only alias pageview, query changes, no hash-only pageview, correct outbound attribution; collection blocked | +| Independent findings resolved | done | Runtime and content/style reviews, followed by targeted approvals after the two fixes below | +| Reviewable pull request without merge/deploy | done | [PR #108](https://github.com/exploretech-la/website/pull/108) is open against master; no merge or deployment performed | + +## Demonstrated regressions and fixes + +- Tailwind's `collapse` utility initially hid open FAQ answers. Renaming the component's state classes to `disclosure` and `disclosure-transition` removed the collision without an override or compatibility layer. +- The first navigation implementation wrapped dropdown keyboard selection. The baseline clamps at the first/last item and does not open on ArrowUp. A new browser regression failed against that candidate build and passes after the correction; the ten-step baseline/candidate keyboard sequences now match. +- Independent runtime review found that the Back-navigation test replaced its spy array while the spy retained the old one. The test now clears the array in place and first proves it observed pushed-route scrolling. Targeted re-review approved the fix. +- Independent content review found a registration schedule variant that was accepted by types but not rendered. The unused union and conditional were removed; registration now requires and always renders its two-day tuple. Targeted re-review approved the fix. + +## Verification results and limits + +Machine-readable results: [verification/refactor-parity.json](verification/refactor-parity.json). + +- Final geometry matrix: 210/210 passed. Maximum observed difference is 0.03125 CSS pixels against a 0.05px ceiling. Text, destinations, image order/loading attributes, and the sampled computed styles compare exactly. Implementation class names are recorded but not treated as a visual contract. +- Final PNG comparison: 210 pairs, no dimension changes, 121 byte-identical pairs, 142 with no flagged pixels. The largest flagged fraction is 0.005645%, using pixelmatch threshold 0.1 with antialiasing excluded. Screenshots are not universally pixel-identical. Sampled residual differences are in text rasterization; diff bounds and counts are in the committed summary. +- All 25 measured hover/focus/video/carousel states match. Slide motion has observed intermediate transforms and the same 600ms easing. Autoplay advances, pauses on hover, and resumes on both baseline and candidate. Interrupted menu operations settle correctly; intermediate samples depend on frame scheduling. +- WebKit and Firefox each pass 34 scenarios, including actual Back reading-position restoration. Chromium's durable test separately proves that the app does not override POP scrolling. +- CSS gzip changed from 28.23KB to 8.41KB after removing the unused resource-list active states. Application JS gzip changed from 99.60KB to 112.98KB with the newer runtime. These are build sizes, not field-performance claims. +- Browser viewports are emulated. Playwright WebKit is not native Safari or a physical iPhone. External links are syntax-checked, not all contacted. Analytics testing proves local vendor processing, not production-property ingestion. + +Before is on the left and after is on the right in these representative, unscaled viewport crops: + +- [Phone team comparison](verification/team-phone.png) +- [Tablet home comparison](verification/home-tablet.png) +- [Desktop resources comparison](verification/resources-desktop.png) + +The full local screenshot/JSON set and nonzero pixel-diff images remain under `/tmp/exploretech-refactor-baseline` and `/tmp/exploretech-refactor-candidate`. The README documents rerunning the capture and asset comparisons. Temporary migration and diagnostic scripts are not application dependencies. + +## Protected state + +`src/image.png` remains untracked and unchanged, with SHA-256 `901b37768de40e95b2718c8a350f5761d6d341f02f2e23c9337164ad95f5ae3b`. The local `/handoffs/` exclusion remains effective. Deployment workflow, hosting, secrets, and branch protection are unchanged. diff --git a/docs/ui-canon.md b/docs/ui-canon.md new file mode 100644 index 0000000..8a04c9e --- /dev/null +++ b/docs/ui-canon.md @@ -0,0 +1,26 @@ +# UI canon + +This is a styling-system migration, not a redesign. `src/styles/theme.css` is the only stylesheet entry. It loads Tailwind's theme and utilities, then explicit base and component layers. Preflight is excluded because it changes the site's heading, image, list, and button geometry. + +## Shared controls + +- `ActionLink` renders a native anchor and preserves the existing Space-key behavior of action links. Use `action` with one of the site's five existing color treatments: `action-info`, `action-primary`, `action-warning`, `action-outline`, or `action-inverse`. `action-large` is the existing large size. The definitions live in `styles/controls.css`; do not add another button palette in a feature. +- `OutboundLink` owns outbound analytics, email redaction through the analytics module, and same-window navigation completion. Its props are native anchor props plus `eventLabel`. +- `Collapse` owns the menu and FAQ height transition. `disclosure` and `disclosure-transition` avoid Tailwind's unrelated `collapse` visibility utility. The 350ms CSS transition and the previously shipped 300ms fallback remain distinct on purpose. +- `Header` owns the single navigation layout and dropdown behavior. `site-nav-*` and `site-menu-*` are internal selectors, not another component library. +- `content-card-*` and `resource-list-*` supply the shared static card structure. Features compose native elements rather than passing variant props through wrappers. +- `People` and `YoutubeEmbed` own reserved media geometry and loading behavior. The home carousel owns its slide classes and timing separately. + +## Values and breakpoints + +`styles/tokens.css` owns the brand colors and section spacing. Breakpoints are 576, 768, 992, and 1200px, not Tailwind's defaults. Existing feature queries include 575.98px and an inclusive 768px mobile team rule; those boundaries remain deliberate. + +Layout, sizing, spacing, and typography use Tailwind utilities through `@apply`. Exact existing values use arbitrary utilities where a default would change geometry. Motion, gradients, pseudo-elements, and state-specific effects remain ordinary CSS. Group utilities only when their properties do not override one another. + +The base reset and some control values derive from the former Bootstrap dependency. Their license is retained in `licenses/bootstrap.txt`; Bootstrap itself, its generic class API, React Bootstrap, and Sass are removed. + +## Verify a change + +Run the type, lint, content, image, build, and browser checks listed in the README. Compare phone, tablet, desktop, and both sides of affected breakpoints. A screenshot with motion disabled does not prove an animation: exercise the transition separately, including keyboard focus and interrupted open/close actions. + +The parity tool stores raw geometry, computed styles, content, links, image attributes, and screenshots. Its comparison ignores implementation class names and permits at most 0.05 CSS pixels of geometry rounding. It does not ignore differences in text, destinations, image order, loading policy, or computed style values. diff --git a/docs/verification/home-tablet.png b/docs/verification/home-tablet.png new file mode 100644 index 0000000..6f58614 Binary files /dev/null and b/docs/verification/home-tablet.png differ diff --git a/docs/verification/refactor-parity.json b/docs/verification/refactor-parity.json new file mode 100644 index 0000000..9728f82 --- /dev/null +++ b/docs/verification/refactor-parity.json @@ -0,0 +1,288 @@ +{ + "baselineCommit": "f435aca007fbaf3bc2726c8239bdd784d56b8d6d", + "runtime": { + "react": "19.3.0", + "react-dom": "19.3.0", + "react-icons": "5.7.0", + "react-router-dom": "7.18.3" + }, + "typescript": "6.0.3", + "geometry": { + "cells": 210, + "toleranceCssPx": 0.05, + "maxObservedCssPx": 0.03125, + "widths": [ + 320, + 390, + 575, + 576, + 577, + 767, + 768, + 769, + 991, + 992, + 993, + 1199, + 1200, + 1201, + 1440 + ], + "routes": [ + "/", + "/our_team/leadership", + "/our_team/content", + "/our_team/design", + "/our_team/operations", + "/our_team/external", + "/our_team/web-dev", + "/our_team/marketing", + "/ignite", + "/register", + "/resources", + "/resources2023", + "/resources2026", + "/not-found" + ] + }, + "pixels": { + "cells": 210, + "algorithm": "pixelmatch threshold 0.1, antialiasing excluded", + "byteIdentical": 121, + "zeroFlaggedPixels": 142, + "dimensionChanges": [], + "maximumFlaggedPercent": 0.005644476899090174 + }, + "checks": { + "unitTests": 12, + "chromiumSmoke": 34, + "webkitScenarios": 34, + "firefoxScenarios": 34, + "interactionStates": 25, + "publicBinaryUrls": 102, + "people": 35, + "teamAssignments": 45, + "speakerAssignments": 8, + "teams": 7, + "resourceEvents": 3, + "imageSources": 59, + "imageVariants": 86 + }, + "limitations": [ + "Emulated viewports and Playwright engines, not physical devices or native Safari.", + "Screenshots are not universally pixel-identical; raw snapshots and pixel-diff images retained in local verification artifacts.", + "Analytics uses a dummy ID and retained public analytics.js with collector traffic blocked; no production-property ingestion claim." + ], + "cells": [ + {"file":"chromium-320-_.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_register.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-320-_resources.png","maxGeometryDelta":0,"flaggedPixels":12,"flaggedRatio":0.000003116947884631369,"bounds":[246,8940,260,8948]}, + {"file":"chromium-320-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":147,"flaggedRatio":0.000039762399376785254,"bounds":[75,465,229,7199]}, + {"file":"chromium-320-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":117,"flaggedRatio":0.000028997144896502498,"bounds":[92,2758,255,9985]}, + {"file":"chromium-320-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_register.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-390-_resources.png","maxGeometryDelta":0,"flaggedPixels":13,"flaggedRatio":0.000002810567734682406,"bounds":[95,408,260,9124]}, + {"file":"chromium-390-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":138,"flaggedRatio":0.00003458906684713136,"bounds":[89,1540,274,7999]}, + {"file":"chromium-390-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":143,"flaggedRatio":0.00003370717656431942,"bounds":[44,404,290,8482]}, + {"file":"chromium-390-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-575-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":0.0000010715529481100485,"bounds":[226,2580,227,2581]}, + {"file":"chromium-575-_resources.png","maxGeometryDelta":0,"flaggedPixels":36,"flaggedRatio":0.000005426774347939145,"bounds":[181,332,486,9090]}, + {"file":"chromium-575-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":183,"flaggedRatio":0.00003864265050573305,"bounds":[120,336,442,6206]}, + {"file":"chromium-575-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":286,"flaggedRatio":0.000056444768990901735,"bounds":[108,1281,449,6710]}, + {"file":"chromium-575-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_.png","maxGeometryDelta":0,"flaggedPixels":30,"flaggedRatio":0.000011722559831945383,"bounds":[55,2753,378,2762]}, + {"file":"chromium-576-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":5,"flaggedRatio":0.000001822114936094785,"bounds":[222,2589,228,2596]}, + {"file":"chromium-576-_resources.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-576-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":107,"flaggedRatio":0.000023455036475869808,"bounds":[103,371,467,5784]}, + {"file":"chromium-576-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":73,"flaggedRatio":0.000014750478481274571,"bounds":[196,374,456,5782]}, + {"file":"chromium-576-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_.png","maxGeometryDelta":0,"flaggedPixels":30,"flaggedRatio":0.00001169961016898917,"bounds":[55,2754,378,2763]}, + {"file":"chromium-577-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-577-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":0.0000010913742147562525,"bounds":[227,2595,228,2596]}, + {"file":"chromium-577-_resources.png","maxGeometryDelta":0,"flaggedPixels":2,"flaggedRatio":4.0540403579717637e-7,"bounds":[71,374,71,375]}, + {"file":"chromium-577-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":48,"flaggedRatio":0.000010503650018381388,"bounds":[107,2622,385,5736]}, + {"file":"chromium-577-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":86,"flaggedRatio":0.00001734715942281563,"bounds":[150,370,457,6338]}, + {"file":"chromium-577-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_.png","maxGeometryDelta":0,"flaggedPixels":48,"flaggedRatio":0.000014152303552758904,"bounds":[55,2897,696,2906]}, + {"file":"chromium-767-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-767-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":8.44782482590441e-7,"bounds":[322,2605,323,2606]}, + {"file":"chromium-767-_resources.png","maxGeometryDelta":0,"flaggedPixels":29,"flaggedRatio":0.000004548802692389257,"bounds":[262,310,705,6105]}, + {"file":"chromium-767-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":163,"flaggedRatio":0.000032150725758254156,"bounds":[129,1260,634,4644]}, + {"file":"chromium-767-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":101,"flaggedRatio":0.00001869949977912373,"bounds":[190,308,631,5051]}, + {"file":"chromium-767-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_.png","maxGeometryDelta":0,"flaggedPixels":53,"flaggedRatio":0.000013253392868574354,"bounds":[67,2414,555,2423]}, + {"file":"chromium-768-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-768-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":5,"flaggedRatio":0.0000014015966989594547,"bounds":[318,2614,324,2621]}, + {"file":"chromium-768-_resources.png","maxGeometryDelta":0,"flaggedPixels":23,"flaggedRatio":0.000003596051472942683,"bounds":[274,6113,406,6121]}, + {"file":"chromium-768-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":129,"flaggedRatio":0.000025357601147342996,"bounds":[127,326,621,4685]}, + {"file":"chromium-768-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":185,"flaggedRatio":0.000034139089663643236,"bounds":[227,326,630,5066]}, + {"file":"chromium-768-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_.png","maxGeometryDelta":0,"flaggedPixels":51,"flaggedRatio":0.000012736680616245561,"bounds":[67,2414,555,2423]}, + {"file":"chromium-769-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-769-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":8.398644458784352e-7,"bounds":[323,2620,324,2621]}, + {"file":"chromium-769-_resources.png","maxGeometryDelta":0,"flaggedPixels":33,"flaggedRatio":0.000005152224019949412,"bounds":[181,324,510,6122]}, + {"file":"chromium-769-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":99,"flaggedRatio":0.000019435178379600837,"bounds":[94,330,627,4681]}, + {"file":"chromium-769-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":121,"flaggedRatio":0.00002229977383237647,"bounds":[151,326,546,5041]}, + {"file":"chromium-769-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_.png","maxGeometryDelta":0,"flaggedPixels":44,"flaggedRatio":0.000009792588523887461,"bounds":[67,2144,703,2153]}, + {"file":"chromium-991-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":6.629971981738405e-7,"bounds":[434,2637,435,2638]}, + {"file":"chromium-991-_resources.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-991-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":157,"flaggedRatio":0.0000267746886078134,"bounds":[170,1215,774,4099]}, + {"file":"chromium-991-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":102,"flaggedRatio":0.00001658764496910551,"bounds":[232,292,735,3873]}, + {"file":"chromium-991-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_.png","maxGeometryDelta":0,"flaggedPixels":43,"flaggedRatio":0.00000956038248644649,"bounds":[79,2144,700,2153]}, + {"file":"chromium-992-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-992-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":5,"flaggedRatio":0.0000011038814237067808,"bounds":[430,2631,436,2638]}, + {"file":"chromium-992-_resources.png","maxGeometryDelta":0,"flaggedPixels":7,"flaggedRatio":8.379588662751722e-7,"bounds":[392,289,720,298]}, + {"file":"chromium-992-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":148,"flaggedRatio":0.000025112531288856553,"bounds":[155,292,780,3656]}, + {"file":"chromium-992-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":209,"flaggedRatio":0.0000338233237872801,"bounds":[185,291,792,4359]}, + {"file":"chromium-992-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_.png","maxGeometryDelta":0,"flaggedPixels":45,"flaggedRatio":0.000009994975858801642,"bounds":[79,2144,701,2153]}, + {"file":"chromium-993-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-993-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":6.616618563849707e-7,"bounds":[435,2637,436,2638]}, + {"file":"chromium-993-_resources.png","maxGeometryDelta":0,"flaggedPixels":1,"flaggedRatio":1.195878571924861e-7,"bounds":[724,300,724,300]}, + {"file":"chromium-993-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":257,"flaggedRatio":0.0000435636562485115,"bounds":[153,289,817,4119]}, + {"file":"chromium-993-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":138,"flaggedRatio":0.000022310613207204,"bounds":[275,291,774,3897]}, + {"file":"chromium-993-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_.png","maxGeometryDelta":0,"flaggedPixels":51,"flaggedRatio":0.000010461250911257004,"bounds":[95,2110,833,2119]}, + {"file":"chromium-1199-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1199-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":5.471430288415505e-7,"bounds":[538,2668,539,2669]}, + {"file":"chromium-1199-_resources.png","maxGeometryDelta":0,"flaggedPixels":3,"flaggedRatio":3.031727942435975e-7,"bounds":[789,291,975,300]}, + {"file":"chromium-1199-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":191,"flaggedRatio":0.0000294290441862461,"bounds":[219,287,1051,3689]}, + {"file":"chromium-1199-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":165,"flaggedRatio":0.000023837637086277944,"bounds":[221,288,987,3561]}, + {"file":"chromium-1199-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_.png","maxGeometryDelta":0,"flaggedPixels":50,"flaggedRatio":0.000010247581570749304,"bounds":[95,2110,833,2119]}, + {"file":"chromium-1200-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1200-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":5,"flaggedRatio":9.111451271958598e-7,"bounds":[534,2662,540,2669]}, + {"file":"chromium-1200-_resources.png","maxGeometryDelta":0,"flaggedPixels":7,"flaggedRatio":7.068136839129206e-7,"bounds":[790,289,972,298]}, + {"file":"chromium-1200-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":156,"flaggedRatio":0.000024016257158692038,"bounds":[202,291,1026,3687]}, + {"file":"chromium-1200-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":304,"flaggedRatio":0.00004388244124949477,"bounds":[188,291,982,3998]}, + {"file":"chromium-1200-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_.png","maxGeometryDelta":0,"flaggedPixels":51,"flaggedRatio":0.000010443830010488882,"bounds":[96,2110,834,2119]}, + {"file":"chromium-1201-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1201-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":3,"flaggedRatio":5.461124620315311e-7,"bounds":[539,2669,540,2670]}, + {"file":"chromium-1201-_resources.png","maxGeometryDelta":0,"flaggedPixels":3,"flaggedRatio":3.026679269759146e-7,"bounds":[790,291,976,300]}, + {"file":"chromium-1201-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":267,"flaggedRatio":0.00004107052239397152,"bounds":[218,287,1052,3691]}, + {"file":"chromium-1201-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":162,"flaggedRatio":0.00002346279242858585,"bounds":[262,288,1005,3537]}, + {"file":"chromium-1201-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_.png","maxGeometryDelta":0,"flaggedPixels":50,"flaggedRatio":0.000008539651308957753,"bounds":[215,2110,953,2119]}, + {"file":"chromium-1440-_our_team_leadership.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_content.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_design.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_operations.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_external.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_web-dev.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_our_team_marketing.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_ignite.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0}, + {"file":"chromium-1440-_register.png","maxGeometryDelta":0.03125,"flaggedPixels":5,"flaggedRatio":7.56145954316686e-7,"bounds":[654,2729,660,2736]}, + {"file":"chromium-1440-_resources.png","maxGeometryDelta":0,"flaggedPixels":7,"flaggedRatio":5.907292637150457e-7,"bounds":[910,289,1092,298]}, + {"file":"chromium-1440-_resources2023.png","maxGeometryDelta":0,"flaggedPixels":156,"flaggedRatio":0.00002065459167461074,"bounds":[277,291,1191,3543]}, + {"file":"chromium-1440-_resources2026.png","maxGeometryDelta":0,"flaggedPixels":297,"flaggedRatio":0.00003776780809375572,"bounds":[263,291,1200,3299]}, + {"file":"chromium-1440-_not-found.png","maxGeometryDelta":0,"flaggedPixels":0,"flaggedRatio":0} + ] +} diff --git a/docs/verification/resources-desktop.png b/docs/verification/resources-desktop.png new file mode 100644 index 0000000..67f67b7 Binary files /dev/null and b/docs/verification/resources-desktop.png differ diff --git a/docs/verification/team-phone.png b/docs/verification/team-phone.png new file mode 100644 index 0000000..90442fa Binary files /dev/null and b/docs/verification/team-phone.png differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..cfc82cb --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,30 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: [ + "build/**", + "node_modules/**", + "handoffs/**", + "playwright-report/**", + "test-results/**", + "src/constants/optimizedImages.ts", + "src/static/**", + ], + }, + ...tseslint.configs.recommended.map((config) => ({ + ...config, + files: ["src/**/*.{ts,tsx}", "scripts/**/*.ts", "playwright.config.ts"], + })), + { + files: ["src/**/*.{ts,tsx}", "scripts/**/*.ts", "playwright.config.ts"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/ban-ts-comment": "error", + }, + }, +); diff --git a/index.html b/index.html index c1445a9..26e6746 100644 --- a/index.html +++ b/index.html @@ -41,6 +41,6 @@
- + diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 5875dc5..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "src" - }, - "include": ["src"] -} diff --git a/licenses/bootstrap.txt b/licenses/bootstrap.txt new file mode 100644 index 0000000..8ed9495 --- /dev/null +++ b/licenses/bootstrap.txt @@ -0,0 +1,24 @@ +The reset and selected control values in src/styles were derived from Bootstrap 4.5.2 before the dependency was removed. + +The MIT License (MIT) + +Copyright (c) 2011-2020 Twitter, Inc. +Copyright (c) 2011-2020 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/licenses/to-title-case.txt b/licenses/to-title-case.txt new file mode 100644 index 0000000..299d71c --- /dev/null +++ b/licenses/to-title-case.txt @@ -0,0 +1,23 @@ +To Title Case 2.1, adapted in src/util/GoogleAnalytics.ts to preserve the previously shipped analytics label normalization. +Source: https://github.com/gouch/to-title-case + +Copyright 2008-2013 David Gouch +Copyright © 2008–2018 David Gouch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/package-lock.json b/package-lock.json index a44ebd2..ea0edd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,24 +8,26 @@ "name": "exploretech-la", "version": "0.1.0", "dependencies": { - "bootstrap": "4.5.2", - "classnames": "2.2.6", - "prop-types": "15.7.2", - "react": "16.13.1", - "react-bootstrap": "1.3.0", - "react-dom": "16.13.1", - "react-ga": "3.1.2", - "react-icons": "3.11.0", - "react-router-dom": "5.2.0" + "react": "19.3.0", + "react-dom": "19.3.0", + "react-icons": "5.7.0", + "react-router-dom": "7.18.3" }, "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/vite": "4.3.3", + "@types/node": "24.13.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "6.1.1", "autoprefixer": "10.5.6", + "eslint": "10.10.0", "gh-pages": "6.3.0", - "history": "4.10.1", "jsdom": "30.0.1", "prettier": "3.9.6", - "sass": "1.104.0", + "tailwindcss": "4.3.3", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", "vite": "8.3.0", "vitest": "5.0.0" }, @@ -66,15 +68,6 @@ "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -88,6 +81,30 @@ "specificity": "bin/cli.js" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", @@ -228,6 +245,113 @@ "node": ">=20.19.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@exodus/bytes": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", @@ -246,6 +370,94 @@ } } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -274,6 +486,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -322,48 +558,28 @@ "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@parcel/watcher": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", - "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">= 10.0.0" + "playwright": "1.63.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "playwright": "cli.js" }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.6.0", - "@parcel/watcher-darwin-arm64": "2.6.0", - "@parcel/watcher-darwin-x64": "2.6.0", - "@parcel/watcher-freebsd-x64": "2.6.0", - "@parcel/watcher-linux-arm-glibc": "2.6.0", - "@parcel/watcher-linux-arm-musl": "2.6.0", - "@parcel/watcher-linux-arm64-glibc": "2.6.0", - "@parcel/watcher-linux-arm64-musl": "2.6.0", - "@parcel/watcher-linux-x64-glibc": "2.6.0", - "@parcel/watcher-linux-x64-musl": "2.6.0", - "@parcel/watcher-win32-arm64": "2.6.0", - "@parcel/watcher-win32-x64": "2.6.0" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", - "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", @@ -372,17 +588,13 @@ "android" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", - "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", "cpu": [ "arm64" ], @@ -390,22 +602,18 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", - "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -414,17 +622,13 @@ "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", - "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", "cpu": [ "x64" ], @@ -432,68 +636,50 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", - "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", "cpu": [ - "arm" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", - "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", - "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", "cpu": [ "arm64" ], @@ -507,17 +693,13 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", - "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", "cpu": [ "arm64" ], @@ -531,19 +713,15 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", - "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "libc": [ @@ -555,17 +733,53 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", - "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", "cpu": [ "x64" ], @@ -579,248 +793,481 @@ "linux" ], "engines": { - "node": ">= 10.0.0" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", - "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">= 10.0.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", - "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">= 10.0.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "url": "https://opencollective.com/parcel" } }, - "node_modules/@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.3.2" + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@restart/hooks": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", - "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.2" + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "peerDependencies": { - "react": ">=16.8.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", - "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ - "arm" + "x64" ], "dev": true, - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", - "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", - "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", - "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", - "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "node_modules/@tailwindcss/node/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", - "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", - "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", - "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "musl" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", - "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "libc": [ @@ -831,20 +1278,20 @@ "os": [ "linux" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", - "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "libc": [ - "glibc" + "musl" ], "license": "MIT", "optional": true, @@ -852,13 +1299,13 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", - "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -872,13 +1319,13 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", - "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -892,30 +1339,43 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", - "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", - "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -926,13 +1386,13 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", - "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -943,15 +1403,23 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 20" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } }, "node_modules/@types/chai": { "version": "5.2.3", @@ -964,15 +1432,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/classnames": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.0.tgz", - "integrity": "sha512-3GsbOoDYteFShlrBTKzI2Eii4vPg/jAf7LXRIn0WQePKlmhpkV0KoTMuawA7gZJkrbPrZGwv9IEAfIWaOaQK8w==", - "license": "MIT", - "dependencies": { - "classnames": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -980,6 +1439,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -987,49 +1453,285 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/invariant": { - "version": "2.2.37", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.37.tgz", - "integrity": "sha512-IwpIMieE55oGWiXkQPSBY1nw1nFs6bsKXTFskNY8sdS17K24vyEBRQZEwlRS7ZmXCWnJcQtbxWzly+cODWGs2A==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } }, "node_modules/@types/react": { - "version": "16.14.70", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.70.tgz", - "integrity": "sha512-DM5Q7rSx9G6QYcVvMgxvEurL5P06OxcDNUXrLxlpBzG4ccUewcBCmsztYbxJBobzO8RIwwmjoaD5OsKqdHDuYQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "^0.16", "csstype": "^3.2.2" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "*" + "@types/react": "^19.3.0" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "license": "MIT" + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@types/warning": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.4.tgz", - "integrity": "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==", - "license": "MIT" + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, "node_modules/@vitejs/plugin-react": { "version": "6.1.1", @@ -1099,6 +1801,46 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1163,6 +1905,16 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.22", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", @@ -1186,19 +1938,17 @@ "require-from-string": "^2.0.2" } }, - "node_modules/bootstrap": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz", - "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==", - "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" + "dependencies": { + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" + "engines": { + "node": "20 || >=22" } }, "node_modules/braces": { @@ -1248,13 +1998,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" } }, "node_modules/caniuse-lite": { @@ -1288,45 +2043,51 @@ "node": ">=18" } }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=18" } }, - "node_modules/classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -1345,6 +2106,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -1376,6 +2138,24 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -1383,14 +2163,12 @@ "dev": true, "license": "MIT" }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", @@ -1415,16 +2193,6 @@ "node": ">=8" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.427", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", @@ -1439,6 +2207,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", @@ -1479,6 +2261,242 @@ "node": ">=0.8.0" } }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1489,6 +2507,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1499,6 +2527,13 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1516,6 +2551,20 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", @@ -1526,6 +2575,16 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -1599,6 +2658,25 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1707,28 +2785,25 @@ "dev": true, "license": "ISC" }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", @@ -1753,20 +2828,14 @@ "node": ">= 4" } }, - "node_modules/immutable": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", - "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "MIT" - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" + "engines": { + "node": ">=0.8.19" } }, "node_modules/is-extglob": { @@ -1809,24 +2878,22 @@ "dev": true, "license": "MIT" }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" + "bin": { + "jiti": "lib/jiti-cli.mjs" + } }, "node_modules/jsdom": { "version": "30.0.1", @@ -1869,17 +2936,55 @@ } } }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">= 0.8.0" } }, "node_modules/lightningcss": { @@ -2168,18 +3273,6 @@ "node": ">=8" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -2247,21 +3340,29 @@ "node": ">=8.6" } }, - "node_modules/mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" + "brace-expansion": "^5.0.8" }, - "peerDependencies": { - "prop-types": "^15.0.0", - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.19", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", @@ -2281,13 +3382,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.55", @@ -2299,15 +3399,6 @@ "node": ">=18" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obug": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", @@ -2322,6 +3413,24 @@ "node": ">=12.20.0" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -2384,13 +3493,14 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", - "dependencies": { - "isarray": "0.0.1" + "engines": { + "node": ">=8" } }, "node_modules/path-type": { @@ -2436,16 +3546,33 @@ "node": ">=8" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" } }, "node_modules/postcss": { @@ -2484,6 +3611,16 @@ "dev": true, "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.9.6", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", @@ -2500,30 +3637,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "node_modules/prop-types-extra": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", - "license": "MIT", - "dependencies": { - "react-is": "^16.3.2", - "warning": "^4.0.0" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2534,6 +3647,26 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2556,185 +3689,71 @@ "license": "MIT" }, "node_modules/react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-bootstrap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.3.0.tgz", - "integrity": "sha512-GYj0c6FO9mx7DaO8Xyz2zs0IcQ6CGCtM3O6/feIoCaG4N8B0+l4eqL7stlMcLpqO4d8NG2PoMO/AbUOD+MO7mg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.4.2", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.21", - "@types/classnames": "^2.2.10", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.35", - "@types/react-transition-group": "^4.4.0", - "@types/warning": "^3.0.0", - "classnames": "^2.2.6", - "dom-helpers": "^5.1.2", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "prop-types-extra": "^1.1.0", - "react-overlays": "^4.1.0", - "react-transition-group": "^4.4.1", - "uncontrollable": "^7.0.0", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^16.13.1" - } - }, - "node_modules/react-ga": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.1.2.tgz", - "integrity": "sha512-OJrMqaHEHbodm+XsnjA6ISBEHTwvpFrxco65mctzl/v3CASMSLSyUkFqz9yYrPDKGBUfNQzKCjuMJwctjlWBbw==", - "license": "Apache-2.0", - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.6.2 || ^16.0" + "react": "^19.3.0" } }, "node_modules/react-icons": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.11.0.tgz", - "integrity": "sha512-JRgiI/vdF6uyBgyZhVyYJUZAop95Sy4XDe/jmT3R/bKliFWpO/uZBwvSjWEdxwzec7SYbEPNPck0Kff2tUGM2Q==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", "license": "MIT", - "dependencies": { - "camelcase": "^5.0.0" - }, "peerDependencies": { "react": "*" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, - "node_modules/react-overlays": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", - "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.1", - "@popperjs/core": "^2.5.3", - "@restart/hooks": "^0.3.25", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.0.0", - "warning": "^4.0.3" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-router": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", - "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", - "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.1.2", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "react-router": "7.18.3" }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "dev": true, - "license": "MIT", "engines": { - "node": ">= 20.19.0" + "node": ">=20.0.0" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/require-from-string": { @@ -2747,12 +3766,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", - "license": "MIT" - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2822,27 +3835,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/sass": { - "version": "1.104.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.0.tgz", - "integrity": "sha512-btHMApW2bgolvClhRW8AlQJzgI9lUB3pPSofBQQT+E46GWvf9o0TVQ13SYv5riWZVFyPN+JNz3TKW9XhBlc10w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=20.19.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -2857,14 +3849,10 @@ } }, "node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -2876,6 +3864,35 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2937,17 +3954,26 @@ "dev": true, "license": "MIT" }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, "license": "MIT" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", - "license": "MIT" + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, "node_modules/tinybench": { "version": "6.1.4", @@ -3089,19 +4115,68 @@ "node": ">=0.10.0" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "react": ">=15.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici": { @@ -3114,6 +4189,13 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -3155,11 +4237,15 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", - "license": "MIT" + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } }, "node_modules/vite": { "version": "8.3.0", @@ -3361,15 +4447,6 @@ "node": ">=18" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -3405,6 +4482,22 @@ "node": "^22.14.0 || >=24.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3422,6 +4515,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -3438,6 +4541,19 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 62822ba..f57a708 100644 --- a/package.json +++ b/package.json @@ -7,24 +7,26 @@ "node": ">=24.21.0 <25" }, "dependencies": { - "bootstrap": "4.5.2", - "classnames": "2.2.6", - "prop-types": "15.7.2", - "react": "16.13.1", - "react-bootstrap": "1.3.0", - "react-dom": "16.13.1", - "react-ga": "3.1.2", - "react-icons": "3.11.0", - "react-router-dom": "5.2.0" + "react": "19.3.0", + "react-dom": "19.3.0", + "react-icons": "5.7.0", + "react-router-dom": "7.18.3" }, "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/vite": "4.3.3", + "@types/node": "24.13.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "6.1.1", "autoprefixer": "10.5.6", + "eslint": "10.10.0", "gh-pages": "6.3.0", - "history": "4.10.1", "jsdom": "30.0.1", "prettier": "3.9.6", - "sass": "1.104.0", + "tailwindcss": "4.3.3", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0", "vite": "8.3.0", "vitest": "5.0.0" }, @@ -32,11 +34,18 @@ "start": "vite --host 127.0.0.1 --port 3000", "images": "node scripts/images.js", "images:check": "node scripts/images.js --check", + "content:check": "node scripts/content-check.mjs", + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "format": "prettier --write src scripts *.json *.mjs *.ts .github/workflows/*.yml", + "format:check": "prettier --check src scripts *.json *.mjs *.ts .github/workflows/*.yml", "prebuild": "npm run images:check", "build": "vite build", "preview": "vite preview --host 127.0.0.1", "test": "vitest run", "test:watch": "vitest", + "test:browser": "playwright test", + "visual:parity": "node scripts/visual-parity.cjs", "predeploy": "npm run build", "deploy": "gh-pages -d build" }, diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e32e640 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Browser smoke suite for the production build. + * + * The suite runs against `build/` served by `scripts/serve-built-site.cjs`, + * which reproduces GitHub Pages' 404-to-root restoration. Running it against + * the dev server would prove less: dev serves index.html for every path, so + * deep links and trailing slashes cannot fail there the way they can in + * production. + * + * npm run build && npm run test:browser + * + * Chromium only. This is a behaviour gate for CI, not the cross-engine visual + * matrix, which stays a session tool. + */ +const port = Number(process.env.SMOKE_PORT ?? 4390); +// Normally the freshly built site; pointed at a saved baseline build when +// comparing current behaviour against the pre-refactor deployment. +const root = process.env.SMOKE_ROOT ?? "build"; +const host = "127.0.0.1"; +const baseURL = `http://${host}:${port}`; +const isCI = Boolean(process.env.CI); + +export default defineConfig({ + testDir: "scripts", + testMatch: /browser-smoke\.spec\.ts/, + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 1 : 0, + workers: isCI ? 2 : undefined, + timeout: 60_000, + expect: { timeout: 10_000 }, + reporter: isCI ? [["github"], ["list"]] : [["list"]], + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "off", + // The site is static and offline in this suite; nothing needs a slow net. + actionTimeout: 10_000, + }, + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1440, height: 900 }, + }, + }, + ], + webServer: { + command: `node scripts/serve-built-site.cjs --root ${root} --port ${port} --host ${host}`, + url: baseURL, + reuseExistingServer: !isCI, + timeout: 30_000, + stdout: "ignore", + stderr: "pipe", + }, +}); diff --git a/scripts/asset-parity.cjs b/scripts/asset-parity.cjs new file mode 100644 index 0000000..3a38b08 --- /dev/null +++ b/scripts/asset-parity.cjs @@ -0,0 +1,51 @@ +// Compare emitted public media, not source filenames or bundler import graphs. +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const assert = require("node:assert/strict"); +function mediaFiles(root, directory = "") { + return fs + .readdirSync(path.join(root, directory), { withFileTypes: true }) + .flatMap((entry) => { + const relative = path.posix.join(directory, entry.name); + if (entry.isDirectory()) return mediaFiles(root, relative); + return /\.(?:png|jpe?g|gif|webp|svg|ico|pdf|woff2?|ttf)$/i.test( + entry.name, + ) + ? [relative] + : []; + }); +} +function manifest(root) { + return Object.fromEntries( + mediaFiles(root) + .sort() + .map((name) => [ + name, + crypto + .createHash("sha256") + .update(fs.readFileSync(path.join(root, name))) + .digest("hex"), + ]), + ); +} +const [baseline, candidate = "build"] = process.argv.slice(2); +if (!baseline) + throw new Error( + "Usage: node scripts/asset-parity.cjs [candidate-build-directory]", + ); +const before = manifest(baseline), + after = manifest(candidate); +for (const [url, hash] of Object.entries(before)) + assert.equal(after[url], hash, `Public media changed or disappeared: ${url}`); +for (const file of ["404.html", "CNAME"]) { + assert.ok( + fs + .readFileSync(path.join(baseline, file)) + .equals(fs.readFileSync(path.join(candidate, file))), + `Hosting file changed: ${file}`, + ); +} +console.log( + `Preserved ${Object.keys(before).length} public asset URLs and SHA-256 hashes; ${Object.keys(after).length - Object.keys(before).length} additions`, +); diff --git a/scripts/browser-smoke.spec.ts b/scripts/browser-smoke.spec.ts new file mode 100644 index 0000000..442c5b3 --- /dev/null +++ b/scripts/browser-smoke.spec.ts @@ -0,0 +1,593 @@ +import { + expect, + test, + type Browser, + type Page, + type Route, +} from "@playwright/test"; + +/** + * Durable browser smoke for the built site. + * + * Scope: the behaviour that has broken before and that unit tests cannot see — + * GitHub Pages deep-link restoration, the mobile menu, hash and history + * scrolling, team rendering and lazy portraits, the click-to-load video + * facade, shared document links, and the carousel's slow/failed image paths. + * + * Nothing here talks to a third party. Every request that is not same-origin + * is aborted and recorded, so "no YouTube before activation" and "no analytics + * beacons" are assertions rather than hopes. + */ + +declare global { + interface Window { + /** Arguments of each window.scrollTo call, installed by the history test. */ + __scrolls?: unknown[][]; + } +} + +const TEAM_SECTIONS = [ + { slug: "leadership", heading: "Leadership" }, + { slug: "content", heading: "Content" }, + { slug: "design", heading: "Design" }, + { slug: "operations", heading: "Operations" }, + { slug: "external", heading: "External" }, + { slug: "web-dev", heading: "Web Dev" }, + { slug: "marketing", heading: "Marketing" }, +] as const; + +const ROUTES = [ + // The header also carries a `Home` class on the home page, hence `div`. + { path: "/", marker: "div.Home" }, + { path: "/register", marker: ".Register" }, + { path: "/resources", marker: ".Resources" }, + { path: "/resources2023", marker: ".Resources" }, + { path: "/resources2026", marker: ".Resources" }, + { path: "/ignite", marker: ".Ignite" }, + ...TEAM_SECTIONS.map((section) => ({ + path: `/our_team/${section.slug}`, + marker: ".Team", + })), +] as const; + +// The Google Fonts stylesheet is a deliberate first-party design choice and is +// merely blocked here; tracking and video vendors must never be reached. +const THIRD_PARTY = /google-analytics|googletagmanager|youtube|doubleclick/; + +type Offline = { + /** Every cross-origin URL the page tried to reach, in order. */ + readonly external: string[]; + /** Uncaught page errors. No route is allowed to produce one. */ + readonly pageErrors: string[]; + /** Same-origin URL fragment whose response waits for `hold.release()`. */ + hold: { fragment: string; wait: Promise; release: () => void } | null; + /** Same-origin URL fragment whose request fails, modelling a broken asset. */ + fail: string | null; +}; + +/** + * Cut a page off from the internet and record what it reached for. + * + * The returned object stays live: setting `hold` or `fail` on it shapes + * later requests, which is how the carousel's slow and broken image paths are + * exercised without shipping a test-only hook into the app. + */ +async function goOffline( + page: Page, + baseURL: string | undefined, +): Promise { + const origin = new URL(baseURL ?? "http://127.0.0.1:4390").origin; + const state: Offline = { + external: [], + pageErrors: [], + hold: null, + fail: null, + }; + + page.on("pageerror", (error) => state.pageErrors.push(String(error))); + + await page.route("**/*", async (route: Route) => { + const url = route.request().url(); + if (!url.startsWith(origin)) { + state.external.push(url); + await route.abort(); + return; + } + if (state.fail && url.includes(state.fail)) { + await route.abort("failed"); + return; + } + if (state.hold && url.includes(state.hold.fragment)) { + await state.hold.wait; + } + await route.continue(); + }); + + return state; +} + +function activeTeamNav(page: Page) { + return page.locator(".team-navigation a.active"); +} + +function activeSlideAlt(page: Page) { + return page.locator(".carousel-item.active img").getAttribute("alt"); +} + +test.describe("routes", () => { + for (const route of ROUTES) { + test(`renders ${route.path}`, async ({ page, baseURL }) => { + const offline = await goOffline(page, baseURL); + await page.goto(route.path); + + await expect(page.locator(route.marker)).toBeVisible(); + await expect(page.locator(".PageNotFound")).toHaveCount(0); + await expect(page.locator(".Header")).toBeVisible(); + // Restoration must leave the clean path in the address bar. + await expect(page).toHaveURL( + new RegExp(`${route.path.replace(/\//g, "\\/")}$`), + ); + expect(offline.pageErrors, `uncaught errors on ${route.path}`).toEqual( + [], + ); + expect( + offline.external.filter((url) => THIRD_PARTY.test(url)), + `third-party requests on ${route.path}`, + ).toEqual([]); + }); + } + + test("unknown paths show the not-found page and restore the title", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + const response = await page.goto("/no-such-page"); + // GitHub Pages answers with its 404 document before the SPA restores. + expect(response?.status()).toBe(404); + await expect(page.locator(".PageNotFound")).toBeVisible(); + await expect(page).toHaveTitle("Page Not Found • exploretech.la"); + + await page.locator(".PageNotFound a").first().click(); + await expect(page.locator("div.Home")).toBeVisible(); + await expect(page).not.toHaveTitle("Page Not Found • exploretech.la"); + }); + + test("/our_team and its trailing slash land on leadership", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + + await page.goto("/our_team"); + await expect(page).toHaveURL(/\/our_team\/leadership$/); + await expect(activeTeamNav(page)).toHaveText("Leadership"); + + await page.goto("/our_team/"); + await expect(page).toHaveURL(/\/our_team\/leadership$/); + await expect(activeTeamNav(page)).toHaveText("Leadership"); + }); + + test("a deep link keeps its query and hash through the Pages restore", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + const response = await page.goto("/our_team/design?ref=newsletter#top"); + expect(response?.status()).toBe(404); + + await expect(page.locator(".Team")).toBeVisible(); + await expect(activeTeamNav(page)).toHaveText("Design"); + await expect(page).toHaveURL(/\/our_team\/design\?ref=newsletter#top$/); + }); +}); + +test.describe("team", () => { + for (const section of TEAM_SECTIONS) { + test(`${section.slug} lists people`, async ({ page, baseURL }) => { + await goOffline(page, baseURL); + await page.goto(`/our_team/${section.slug}`); + + await expect( + page.getByRole("heading", { name: "Meet Our Team" }), + ).toBeVisible(); + await expect(activeTeamNav(page)).toHaveCount(1); + await expect( + page.locator(".Team h3", { hasText: section.heading }).first(), + ).toBeVisible(); + + const cards = page.locator(".person"); + const count = await cards.count(); + expect(count, `${section.slug} renders no people`).toBeGreaterThan(0); + + // Every card carries text, including the members with no portrait. + const texts = await cards.evaluateAll((nodes) => + nodes.map((node) => (node.textContent ?? "").trim()), + ); + expect( + texts.filter((text) => text.length === 0), + `blank cards in ${section.slug}`, + ).toEqual([]); + expect(await cards.locator("img").count()).toBeLessThanOrEqual(count); + }); + } + + test("portraits past the fold are lazy", async ({ page, baseURL }) => { + await goOffline(page, baseURL); + await page.goto("/our_team/leadership"); + + const loading = await page + .locator(".person img") + .evaluateAll((images) => + images.map((image) => (image as HTMLImageElement).loading), + ); + expect(loading.length).toBeGreaterThan(2); + // The shared People component keeps the first two eager and defers the rest. + expect(loading.slice(0, 2)).toEqual(["eager", "eager"]); + expect(loading.slice(2).every((value) => value === "lazy")).toBe(true); + }); + + test("a member without a portrait still renders", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + let cardsWithoutImage = 0; + for (const section of TEAM_SECTIONS) { + await page.goto(`/our_team/${section.slug}`); + await expect(page.locator(".person").first()).toBeVisible(); + cardsWithoutImage += await page.locator(".person:not(:has(img))").count(); + } + // Two roster members deliberately have no photo; their cards must survive. + expect(cardsWithoutImage).toBeGreaterThan(0); + }); +}); + +test.describe("navigation", () => { + test("dropdown arrow keys retain the first and last item boundaries", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + await page.goto("/"); + const trigger = page.locator("#navbar-home"); + await trigger.focus(); + await trigger.press("ArrowUp"); + await expect(trigger).toHaveAttribute("aria-expanded", "false"); + await expect(trigger).toBeFocused(); + await trigger.press("ArrowDown"); + const links = page.locator(".site-menu-link"); + await expect(links.first()).toBeFocused(); + await page.keyboard.press("ArrowUp"); + await expect(links.first()).toBeFocused(); + for (let index = 0; index < 5; index++) + await page.keyboard.press("ArrowDown"); + await expect(links.last()).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(trigger).toHaveAttribute("aria-expanded", "false"); + await expect(trigger).toBeFocused(); + }); + + test.describe("on a phone", () => { + test.use({ viewport: { width: 390, height: 844 } }); + + test("the menu opens, navigates and closes", async ({ page, baseURL }) => { + await goOffline(page, baseURL); + await page.goto("/"); + + const toggle = page.locator(".site-nav-toggle"); + const collapse = page.locator(".site-nav-panel"); + await expect(toggle).toBeVisible(); + + await toggle.click(); + await expect(collapse).toHaveClass(/show/); + + await collapse.getByRole("link", { name: "Our Team" }).click(); + await expect(page).toHaveURL(/\/our_team\/leadership$/); + await expect(collapse).not.toHaveClass(/show/); + }); + + test("a hash link closes the menu and still reaches its section", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + await page.goto("/"); + + const collapse = page.locator(".site-nav-panel"); + await page.locator(".site-nav-toggle").click(); + await expect(collapse).toHaveClass(/show/); + + await collapse.locator("#navbar-home").click(); + await collapse.getByRole("link", { name: "About", exact: true }).click(); + + await expect(collapse).not.toHaveClass(/show/); + // The final scroll happens after the menu finishes closing, so the + // section must be at the top once the collapse animation is done. + await expect + .poll(() => + page + .locator("#about") + .evaluate((node) => Math.abs(node.getBoundingClientRect().top)), + ) + .toBeLessThan(80); + }); + }); + + test("repeating the same hash scrolls back to the section", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + await page.goto("/"); + + const about = page.locator("#about"); + await expect(about).toHaveCount(1); + + const openAbout = async () => { + await page.locator("#navbar-home").click(); + await page.getByRole("link", { name: "About", exact: true }).click(); + }; + + await openAbout(); + await expect(page).toHaveURL(/#about$/); + await expect + .poll(() => + about.evaluate((node) => Math.abs(node.getBoundingClientRect().top)), + ) + .toBeLessThan(80); + + await page.evaluate(() => window.scrollTo(0, 0)); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); + + // Same hash again: the location does not change, so the app has to scroll + // deliberately rather than rely on the browser. + await openAbout(); + await expect + .poll(() => + about.evaluate((node) => Math.abs(node.getBoundingClientRect().top)), + ) + .toBeLessThan(80); + }); + + test("a new page starts at the top and Back does not", async ({ + page, + baseURL, + }) => { + await goOffline(page, baseURL); + await page.goto("/"); + await expect(page.locator("div.Home")).toBeVisible(); + + // Record the app's own scrolling. The spy survives client navigation + // because these are same-document history moves, and it separates "the + // app scrolled you to the top" from "the browser restored a position", + // which a scroll offset alone cannot. + await page.evaluate(() => { + const scrolls: unknown[][] = []; + window.__scrolls = scrolls; + const original = window.scrollTo.bind(window); + window.scrollTo = (x?: number | ScrollToOptions, y?: number) => { + if (typeof x === "number") { + scrolls.push([x, y ?? 0]); + original(x, y ?? 0); + } else { + scrolls.push([x]); + original(x); + } + }; + }); + await page.evaluate(() => window.scrollTo(0, 1200)); + await expect + .poll(() => page.evaluate(() => Math.round(window.scrollY))) + .toBeGreaterThan(600); + + await page + .locator(".Header") + .getByRole("link", { name: "Our Team" }) + .click(); + await expect(page.locator(".Team")).toBeVisible(); + // A pushed route starts at the top of the new page. + await expect + .poll(() => page.evaluate(() => Math.round(window.scrollY))) + .toBeLessThan(100); + expect(await page.evaluate(() => window.__scrolls)).toContainEqual([0, 0]); + + await page.evaluate(() => { + window.__scrolls?.splice(0); + }); + await page.goBack(); + await expect(page.locator("div.Home")).toBeVisible(); + await page.waitForTimeout(500); + + // Going back must leave scrolling to the browser's restoration. + const scrollsOnBack = await page.evaluate(() => window.__scrolls); + expect(scrollsOnBack, "the app scrolled during a Back navigation").toEqual( + [], + ); + }); +}); + +test.describe("media", () => { + test("archived videos load nothing until activated, and keep their box", async ({ + page, + baseURL, + }) => { + const offline = await goOffline(page, baseURL); + await page.goto("/resources"); + + const facade = page.locator(".video-facade").first(); + await expect(facade).toBeVisible(); + expect( + offline.external.filter((url) => url.includes("youtube")), + "a video requested YouTube before activation", + ).toEqual([]); + await expect(page.locator("iframe")).toHaveCount(0); + + const frame = page.locator(".video-frame").first(); + const before = await frame.boundingBox(); + + // Keyboard activation: the facade is a real button, not a clickable div. + await facade.focus(); + await expect(facade).toBeFocused(); + await page.keyboard.press("Enter"); + + const iframe = page.locator(".video-frame iframe").first(); + await expect(iframe).toHaveCount(1); + await expect(iframe).toHaveAttribute("src", /youtube\.com\/embed\//); + + const after = await frame.boundingBox(); + if (!before || !after) throw new Error("the video frame has no box"); + expect(Math.abs(after.width - before.width)).toBeLessThanOrEqual(1); + expect(Math.abs(after.height - before.height)).toBeLessThanOrEqual(1); + // Activation is the only thing that may reach YouTube, and it is blocked here. + await expect + .poll(() => offline.external.some((url) => url.includes("youtube"))) + .toBe(true); + }); + + test("shared documents and maps resolve", async ({ + page, + baseURL, + request, + }) => { + await goOffline(page, baseURL); + const checked = new Set(); + + for (const path of [ + "/resources", + "/resources2023", + "/resources2026", + "/register", + ]) { + await page.goto(path); + const hrefs = await page + .locator('a[href*="/static/media/"]') + .evaluateAll((links) => + links.map((link) => (link as HTMLAnchorElement).href), + ); + expect(hrefs.length, `no document links on ${path}`).toBeGreaterThan(0); + + for (const href of hrefs) { + if (checked.has(href)) continue; + checked.add(href); + const response = await request.head(href); + expect(response.status(), `${href} on ${path}`).toBe(200); + expect(Number(response.headers()["content-length"])).toBeGreaterThan(0); + } + } + + // The test server must not dress a missing asset up as the SPA shell. + const missing = await request.get( + "/static/media/not-a-real-file.abcdef12.pdf", + ); + expect(missing.status()).toBe(404); + expect(missing.headers()["content-type"]).toContain("text/plain"); + }); +}); + +test.describe("carousel", () => { + type Slide = { readonly alt: string; readonly asset: string }; + + /** + * Read the slide list from a throwaway context. + * + * Slides are responsive, and which srcset candidate the browser picks is + * not known before it requests one, so slides are identified by the shared + * part of their filename rather than by one URL. The throwaway context also + * leaves the real page's HTTP cache empty, which is what lets the held and + * failed requests reach the route handler at all. + */ + async function readSlides( + browser: Browser, + baseURL: string | undefined, + ): Promise { + const context = await browser.newContext({ baseURL }); + try { + const page = await context.newPage(); + await goOffline(page, baseURL); + await page.goto("/"); + await expect(page.locator(".carousel-item.active img")).toBeVisible(); + return await page.locator(".carousel-item img").evaluateAll((images) => + images.map((image) => { + const slide = image as HTMLImageElement; + const file = new URL(slide.src).pathname.split("/").pop() ?? ""; + return { alt: slide.alt, asset: file.split(/-\d+w\./)[0] }; + }), + ); + } finally { + await context.close(); + } + } + + test("a slow slide keeps the current one visible until it loads", async ({ + browser, + baseURL, + }) => { + const slides = await readSlides(browser, baseURL); + expect(slides.length).toBeGreaterThan(2); + + const context = await browser.newContext({ baseURL }); + const page = await context.newPage(); + const offline = await goOffline(page, baseURL); + const { promise, resolve } = Promise.withResolvers(); + offline.hold = { + fragment: slides[2].asset, + wait: promise, + release: resolve, + }; + // `load` would wait for the held image, so settle on the parsed document. + await page.goto("/", { waitUntil: "domcontentloaded" }); + await expect(page.locator(".carousel-item.active img")).toBeVisible(); + + const firstAlt = await activeSlideAlt(page); + expect(firstAlt).toBe(slides[0].alt); + + await page.locator(".carousel-indicators li").nth(2).click(); + // The requested slide has not arrived, so the visible one must not change. + await page.waitForTimeout(1000); + expect(await activeSlideAlt(page)).toBe(firstAlt); + + offline.hold.release(); + await expect + .poll(() => activeSlideAlt(page), { timeout: 10_000 }) + .toBe(slides[2].alt); + expect(offline.pageErrors).toEqual([]); + await context.close(); + }); + + test("a broken slide is skipped and the carousel keeps working", async ({ + browser, + baseURL, + }) => { + const slides = await readSlides(browser, baseURL); + expect(slides.length).toBeGreaterThan(2); + + const context = await browser.newContext({ baseURL }); + const page = await context.newPage(); + const offline = await goOffline(page, baseURL); + offline.fail = slides[1].asset; + await page.goto("/"); + + const firstAlt = await activeSlideAlt(page); + await page.locator(".carousel-indicators li").nth(1).click(); + + // The broken slide is passed over rather than displayed or retried. + await expect + .poll(() => activeSlideAlt(page), { timeout: 5_000 }) + .toBe(slides[2].alt); + const seen: (string | null)[] = []; + for (let sample = 0; sample < 15; sample += 1) { + seen.push(await activeSlideAlt(page)); + await page.waitForTimeout(200); + } + expect(seen, "the broken slide was displayed").not.toContain(slides[1].alt); + + // Selection still works afterwards: the failure did not lock the carousel. + await page.locator(".carousel-indicators li").nth(0).click(); + await expect + .poll(() => activeSlideAlt(page), { timeout: 5_000 }) + .toBe(firstAlt); + expect(offline.pageErrors).toEqual([]); + await context.close(); + }); +}); diff --git a/scripts/content-check.mjs b/scripts/content-check.mjs new file mode 100755 index 0000000..febee12 --- /dev/null +++ b/scripts/content-check.mjs @@ -0,0 +1,324 @@ +#!/usr/bin/env node +/** + * Cross-record integrity check for the authored content in `src/content`. + * + * TypeScript already proves the shape of this data. This checks the things a + * type cannot: that a team assignment names a person who exists, that nobody + * appears twice in one team or twice in the registry, that imported images and + * documents exist on disk, that outbound links are well-formed and unique, and + * that archived videos carry a bare YouTube id. + * + * Remote URLs are checked for syntax only. Nothing here makes a network + * request, so a live-but-dead external link still passes. + * + * node scripts/content-check.mjs + * + * Content modules import optimized images, so Vite loads them exactly the way + * the app does; a broken asset import fails here instead of at build time. + */ + +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +const problems = []; +const report = (where, message) => problems.push(`${where}: ${message}`); + +/** Reject unusable link targets, and prove bundled assets exist. */ +function checkUrl(where, value, { local = false } = {}) { + if (value.trim() === "") return report(where, "URL is empty"); + if (value.startsWith("data:")) return; + if (/^[a-z][a-z0-9+.-]*:/i.test(value)) { + let parsed; + try { + parsed = new URL(value); + } catch { + return report(where, `not a valid URL: ${value}`); + } + if (!["http:", "https:"].includes(parsed.protocol)) { + return report(where, `unsupported URL scheme: ${value}`); + } + if (local) + report( + where, + `expected a bundled asset import, got a remote URL: ${value}`, + ); + return; + } + if (!value.startsWith("/")) { + return report( + where, + `expected an imported asset or absolute URL, got: ${value}`, + ); + } + // Vite serves imported assets from `/src/...` (or `/@fs/`) in the graph + // this checker loads, and from `/static/media/...` in a build. + const clean = value.split(/[?#]/)[0]; + if (!clean.startsWith("/src/") && !clean.startsWith("/@fs/")) return; + const onDisk = clean.startsWith("/@fs/") + ? clean.slice(4) + : path.join(repoRoot, clean); + if (!fs.existsSync(onDisk)) report(where, `asset does not exist: ${value}`); +} + +/** Document links: usable target, visible text, no repeated target in one list. */ +function checkLinks(where, links, options) { + const seen = new Map(); + links.forEach((link, index) => { + const at = `${where}[${index}]`; + if (link.name.trim() === "") + report(at, "name is empty; it is the visible link text"); + checkUrl(`${at}.src`, link.src, options); + if (seen.has(link.src)) + report(at, `same target as ${where}[${seen.get(link.src)}]`); + else seen.set(link.src, index); + }); +} + +function checkPeople(people) { + const byName = new Map(); + const byLink = new Map(); + for (const [id, person] of Object.entries(people)) { + const where = `people["${id}"]`; + if (person.name.trim() === "") report(where, "name is empty"); + const twin = byName.get(person.name); + if (twin) + report( + where, + `duplicate profile: "${person.name}" is also people["${twin}"]`, + ); + else byName.set(person.name, id); + + if (person.image !== undefined) + checkUrl(`${where}.image`, person.image, { local: true }); + if (person.link !== undefined) { + checkUrl(`${where}.link`, person.link); + const shared = byLink.get(person.link); + if (shared) + report( + where, + `profile link duplicates people["${shared}"]: ${person.link}`, + ); + else byLink.set(person.link, id); + } + person.descriptions?.forEach((text, index) => { + if (text.trim() === "") + report(`${where}.descriptions[${index}]`, "is empty"); + }); + } +} + +function checkRoles(members, where, people, referenced) { + const seen = new Set(); + for (const [index, member] of members.entries()) { + const at = `${where}[${index}]`; + if (!Object.hasOwn(people, member.personId)) { + report(at, `personId "${member.personId}" is not in the people registry`); + } else { + referenced.add(member.personId); + if (seen.has(member.personId)) + report(at, `"${member.personId}" is already listed here`); + seen.add(member.personId); + } + if (member.title.trim() === "") + report(at, "title is empty; it is the role on the card"); + } +} + +function checkTeams(teamSections, people, pastSpeakers) { + const referenced = new Set(); + const sectionIds = new Set(); + for (const section of teamSections) { + const where = `teamSections["${section.id}"]`; + if (sectionIds.has(section.id)) report(where, "duplicate section id"); + sectionIds.add(section.id); + if (section.navLabel.trim() === "" || section.heading.trim() === "") { + report(where, "navLabel and heading must be non-empty"); + } + checkRoles(section.members, `${where}.members`, people, referenced); + } + checkRoles(pastSpeakers, "pastSpeakers", people, referenced); + for (const id of Object.keys(people)) { + if (!referenced.has(id)) + report(`people["${id}"]`, "has no team or speaker assignment"); + } +} + +function checkEvent(year, event) { + const where = `EVENTS["${year}"]`; + if (event.year !== year) + report(where, `year field "${event.year}" does not match its key`); + checkUrl(`${where}.programHref`, event.programHref); + checkLinks(`${where}.maps`, event.maps, { local: true }); + if (event.waivers) + checkLinks(`${where}.waivers`, event.waivers, { local: true }); + if (event.wristbandImage) { + checkUrl(`${where}.wristbandImage.src`, event.wristbandImage.src, { + local: true, + }); + } + if (event.feedback.kind === "link") + checkUrl(`${where}.feedback.href`, event.feedback.href); + + const scheduleKeys = new Set(); + for (const entry of event.schedule) { + if (scheduleKeys.has(entry.key)) + report(where, `duplicate schedule key ${entry.key}`); + scheduleKeys.add(entry.key); + if (entry.name.trim() === "" || entry.time.trim() === "") { + report( + `${where}.schedule[${entry.key}]`, + "time and name must be non-empty", + ); + } + } + + const titles = new Set(); + const embedIds = new Set(); + for (const section of event.workshops) { + for (const card of section.cards) { + const at = `${where} "${card.title}"`; + if (card.title.trim() === "") + report(where, "a workshop card has no title"); + else if (titles.has(card.title)) + report(at, "duplicate workshop title in this event"); + titles.add(card.title); + checkLinks(`${at} links`, card.links); + if (card.kind !== "archivedVideo") continue; + if (!/^[A-Za-z0-9_-]{6,}$/.test(card.embedId)) { + report(at, `embedId must be a bare YouTube id, got "${card.embedId}"`); + } else if (embedIds.has(card.embedId)) { + report(at, `embedId "${card.embedId}" is used by another card`); + } + embedIds.add(card.embedId); + } + } + + if (event.faq) { + const keys = new Set(event.faq.items.map((item) => item.key)); + if (keys.size !== event.faq.items.length) + report(`${where}.faq`, "duplicate FAQ keys"); + if ( + event.faq.initialOpenKey !== null && + !keys.has(event.faq.initialOpenKey) + ) { + report( + `${where}.faq.initialOpenKey`, + `${event.faq.initialOpenKey} matches no entry`, + ); + } + for (const item of event.faq.items) { + if (item.question.trim() === "" || item.answer.trim() === "") { + report( + `${where}.faq[${item.key}]`, + "question and answer must be non-empty", + ); + } + } + } +} + +function checkEvents(events, eventRoutes, registration) { + for (const [year, event] of Object.entries(events)) checkEvent(year, event); + + // EVENT_ROUTES drives both the router and the Resources menu, so a repeated + // path or label would silently shadow a published year. + const routes = new Set(); + const labels = new Set(); + const covered = new Set(); + for (const { route, year, navLabel } of eventRoutes) { + if (routes.has(route)) report("EVENT_ROUTES", `duplicate route "${route}"`); + routes.add(route); + if (!route.startsWith("/")) + report("EVENT_ROUTES", `route "${route}" is not an absolute path`); + if (navLabel.trim() === "") + report("EVENT_ROUTES", `"${route}" has no menu label`); + else if (labels.has(navLabel)) + report("EVENT_ROUTES", `duplicate menu label "${navLabel}"`); + labels.add(navLabel); + if (!Object.hasOwn(events, year)) + report("EVENT_ROUTES", `"${route}" names unknown year ${year}`); + covered.add(year); + } + for (const year of Object.keys(events)) { + if (!covered.has(year)) + report(`EVENTS["${year}"]`, "no EVENT_ROUTES entry reaches it"); + } + + if (!registration.alert.to.startsWith("/")) { + report( + "REGISTRATION_2021.alert.to", + `must be an in-app route, got "${registration.alert.to}"`, + ); + } + for (const [field, value] of Object.entries(registration.importantLinks)) { + if (Array.isArray(value)) + checkLinks(`REGISTRATION_2021.importantLinks.${field}`, value); + else if (value?.src) + checkUrl(`REGISTRATION_2021.importantLinks.${field}.src`, value.src); + } + checkLinks("REGISTRATION_2021.waivers.forms", registration.waivers.forms, { + local: true, + }); + // The combined-forms link is an external short URL, not a bundled PDF. + checkUrl( + "REGISTRATION_2021.waivers.allForms.src", + registration.waivers.allForms.src, + ); +} + +async function loadContent() { + const { createServer } = await import("vite"); + // The project config gives the same resolution the app and build use, so a + // module that loads here loads there. + const server = await createServer({ + root: repoRoot, + appType: "custom", + logLevel: "error", + server: { middlewareMode: true, watch: null, hmr: false }, + optimizeDeps: { noDiscovery: true }, + }); + try { + // Sequential: a missing module reports itself instead of being buried + // under the transport errors its siblings then raise. + const { people } = await server.ssrLoadModule("/src/content/people.ts"); + const { teamSections } = await server.ssrLoadModule( + "/src/content/teams.ts", + ); + const { pastSpeakers } = await server.ssrLoadModule( + "/src/content/speakers.ts", + ); + const events = await server.ssrLoadModule("/src/content/events/index.ts"); + return { people, teamSections, pastSpeakers, ...events }; + } finally { + await server.close(); + } +} + +let content; +try { + content = await loadContent(); +} catch (error) { + console.error("Could not load the content modules:"); + console.error(` ${error?.message ?? error}`); + process.exit(1); +} + +checkPeople(content.people); +checkTeams(content.teamSections, content.people, content.pastSpeakers); +checkEvents(content.EVENTS, content.EVENT_ROUTES, content.REGISTRATION_2021); + +if (problems.length > 0) { + console.error(`Content check failed with ${problems.length} problem(s):\n`); + for (const problem of problems) console.error(` ${problem}`); + process.exit(1); +} +console.log( + `Content check passed: ${Object.keys(content.people).length} people, ` + + `${content.teamSections.length} team sections, ${Object.keys(content.EVENTS).length} events.`, +); diff --git a/scripts/image-sources.json b/scripts/image-sources.json index c2e3ad5..8a6c60d 100644 --- a/scripts/image-sources.json +++ b/scripts/image-sources.json @@ -2,7 +2,7 @@ "_comment": [ "Inventory of every raster image actively rendered by the site, plus the", "encoding profile used to produce its optimized WebP variants.", - "Edit this file (never src/static/optimized/** or src/constants/optimizedImages.js)", + "Edit this file (never src/static/optimized/** or src/constants/optimizedImages.ts)", "and re-run `node scripts/images.js`. See `node scripts/images.js --help`.", "Keys are the ORIGINAL path relative to src/static; the originals stay in the", "repository so generation is repeatable." @@ -10,10 +10,10 @@ "sourceRoot": "src/static", "outputRoot": "src/static/optimized", "manifestPath": "src/static/optimized/manifest.json", - "mapPath": "src/constants/optimizedImages.js", + "mapPath": "src/constants/optimizedImages.ts", "profiles": { "portrait": { - "_comment": "People.jsx renders headshots in a fixed 160x160 box; 320px square covers 2x displays. Deliberate square crop, centered unless the source needs a focus override.", + "_comment": "People.tsx renders headshots in a fixed 160x160 box; 320px square covers 2x displays. Deliberate square crop, centered unless the source needs a focus override.", "type": "portrait", "size": 320, "quality": 82, @@ -53,8 +53,8 @@ "Asset families listed in allowUnoptimized are deliberately out of scope:", "vector art stays vector, and maps/PDFs are click-only downloads." ], - "roots": ["src/components", "src/constants"], - "extensions": [".js", ".jsx"], + "roots": ["src"], + "extensions": [".ts", ".tsx"], "allowUnoptimized": [ "svg/", "maps/", diff --git a/scripts/images.js b/scripts/images.js index a95f491..6154e48 100755 --- a/scripts/images.js +++ b/scripts/images.js @@ -3,7 +3,7 @@ * Image pipeline for exploretech.la. * * `node scripts/images.js` regenerate the optimized WebP assets, the - * manifest and src/constants/optimizedImages.js + * manifest and src/constants/optimizedImages.ts * `node scripts/images.js --check` validate the checked-in outputs (no external * tools required -- safe for CI / npm scripts) * `node scripts/images.js --help` usage, including how to install cwebp/ffprobe @@ -49,7 +49,7 @@ Inputs / outputs src/static/optimized/manifest.json generated record of every variant: dimensions, byte size, sha256, source sha256, quality used - src/constants/optimizedImages.js + src/constants/optimizedImages.ts generated explicit map, default export, keyed by the ORIGINAL src/static-relative path: images['team/leadership/sandra-pan.jpg'] @@ -57,7 +57,7 @@ Inputs / outputs { src, width, height, srcSet?, sizes? } Profiles - portrait deliberate square crop then a single <=320x320 variant (People.jsx + portrait deliberate square crop then a single <=320x320 variant (People.tsx renders a fixed 160x160 box, so 320 covers 2x displays). Sources smaller than 320px are never upscaled. content responsive variants at the profile widths (640/1280 by default), @@ -603,8 +603,11 @@ function importIdentifier(outputRelativePath, taken) { } function renderMap(config, manifest) { - // `static` is a bundler resolve alias (see vite.config.mjs / jsconfig.json). - const assetRoot = config.outputRoot.replace(/^src\//, ""); + // Relative imports work in both Vite and editor/type resolution. + const assetRoot = path + .relative(path.dirname(config.mapPath), config.outputRoot) + .split(path.sep) + .join("/"); const paths = new Set(); manifest.entries.forEach((entry) => entry.outputs.forEach((output) => paths.add(output.path)), @@ -623,7 +626,7 @@ function renderMap(config, manifest) { ); lines.push("// Each value spreads straight onto a native :"); lines.push("//"); - lines.push('// import images from "constants/optimizedImages";'); + lines.push('// import images from "../constants/optimizedImages";'); lines.push( '// Sandra Pan', ); @@ -697,7 +700,7 @@ const MAP_LOOKUP = /images\[\s*["'`]([^"'`\n]+)["'`]\s*\]/g; function scanReferences(config, keys, problems) { const scan = config.scan || {}; const roots = scan.roots || []; - const extensions = scan.extensions || [".js", ".jsx"]; + const extensions = scan.extensions || [".ts", ".tsx"]; const allowPrefixes = scan.allowUnoptimized || []; const allowPaths = scan.allowUnoptimizedPaths || []; diff --git a/scripts/serve-built-site.cjs b/scripts/serve-built-site.cjs new file mode 100755 index 0000000..985bfaf --- /dev/null +++ b/scripts/serve-built-site.cjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node +/** + * Static server for the production build that behaves like GitHub Pages. + * + * The deployed site is a single-page app served from the root of the + * `gh-pages` branch. Pages has no SPA rewrite: a deep path such as + * `/our_team/design` is a real 404, and the published `404.html` turns that + * path into `/?/our_team/design` so the restore script in `index.html` can + * hand it back to the router. A plain "serve index.html for everything" + * development server hides that hop, so deep links, trailing slashes and the + * not-found page all look healthier locally than in production. + * + * This server therefore: + * - serves real files from the build directory, + * - answers unknown document paths with `404.html` and a 404 status, + * - answers missing files under the emitted asset prefixes with a plain 404 + * instead of HTML, so a broken image or PDF URL fails loudly, + * - mirrors the `max-age=600` caching Pages was observed to send. + * + * Usage: + * node scripts/serve-built-site.cjs [--root build] [--port 4390] [--host 127.0.0.1] + * + * It prints `Serving at http://:/` once listening, which is + * what the Playwright `webServer` configuration waits for. It can also be + * required: `const { startSiteServer } = require('./serve-built-site.cjs')`. + */ + +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +const zlib = require("zlib"); + +const DEFAULT_ROOT = "build"; +const DEFAULT_PORT = 4390; +const DEFAULT_HOST = "127.0.0.1"; + +// Only the types the built site actually emits, plus the media contributors +// add. Anything else is served as a download rather than guessed. +const CONTENT_TYPES = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".pdf": "application/pdf", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", +}; + +// Paths the bundler owns. A miss here is a broken URL, never a route. +const ASSET_PREFIXES = ["/static/", "/assets/"]; + +const COMPRESSIBLE = /\.(html|css|js|mjs|json|map|svg|txt|xml)$/; + +function contentType(file) { + return ( + CONTENT_TYPES[path.extname(file).toLowerCase()] || + "application/octet-stream" + ); +} + +function isAssetPath(pathname) { + return ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix)); +} + +function resolveWithin(root, pathname) { + const candidate = path.resolve(root, "." + pathname); + if (candidate !== root && !candidate.startsWith(root + path.sep)) return null; + return candidate; +} + +function readableFile(file) { + try { + return fs.statSync(file).isFile() ? file : null; + } catch { + return null; + } +} + +function createSiteServer(options = {}) { + const root = path.resolve(options.root || DEFAULT_ROOT); + if (!readableFile(path.join(root, "index.html"))) { + throw new Error( + `No index.html in ${root}. Run \`npm run build\` first, or pass --root .`, + ); + } + const notFoundPage = readableFile(path.join(root, "404.html")); + + return http.createServer((req, res) => { + let pathname; + try { + pathname = decodeURIComponent( + new URL(req.url, "http://localhost").pathname, + ); + } catch { + res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Bad request URI"); + return; + } + + try { + const resolved = resolveWithin(root, pathname); + if (!resolved) { + res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Forbidden"); + return; + } + + let status = 200; + // GitHub Pages serves index.html for a directory that contains one. + let file = + readableFile(resolved) || + (pathname.endsWith("/") + ? readableFile(path.join(resolved, "index.html")) + : null); + + if (!file) { + if (isAssetPath(pathname)) { + res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(`Missing asset: ${pathname}`); + return; + } + // Unknown document path: Pages returns its 404 page, whose script + // rewrites the path into the `/?/...` query the SPA restores from. + if (!notFoundPage) { + res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Not found"); + return; + } + file = notFoundPage; + status = 404; + } + + const headers = { + "Content-Type": contentType(file), + "Cache-Control": "max-age=600", + }; + + if (req.method === "HEAD") { + headers["Content-Length"] = fs.statSync(file).size; + res.writeHead(status, headers); + res.end(); + return; + } + if (req.method !== "GET") { + res.writeHead(405, { Allow: "GET, HEAD" }); + res.end(); + return; + } + + let body = fs.readFileSync(file); + if ( + COMPRESSIBLE.test(file) && + (req.headers["accept-encoding"] || "").includes("gzip") + ) { + body = zlib.gzipSync(body); + headers["Content-Encoding"] = "gzip"; + headers.Vary = "Accept-Encoding"; + } + headers["Content-Length"] = body.length; + res.writeHead(status, headers); + res.end(body); + } catch (error) { + res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(String((error && error.message) || error)); + } + }); +} + +function startSiteServer(options = {}) { + const host = options.host || DEFAULT_HOST; + const port = Number(options.port ?? DEFAULT_PORT); + const server = createSiteServer(options); + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, host, () => { + server.removeListener("error", reject); + resolve(server); + }); + }); +} + +function parseArgs(argv) { + const options = { + root: process.env.SITE_ROOT || DEFAULT_ROOT, + port: process.env.PORT || DEFAULT_PORT, + host: process.env.HOST || DEFAULT_HOST, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + const next = () => { + const value = argv[index + 1]; + if (value === undefined) throw new Error(`${arg} needs a value`); + index += 1; + return value; + }; + if (arg === "--root") options.root = next(); + else if (arg === "--port") options.port = next(); + else if (arg === "--host") options.host = next(); + else if (arg === "--help" || arg === "-h") options.help = true; + else throw new Error(`Unknown argument: ${arg}`); + } + return options; +} + +if (require.main === module) { + let options; + try { + options = parseArgs(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + process.exit(2); + } + if (options.help) { + console.log( + "Usage: node scripts/serve-built-site.cjs [--root build] [--port 4390] [--host 127.0.0.1]", + ); + process.exit(0); + } + startSiteServer(options).then( + (server) => { + const { port } = server.address(); + console.log( + `Serving ${path.resolve(options.root)} at http://${options.host}:${port}/`, + ); + const shutdown = () => server.close(() => process.exit(0)); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + }, + (error) => { + console.error(String((error && error.message) || error)); + process.exit(1); + }, + ); +} + +module.exports = { + createSiteServer, + startSiteServer, + DEFAULT_PORT, + DEFAULT_HOST, +}; diff --git a/scripts/visual-parity.cjs b/scripts/visual-parity.cjs new file mode 100644 index 0000000..16ace64 --- /dev/null +++ b/scripts/visual-parity.cjs @@ -0,0 +1,203 @@ +// Capture a built site, or compare it with a baseline from the same browser. +// PLAYWRIGHT_MODULE optionally selects an existing local Playwright installation. +const { chromium, firefox, webkit } = require( + process.env.PLAYWRIGHT_MODULE || "@playwright/test", +); +const fs = require("node:fs/promises"); +const path = require("node:path"); +const assert = require("node:assert/strict"); +const routes = [ + "/", + "/our_team/leadership", + "/our_team/content", + "/our_team/design", + "/our_team/operations", + "/our_team/external", + "/our_team/web-dev", + "/our_team/marketing", + "/ignite", + "/register", + "/resources", + "/resources2023", + "/resources2026", + "/not-found", +]; +const widths = ( + process.env.WIDTHS || + "320,390,575,576,577,767,768,769,991,992,993,1199,1200,1201,1440" +) + .split(",") + .map(Number); +const base = process.env.SITE_URL || "http://127.0.0.1:4391"; +const output = + process.env.OUTPUT_DIR || "/tmp/exploretech-refactor-baseline/visual"; +const engine = process.env.ENGINE || "chromium"; +(async () => { + await fs.mkdir(output, { recursive: true }); + const browser = await { chromium, firefox, webkit }[engine].launch(); + const results = []; + try { + for (const width of widths) + for (const route of routes) { + const context = await browser.newContext({ + viewport: { width, height: 900 }, + reducedMotion: "reduce", + }); + const page = await context.newPage(); + const errors = []; + page.on("pageerror", (error) => errors.push(error.message)); + await context.route("**/*", (request) => { + const url = new URL(request.request().url()); + if ( + url.origin === new URL(base).origin || + ["fonts.googleapis.com", "fonts.gstatic.com"].includes(url.hostname) + ) + return request.continue(); + return request.fulfill({ + status: 200, + contentType: "text/html", + body: "External content intercepted for visual capture", + }); + }); + await page.goto(base + route, { waitUntil: "networkidle" }); + await page.evaluate(() => document.fonts.ready); + await page.addStyleTag({ + content: + "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}html{scroll-behavior:auto!important}", + }); + // Reveal lazy media before recording, then return to the initial viewport. + await page.evaluate(async () => { + for (let y = 0; y < document.body.scrollHeight; y += 700) { + scrollTo(0, y); + await new Promise((r) => setTimeout(r, 20)); + } + scrollTo(0, 0); + await Promise.all( + Array.from(document.images) + .filter((i) => i.loading !== "lazy" || i.complete) + .map((i) => i.decode().catch(() => {})), + ); + }); + const snapshot = await page.evaluate(() => { + const selectors = + ".Header,.Hero,.team-title,.team-navigation,.team-content,.People,.person-image,.Footer,section,h1,h2,h3,h4,p,.card,.content-card,.video-frame,.btn,.action,table"; + const props = [ + "fontFamily", + "fontSize", + "fontWeight", + "lineHeight", + "color", + "backgroundColor", + "paddingTop", + "paddingRight", + "paddingBottom", + "paddingLeft", + "marginTop", + "marginRight", + "marginBottom", + "marginLeft", + "borderRadius", + "objectFit", + "objectPosition", + "display", + "flexDirection", + "flexWrap", + "gap", + "overflowX", + ]; + return { + text: document.body.innerText.replace(/\s+/g, " ").trim(), + title: document.title, + links: Array.from(document.querySelectorAll("a[href]")).map( + (a) => ({ + text: a.textContent.trim(), + href: a.getAttribute("href"), + target: a.target, + }), + ), + images: Array.from(document.images).map((i) => ({ + alt: i.alt, + src: new URL(i.src).pathname, + loading: i.loading, + width: i.width, + height: i.height, + })), + overflow: document.documentElement.scrollWidth > innerWidth, + geometry: Array.from(document.querySelectorAll(selectors)).map( + (e) => { + const r = e.getBoundingClientRect(), + s = getComputedStyle(e); + return { + tag: e.tagName, + class: e.className, + x: r.x, + y: r.y, + width: r.width, + height: r.height, + style: Object.fromEntries(props.map((p) => [p, s[p]])), + }; + }, + ), + }; + }); + const name = `${engine}-${width}-${route.replaceAll("/", "_") || "home"}`; + await page.screenshot({ + path: path.join(output, name + ".png"), + fullPage: true, + animations: "disabled", + }); + await fs.writeFile( + path.join(output, name + ".json"), + JSON.stringify(snapshot, null, 2), + ); + let maxGeometryDelta = 0; + if (process.env.BASELINE_DIR) { + const before = JSON.parse( + await fs.readFile( + path.join(process.env.BASELINE_DIR, name + ".json"), + "utf8", + ), + ); + const comparable = structuredClone(snapshot); + assert.equal( + snapshot.geometry.length, + before.geometry.length, + `Element count changed: ${name}`, + ); + for (let index = 0; index < snapshot.geometry.length; index++) { + // Class names belong to the implementation, not the visual contract. + // Retain them in raw snapshots for diagnosis; compare their rendered effects. + delete comparable.geometry[index].class; + delete before.geometry[index].class; + for (const key of ["x", "y", "width", "height"]) { + const delta = Math.abs( + snapshot.geometry[index][key] - before.geometry[index][key], + ); + maxGeometryDelta = Math.max(maxGeometryDelta, delta); + // Two Chromium layout quanta can differ when React changes text-node boundaries. + // Keep the raw measurements; tolerate less than 1/20th of a CSS pixel only. + assert.ok( + delta <= 0.05, + `${name} geometry[${index}].${key} changed by ${delta}px`, + ); + comparable.geometry[index][key] = before.geometry[index][key]; + } + } + assert.deepEqual(comparable, before, `Parity failed: ${name}`); + } + assert.deepEqual(errors, [], `Application errors: ${name}`); + results.push({ route, width, name, status: "pass", maxGeometryDelta }); + await fs.writeFile( + path.join(output, "results.json"), + JSON.stringify(results, null, 2), + ); + console.log("Captured", name); + await context.close(); + } + } finally { + await browser.close(); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 1b16a6b..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { - Switch, - Route, - Redirect, - useHistory, - useLocation, -} from "react-router-dom"; - -import GA from "util/GoogleAnalytics"; - -import Header from "components/common/Header"; -import Home from "components/Home"; -import Register from "components/Registration/Register"; -import Resources from "components/Resources/Resources"; -import Resources2023 from "components/Resources2023/Resources"; -import Resources2026 from "components/Resources2026/Resources"; -import Team from "components/Team/Team"; -import PageNotFound from "components/PageNotFound"; -import Ignite from "components/Ignite/Ignite"; - -import "./App.scss"; - -export default function App() { - const { pathname, search, hash, key } = useLocation(); - const history = useHistory(); - const initialNavigation = useRef(true); - - useEffect(() => { - // Redirects can replace history before this render's effect runs. - if ( - history.location.pathname !== pathname || - history.location.search !== search - ) - return; - if (GA.init()) { - GA.trackPageView(pathname + search); - } - }, [pathname, search, history]); - - useEffect(() => { - const initial = initialNavigation.current; - initialNavigation.current = false; - if (!initial && history.action === "POP") return; - if (hash) { - const target = document.getElementById(hash.slice(1)); - if (target) target.scrollIntoView(); - } else { - window.scrollTo(0, 0); - } - }, [pathname, hash, key, history]); - - return ( -
-
- - - - - - - } - /> - - - - -
- ); -} diff --git a/src/App.scss b/src/App.scss deleted file mode 100644 index cb1c652..0000000 --- a/src/App.scss +++ /dev/null @@ -1,138 +0,0 @@ -@import "bootstrap/scss/bootstrap"; - -// See colors in the exploretech.la Design System in Figma -$color-aqua: #98d6d6; -$color-muted-blue: #448494; -$color-navy: #0b546f; -$color-teal: #57ada3; -$color-white: #ffffff; - -$section-width: 1040px; -$section-horizontal-padding-xs: 24px; -$section-horizontal-padding-sm: 40px; -$section-horizontal-padding-md: 52px; -$section-horizontal-padding-lg: 64px; -$section-vertical-padding: 64px; - -@keyframes bobble { - 0% { - transform: translateY(0); - } - - 100% { - transform: translateY(-1rem); - } -} - -.pill-divider { - height: 0.5rem; - width: 4rem; - margin-bottom: 1.5rem; - background: $color-aqua; - border-radius: 0.25rem; -} - -html { - scroll-behavior: smooth; -} - -.App { - section, - .Section { - padding-top: $section-vertical-padding; - padding-bottom: $section-vertical-padding; - padding-left: $section-horizontal-padding-xs; - padding-right: $section-horizontal-padding-xs; - } - - /* Height */ - ::-webkit-scrollbar { - height: 2px; - } - - /* Track */ - ::-webkit-scrollbar-track { - background: #ebebeb; - border-radius: 1px; - } - - /* Handle */ - ::-webkit-scrollbar-thumb { - background: $color-aqua; - border-radius: 1px; - } -} - -@include media-breakpoint-down(xs) { - .App { - overflow-x: hidden; - } -} - -@include media-breakpoint-up(sm) { - .App { - section, - .Section { - padding-left: $section-horizontal-padding-sm; - padding-right: $section-horizontal-padding-sm; - } - - /* Height */ - ::-webkit-scrollbar { - height: 4px; - } - - /* Track */ - ::-webkit-scrollbar-track { - border-radius: 2px; - } - - /* Handle */ - ::-webkit-scrollbar-thumb { - border-radius: 2px; - } - } -} - -@include media-breakpoint-up(md) { - .App { - section, - .Section { - padding-left: $section-horizontal-padding-md; - padding-right: $section-horizontal-padding-md; - } - - ::-webkit-scrollbar { - display: none; - } - } -} - -@include media-breakpoint-up(lg) { - .App { - section, - .Section { - padding-left: $section-horizontal-padding-lg; - padding-right: $section-horizontal-padding-lg; - } - } -} - -// Common Component Styles -@import "components/common/Header.scss"; -@import "components/common/LinkWithArrow.scss"; -@import "components/common/UnorderedItemList.scss"; -@import "components/common/People.scss"; - -// Page Styles -@import "components/Home.scss"; -@import "components/PageNotFound.scss"; -@import "components/Registration/Register.scss"; -@import "components/Ignite/Ignite.scss"; -@import "components/Resources/Resources.scss"; -@import "components/Team/Team.scss"; -@import "components/Team/Leadership.scss"; -@import "components/Team/Content.scss"; -@import "components/Team/Design.scss"; -@import "components/Team/Operations.scss"; -@import "components/Team/External.scss"; diff --git a/src/App.test.jsx b/src/App.test.jsx deleted file mode 100644 index 209c42a..0000000 --- a/src/App.test.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import { vi } from "vitest"; -import React from "react"; -import ReactDOM from "react-dom"; -import { act } from "react-dom/test-utils"; -import { Router } from "react-router-dom"; -import { createMemoryHistory } from "history"; -import App from "./App"; -import GA from "util/GoogleAnalytics"; - -vi.mock("util/GoogleAnalytics", () => ({ - default: { - init: vi.fn(() => true), - trackPageView: vi.fn(), - trackEvent: vi.fn(), - }, -})); - -let container; -let scrollTo; -const originalScrollIntoView = Element.prototype.scrollIntoView; - -beforeEach(() => { - vi.clearAllMocks(); - container = document.createElement("div"); - document.body.appendChild(container); - scrollTo = vi.spyOn(window, "scrollTo").mockImplementation(() => {}); - Element.prototype.scrollIntoView = vi.fn(); -}); - -afterEach(() => { - act(() => { - ReactDOM.unmountComponentAtNode(container); - }); - container.remove(); - scrollTo.mockRestore(); - if (originalScrollIntoView) - Element.prototype.scrollIntoView = originalScrollIntoView; - else delete Element.prototype.scrollIntoView; -}); - -it.each(["/our_team", "/our_team/"])( - "records only the resolved page when entering %s", - (path) => { - const history = createMemoryHistory({ initialEntries: [path] }); - act(() => { - ReactDOM.render( - - - , - container, - ); - }); - expect(history.location.pathname).toBe("/our_team/leadership"); - expect(GA.trackPageView.mock.calls).toEqual([["/our_team/leadership"]]); - }, -); - -it("records a new route with its query but not another view for an anchor", () => { - const history = createMemoryHistory({ initialEntries: ["/ignite"] }); - act(() => { - ReactDOM.render( - - - , - container, - ); - }); - act(() => history.push("/our_team/leadership?source=local")); - act(() => history.push("/our_team/leadership?source=local#leadership")); - expect(GA.trackPageView.mock.calls).toEqual([ - ["/ignite"], - ["/our_team/leadership?source=local"], - ]); -}); diff --git a/src/app/App.test.tsx b/src/app/App.test.tsx new file mode 100644 index 0000000..6b4b706 --- /dev/null +++ b/src/app/App.test.tsx @@ -0,0 +1,65 @@ +import { vi } from "vitest"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { + createMemoryRouter, + RouterProvider, + type DataRouter, +} from "react-router-dom"; +import App from "./App"; +import GA from "../util/GoogleAnalytics"; + +vi.mock("../util/GoogleAnalytics", () => ({ + default: { + init: vi.fn(() => true), + trackPageView: vi.fn(), + trackEvent: vi.fn(), + trackOutbound: vi.fn(), + }, +})); +let container: HTMLDivElement; +let root: Root; +let router: DataRouter; + +beforeEach(() => { + vi.clearAllMocks(); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + vi.spyOn(window, "scrollTo").mockImplementation(() => {}); +}); +afterEach(async () => { + await act(async () => root.unmount()); + router?.dispose(); + container.remove(); + vi.restoreAllMocks(); +}); +async function renderPath(path: string) { + router = createMemoryRouter([{ path: "*", element: }], { + initialEntries: [path], + }); + await act(async () => root.render()); +} +it.each(["/our_team", "/our_team/"])( + "records only the resolved page when entering %s", + async (path) => { + await renderPath(path); + expect(router.state.location.pathname).toBe("/our_team/leadership"); + expect(vi.mocked(GA.trackPageView).mock.calls).toEqual([ + ["/our_team/leadership"], + ]); + }, +); +it("records a new route with its query but not another view for an anchor", async () => { + await renderPath("/ignite"); + await act(async () => { + await router.navigate("/our_team/leadership?source=local"); + }); + await act(async () => { + await router.navigate("/our_team/leadership?source=local#leadership"); + }); + expect(vi.mocked(GA.trackPageView).mock.calls).toEqual([ + ["/ignite"], + ["/our_team/leadership?source=local"], + ]); +}); diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..1592707 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,57 @@ +import { useEffect, useRef } from "react"; +import { + Navigate, + Route, + Routes, + useLocation, + useNavigationType, +} from "react-router-dom"; +import GA from "../util/GoogleAnalytics"; +import Header from "../components/common/Header"; +import Home from "../features/home/Home"; +import RegistrationPage from "../features/registration/RegistrationPage"; +import EventPage from "../features/events/EventPage"; +import Team from "../features/team/Team"; +import PageNotFound from "../components/PageNotFound"; +import Ignite from "../features/ignite/Ignite"; + +import { EVENT_ROUTES } from "../content/events"; +export default function App() { + const { pathname, search, hash, key } = useLocation(); + const navigationType = useNavigationType(); + const initialNavigation = useRef(true); + const redirecting = /^\/our_team\/?$/i.test(pathname); + + useEffect(() => { + // Alias routes never represent a displayed page. Track only their destination. + if (!redirecting && GA.init()) GA.trackPageView(pathname + search); + }, [pathname, search, redirecting]); + + useEffect(() => { + const initial = initialNavigation.current; + initialNavigation.current = false; + if (!initial && navigationType === "POP") return; + if (hash) document.getElementById(hash.slice(1))?.scrollIntoView(); + else window.scrollTo(0, 0); + }, [pathname, hash, key, navigationType]); + + return ( +
+
+ + } /> + } /> + {EVENT_ROUTES.map(({ route, year }) => ( + } /> + ))} + } + /> + } /> + } /> + } /> + +
+ ); +} diff --git a/src/app/main.tsx b/src/app/main.tsx new file mode 100644 index 0000000..404d286 --- /dev/null +++ b/src/app/main.tsx @@ -0,0 +1,12 @@ +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import "../styles/theme.css"; + +const root = document.getElementById("root"); +if (!root) throw new Error("The application root is missing from index.html"); +createRoot(root).render( + + + , +); diff --git a/src/components/About.scss b/src/components/About.scss deleted file mode 100644 index 97ffe07..0000000 --- a/src/components/About.scss +++ /dev/null @@ -1,97 +0,0 @@ -.About { - .about-content { - display: flex; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.about-org { - flex-direction: column-reverse; - } - - &.about-team { - flex-direction: column; - } - - .left-column, - .right-column { - &.text { - padding-top: 32px; - } - } - - .founding-story, - .current-team { - a { - display: flex; - flex-direction: row; - align-items: center; - text-decoration: none; - color: $color-muted-blue; - - &:hover { - .MdKeyboardArrowRight { - fill: white; - background-color: $color-muted-blue; - border: 2px solid $color-muted-blue; - } - } - - p { - margin-bottom: 0; - margin-right: 0.5rem; - } - } - } - - img { - max-width: 100%; - max-height: 100vh; - height: auto; - } - } - - .MdKeyboardArrowRight { - fill: $color-muted-blue; - border: 2px solid #e2e2e2; - border-radius: 50%; - width: 1.5rem; - min-width: 1.5rem; - height: 1.5rem; - transition: all 0.3s ease-in-out; - -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ - -moz-transition: all 0.3s ease-in-out; /** Firefox **/ - -o-transition: all 0.3s ease-in-out; /** Opera **/ - } -} - -@include media-breakpoint-up(md) { - .About { - .about-content { - justify-content: space-between; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.about-org, - &.about-team { - flex-direction: row; - } - - .left-column, - .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} diff --git a/src/components/ActionLink.tsx b/src/components/ActionLink.tsx new file mode 100644 index 0000000..7d84ecb --- /dev/null +++ b/src/components/ActionLink.tsx @@ -0,0 +1,22 @@ +import type { AnchorHTMLAttributes } from "react"; + +/** + * React Bootstrap rendered ` - - - - - ); - } - - _trackGetInvolvedClick() { - GA.trackEvent({ - category: "Hero", - action: "Click", - label: "Get Involved", - }); - } - - _trackRegisterTodayClick() { - GA.trackEvent({ - category: "Hero", - action: "Click", - label: "Register Today", - }); - } - - _trackLearnMoreClick() { - GA.trackEvent({ - category: "Hero", - action: "Click", - label: "Learn More", - }); - } -} diff --git a/src/components/Hero.scss b/src/components/Hero.scss deleted file mode 100644 index 5d556ff..0000000 --- a/src/components/Hero.scss +++ /dev/null @@ -1,101 +0,0 @@ -.Hero { - height: 450px; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - background-color: $color-aqua; - color: $color-navy; - - .logo-with-icon-group { - width: 20rem; - height: auto; - margin-bottom: 2rem; - padding-left: 24px; - padding-right: 24px; - } - - .hero-content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - - .h1-title { - display: none; - } - - .h2-title { - display: block; - } - - .hero-cta { - margin-top: 1rem; - display: flex; - flex-direction: row; - - .btn:not(:last-child) { - margin-right: 1rem; - } - } - } - - .expand-icon { - position: absolute; - bottom: 2rem; - cursor: pointer; - animation: bobble 1s infinite alternate; - } - - p { - margin: 0; - } -} - -@include media-breakpoint-up(sm) { - .Hero { - .logo-with-icon-group { - width: 25rem; - } - - .hero-content { - .h1-title { - display: block; - } - - .h2-title { - display: none; - } - } - } -} - -@include media-breakpoint-up(md) { - .Hero { - flex-direction: row; - - .logo-with-icon-group { - margin-right: 3rem; - margin-bottom: 0; - padding-left: 0; - padding-right: 0; - } - - .hero-content { - justify-content: flex-start; - align-items: flex-start; - text-align: left; - max-width: 400px; - } - } -} - -@include media-breakpoint-up(lg) { - .Hero { - .logo-with-icon-group { - width: 30rem; - margin-right: 4rem; - } - } -} diff --git a/src/components/Home.jsx b/src/components/Home.jsx deleted file mode 100644 index ba87dab..0000000 --- a/src/components/Home.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import Hero from './Hero'; -// import Alerts from './common/Alerts' -import About from './About'; -import GetInvolved from './GetInvolved'; -import Speakers from './Speakers'; -import Sponsors from './Sponsors'; -import Footer from './Footer'; -// import RegistrationAlert from './Registration/RegistrationAlert'; - - -function Home() { - return ( -
- - {/* */} - {/* */} - - - - -
-
- ); -} - -Home.displayName = "Home"; -export default Home; \ No newline at end of file diff --git a/src/components/Home.scss b/src/components/Home.scss deleted file mode 100644 index d19b73b..0000000 --- a/src/components/Home.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Home Component Styles -@import "./common/Alerts.scss"; -@import "./Hero.scss"; -@import "./About.scss"; -@import "./GetInvolved.scss"; -@import "./Speakers.scss"; -@import "./Sponsors.scss"; -@import "./Footer.scss"; diff --git a/src/components/Ignite/Ignite.jsx b/src/components/Ignite/Ignite.jsx deleted file mode 100644 index bcdfc33..0000000 --- a/src/components/Ignite/Ignite.jsx +++ /dev/null @@ -1,290 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; -import Button from "react-bootstrap/Button"; -import Footer from "components/Footer"; -import PageNotFound from "components/PageNotFound"; -import images from "constants/optimizedImages"; - -// Workshop cards sit three-up on desktop; the capstone and apply photos are -// single large images inside their own containers. -const WorkshopImageSizes = "(min-width: 992px) 33vw, 100vw"; -const CapstoneImageSizes = "(min-width: 1000px) 900px, 90vw"; -const ApplyImageSizes = "(min-width: 992px) 50vw, 100vw"; - -// Every image on this page is below the header section, so they are all lazy. -const lazyPhoto = (key, sizes) => ({ - ...images[key], - sizes, - loading: "lazy", - decoding: "async", -}); - -const IGNITE_ENABLED = true; // gate - -export default class Ignite extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Ignite", this.props.className); - - if (!IGNITE_ENABLED) { - return ; - } - - return ( -
-
-
-

Ignite

-
-

- Ignite is a 4-week series of beginner-friendly workshops - offering an in-depth exploration of current trending topics in - technology. It is designed to inspire and encourage high school - students to pursue a career in the STEM/Tech field. -

-
-
- - -
-
-
- -
-
-

Program Structure

-
-
-

Week 1: Foundation

-

- Building core skills and understanding key concepts. -

-
-
-

Week 2: Development

-

- Hands-on learning and practical application. -

-
-
-

Week 3: Integration

-

- Advanced concepts and team collaboration. -

-
-
-

- Week 4: -
- Capstone -

-

- Project development and final presentation. -

-
-
-
-
- -
-
-

2025 Workshops

-
-
-

Introduction to Python

-

- Dive into one of the most versatile and widely used - programming languages. This track will teach you essential - Python concepts and guide you through practical projects, - perfect for beginners. -

-
- Introduction to Python -
-
-
-

Game Development with Roblox

-

- Design and develop an exciting speed run parkour game using - Roblox Studio. From building dynamic levels to scripting game - mechanics with Lua, you'll gain hands-on experience in game - creation. -

-
- Game Development with Roblox -
-
-
-

Web Development

-

- Learn how websites are built using HTML and CSS. Build your - skills throughout the sessions and create your very own custom - website as a capstone project. -

-
- Web Development -
-
-
-
-
- -
-
-

2024 Workshops

-
-
-

Introduction to Python

-

- Dive into one of the most versatile and widely used - programming languages. This track will teach you essential - Python concepts and guide you through practical projects, - perfect for beginners. -

-
- Introduction to Python -
-
-
-

Game Development with Unity

-

- Design and develop an exciting speed run parkour game using - Roblox Studio. From building dynamic levels to scripting game - mechanics with Lua, you'll gain hands-on experience in game - creation. -

-
- Game Development with Unity -
-
-
-

Machine Learning

-

- Learn how websites are built using HTML and CSS. Build your - skills throughout the sessions and create your very own custom - website as a capstone project. -

-
- Machine Learning -
-
-
-
-
- -
-
-

Capstone Project

-

- The program culminates in a capstone project... -

-
- Capstone Project Presentation -
-
-
- -
-
-
-

Apply Now!

-

- Applications for the next IGNITE are open... -

- -
- -
-
- Students Workshop -
-
-
-
- -
-
- ); - } -} diff --git a/src/components/Ignite/Ignite.scss b/src/components/Ignite/Ignite.scss deleted file mode 100644 index 8cd0981..0000000 --- a/src/components/Ignite/Ignite.scss +++ /dev/null @@ -1,492 +0,0 @@ -// Variables for the gradient and layout -$ignite-teal-dark: #2ca6bc; -$ignite-teal-light: #ddf6f8; -$ignite-white: #ffffff; -$ignite-text-dark: #06546e; -$max-content-width: 1140px; - -.Ignite { - width: 100%; - min-height: 100vh; - - .large-wireframe-display { - width: 100%; - - .display-placeholder { - width: 100%; - aspect-ratio: 16 / 9; - background: #333; - color: #aaa; - display: flex; - align-items: center; - justify-content: center; - border-radius: 12px; - font-size: 1.5rem; - border: 2px solid #555; - } - } - - // General Section Layout - .ignite-section { - width: 100%; - padding: 80px 20px; - display: flex; - justify-content: center; - - .content-wrapper { - width: 100%; - max-width: $max-content-width; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - } - } - - // 1. Header Section Styles - .header-section { - padding-top: 100px; - padding-bottom: 80px; - color: white; - - /* NEW — solid blue background */ - background: $ignite-teal-dark; - - .ignite-title { - font-size: 7rem; - font-weight: 700; - letter-spacing: -2px; - margin-bottom: 42px; - line-height: 1; - } - - .ignite-description { - max-width: 800px; - font-size: 1.25rem; - line-height: 1.5; - margin-bottom: 40px; - font-weight: 400; - color: #d0ecf0; - } - - .header-buttons { - display: flex; - gap: 20px; - - .btn-apply { - background-color: #07556f; - border: none; - padding: 12px 18px; - font-size: 1.1rem; - font-weight: 400; - border-radius: 1rem; - } - - .btn-previous { - border: 2px solid #06546f; - color: #06546f; - padding: 12px 18px; - font-size: 1.1rem; - background: transparent; - border-radius: 1rem; - font-weight: 400; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - } - } - } - - // 2. Structure Section - .structure-section { - - /* NEW — blue → white gradient */ - background: linear-gradient( - to bottom, - $ignite-teal-dark 0%, - $ignite-teal-light 100% - ); - - .section-header { - font-size: 3.5rem; - font-weight: 700; - margin-bottom: 40px; - color: white; - } - - .program-cards-container { - display: flex; - flex-direction: column; - gap: 20px; - width: 100%; - - @media (min-width: 768px) { - flex-direction: row; - gap: 14px; - } - } - - .program-card { - background: rgba(255, 255, 255, 0.2); - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - backdrop-filter: blur(10px); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - transition: all 0.3s ease; - opacity: 1; - - .card-title { - font-size: 1.5rem; - font-weight: 700; - color: white; - margin-bottom: 10px; - line-height: 1.2; - } - - .card-description { - font-size: 1rem; - color: rgba(255, 255, 255, 0.9); - margin-bottom: 8px; - line-height: 1.5; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - min-height: 3em; // Ensures consistent height for 2 lines - } - - .card-image { - width: 100%; - height: 240px; // Fixed height for consistent image size - border-radius: 12px; - overflow: hidden; - margin-top: auto; - background: rgba(0, 0, 0, 0.1); - border: 3px solid white; - flex-shrink: 0; // Prevent image from shrinking - margin-top: 0; // Remove auto margin to bring image closer to text - - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - } - } - } - - // Hover effect: highlight hovered card, dull others - .program-cards-container:hover .program-card { - opacity: 0.6; - transform: scale(0.98); - } - - .program-cards-container .program-card:hover { - opacity: 1; - transform: translateY(-8px) scale(1.02); - background: rgba(255, 255, 255, 0.3); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); - z-index: 10; - position: relative; - } - } - - // 3 & 4. Workshop Sections (Light Background Area) - .workshop-section { - - /* NEW — solid light background */ - background: $ignite-teal-light; - - .section-header { - font-size: 3rem; - font-weight: 700; - margin-bottom: 40px; - color: #06546f; - } - - .workshop-cards-container { - display: flex; - flex-direction: column; - gap: 20px; - width: 100%; - - @media (min-width: 768px) { - flex-direction: row; - gap: 14px; - } - } - - .workshop-card { - background: #1cb0c6; - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - flex: 1; - min-height: 450px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - transition: all 0.3s ease; - opacity: 1; - - .card-title { - font-size: 1.5rem; - font-weight: 700; - color: white; - margin-bottom: 10px; - line-height: 1.2; - } - - .card-description { - font-size: 1rem; - color: rgba(255, 255, 255, 0.9); - margin-bottom: 8px; - line-height: 1.5; - } - - .card-image { - width: 100%; - height: 240px; // Fixed height for consistent image size - border-radius: 12px; - overflow: hidden; - margin-top: auto; - background: rgba(0, 0, 0, 0.1); - border: 3px solid white; - flex-shrink: 0; // Prevent image from shrinking - margin-top: 0; // Remove auto margin to bring image closer to text - - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - } - } - } - - // Hover effect: highlight hovered card, dull others - .workshop-cards-container:hover .workshop-card { - opacity: 0.6; - transform: scale(0.98); - } - - .workshop-cards-container .workshop-card:hover { - opacity: 1; - transform: translateY(-8px) scale(1.02); - background: #2ca6bc; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); - z-index: 10; - position: relative; - } - } - - // 5. Capstone Section - .capstone-section { - - /* NEW — light grey background */ - background: #f5f5f5; - - .section-header { - font-size: 3rem; - font-weight: 700; - margin-bottom: 10px; - color: #06546f; - } - - .section-subtitle { - color: #6796a5; - margin-bottom: 30px; - } - - .capstone-image-container { - width: 90%; - max-width: 900px; - margin: 0 auto; - border-radius: 16px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - - img { - width: 100%; - height: auto; - display: block; - } - } - } - - // 6. Apply Section (Bottom Dark Gradient) - .footer-apply-section { - color: white; - padding-top: 80px; - padding-bottom: 60px; - - /* NEW — gradient from light blue to darker teal */ - background: linear-gradient( - to bottom, - #a8d8e8 0%, - $ignite-teal-dark 100% - ); - - .content-wrapper { - flex-direction: column; - - @media (min-width: 768px) { - flex-direction: row !important; - align-items: flex-start; - justify-content: space-between; - } - } - - .split-layout { - display: flex; - flex-direction: column; - gap: 40px; - align-items: stretch; - } - - .left-text-col { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - text-align: left; - - .align-left { - text-align: left; - font-size: 4rem; - font-weight: 700; - margin-bottom: 15px; - line-height: 1; - color: $ignite-white; - } - - .section-subtitle-small { - font-size: 1.1rem; - margin-top: 15px; - margin-bottom: 30px; - color: white; - } - - .btn-apply-large { - background-color: #07556f; - border: none; - padding: 12px 40px; - font-size: 1.1rem; - font-weight: 400; - border-radius: 8px; - margin-top: 10px; - color: white; - cursor: pointer; - width: 100%; - } - } - - .right-image-col { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - - .apply-image { - width: 100%; - border-radius: 20px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); - - img { - width: 100%; - height: auto; - display: block; - } - } - } - } - - /* DESKTOP FIX — PURE CSS, ALWAYS APPLIES */ - @media (min-width: 768px) { - .footer-apply-section .left-text-col { - width: 50% !important; - } - - .footer-apply-section .right-image-col { - width: 50% !important; - } - - .footer-apply-section .btn-apply-large { - width: 100% !important; - } - - .Ignite .footer-apply-section .split-layout { - flex-direction: row !important; - gap: 60px; - align-items: stretch; - } - - .Ignite .footer-apply-section .left-text-col { - width: 40% !important; - } - - .Ignite .footer-apply-section .right-image-col { - width: 55% !important; - } - } - - // 7. Gallery Section - .gallery-section { - color: white; - padding-top: 60px; - padding-bottom: 100px; - - /* NEW — solid teal background */ - background: $ignite-teal-dark; - - .section-header { - font-size: 3.5rem; - font-weight: 700; - margin-bottom: 10px; - color: white; - } - - .section-subtitle { - font-size: 1.1rem; - margin-bottom: 50px; - opacity: 0.85; - color: rgba(255, 255, 255, 0.85); - } - - .gallery-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; - width: 100%; - - @media (max-width: 768px) { - grid-template-columns: repeat(2, 1fr); - } - - @media (max-width: 480px) { - grid-template-columns: 1fr; - } - - .gallery-item { - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - aspect-ratio: 4 / 3; - - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - } - } - } - } -} diff --git a/src/components/Leadership.jsx b/src/components/Leadership.jsx deleted file mode 100644 index 5ce4489..0000000 --- a/src/components/Leadership.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import HomePageSections from "constants/HomePageSections"; -import { Leadership } from "constants/leadership"; - -export default class Speakers extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Leadership", - this.props.className - ); - return ( -
-
-
-

Our Leadership

-
-
- -
-
- ); - } -} diff --git a/src/components/Leadership.scss b/src/components/Leadership.scss deleted file mode 100644 index 0a92709..0000000 --- a/src/components/Leadership.scss +++ /dev/null @@ -1,25 +0,0 @@ -.Leadership { - background-color: #f5f5f5; - - .leadership-container { - max-width: $section-width; - margin: auto; - - .leadership-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .Leadership { - .leadership-container { - .leadership-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/OutboundLink.tsx b/src/components/OutboundLink.tsx new file mode 100644 index 0000000..75962ba --- /dev/null +++ b/src/components/OutboundLink.tsx @@ -0,0 +1,40 @@ +import type { ComponentProps } from "react"; +import GA from "../util/GoogleAnalytics"; + +type Props = ComponentProps<"a"> & { eventLabel: string }; + +export default function OutboundLink({ + eventLabel, + target, + href, + onClick, + rel, + ...props +}: Props) { + return ( + { + onClick?.(event); + if (event.defaultPrevented) return; + const sameWindow = + target !== "_blank" && + !event.ctrlKey && + !event.shiftKey && + !event.metaKey && + event.button !== 1; + if (sameWindow && href) { + event.preventDefault(); + GA.trackOutbound(eventLabel, () => { + window.location.href = href; + }); + } else { + GA.trackOutbound(eventLabel, () => {}); + } + }} + /> + ); +} diff --git a/src/components/PageNotFound.scss b/src/components/PageNotFound.scss deleted file mode 100644 index 62b8b20..0000000 --- a/src/components/PageNotFound.scss +++ /dev/null @@ -1,10 +0,0 @@ -.PageNotFound { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - - h3 { - margin-bottom: 2rem; - } -} diff --git a/src/components/PageNotFound.jsx b/src/components/PageNotFound.tsx similarity index 70% rename from src/components/PageNotFound.jsx rename to src/components/PageNotFound.tsx index d19f759..bb14e77 100644 --- a/src/components/PageNotFound.jsx +++ b/src/components/PageNotFound.tsx @@ -1,8 +1,8 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; -import LinkWithArrow from "components/common/LinkWithArrow"; +import LinkWithArrow from "./common/LinkWithArrow"; -function PageNotFound() { +export default function PageNotFound() { useEffect(() => { const previousTitle = document.title; document.title = "Page Not Found • exploretech.la"; @@ -21,6 +21,3 @@ function PageNotFound() { ); } - -PageNotFound.displayName = "PageNotFound"; -export default PageNotFound; diff --git a/src/components/Registration/Eligibility.jsx b/src/components/Registration/Eligibility.jsx deleted file mode 100644 index 25111e4..0000000 --- a/src/components/Registration/Eligibility.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -export default class Eligibility extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Eligibility", - this.props.className - ); - - return ( -
-
-
-

Eligibility

-
-
-
-
-
-

Student Eligibility

-
-
-
    -
  • Must be a current high school student (grades 9-12)
  • -
      -
    • - Public or private high school students are eligible, as - well as homeschooled students -
    • -
    -
  • Must be a Los Angeles County resident
  • -
  • - Must have a computer and reliable internet connection for - the duration of the event -
  • -
  • - Must fill out all required waivers (Liability, Multimedia - Release, and Student Verification) -
  • -
-
-
-
-
-

Teacher/Educator Eligibility

-
-
-
    -
  • - Must be a current high school teacher in Los Angeles County -
  • -
  • - Must have a computer and reliable internet connection for - the duration of the event -
  • -
  • - Must fill out all required waivers (Liability and Multimedia - Release) -
  • -
  • Must submit photo of teacher badge/ID as verification
  • -
-
-
-
-
-
- ); - } -} diff --git a/src/components/Registration/Eligibility.scss b/src/components/Registration/Eligibility.scss deleted file mode 100644 index e7898a0..0000000 --- a/src/components/Registration/Eligibility.scss +++ /dev/null @@ -1,65 +0,0 @@ -.Eligibility { - .eligibility-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .eligibility-column-container { - flex-direction: column; - justify-content: space-between; - } - - .eligibility-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - &.text { - padding-top: 32px; - } - } - - img { - max-width: 100%; - max-height: 100vh; - height: auto; - } - } - - ul { - margin-left: -22px; - } -} - -@include media-breakpoint-up(md) { - .Eligibility { - .eligibility-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.eligibility-column-container { - flex-direction: row; - } - - .left-column, - .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} diff --git a/src/components/Registration/FAQ.jsx b/src/components/Registration/FAQ.jsx deleted file mode 100644 index ebbb91c..0000000 --- a/src/components/Registration/FAQ.jsx +++ /dev/null @@ -1,311 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Accordion from "react-bootstrap/Accordion"; -import Card from "react-bootstrap/Card"; - -export default class FAQ extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "FAQ", this.props.className); - - return ( -
-
-
-

FAQs

-
-

- If your question is not answered below, please reach out to us at - exploretechla@cs.ucla.edu for more information! -

-

Still interested in attending?

-

- If you are still interested in attending, fill out the waivers at - the bottom of the page (under "Waivers") and email them to us at - exploretechla@cs.ucla.edu ASAP so we can get you registered! -

-
- {this._getStudentFAQ()} - {/* {this._getTeacherFAQ()} - {this._getParentFAQ()} */} -
-
- ); - } - - _getStudentFAQ() { - return ( -
-
-

Student FAQs

-
-
- - - - - I'm still interested in attending but registration is closed! - Can I still attend?{" "} - - - - - Yes you can! Just fill out all of the waivers under the - "Waivers" secrtion below and email them to us at - exploretechla@cs.ucla.edu ASAP so we can get you registered! - - - - - - - Is there a registration fee? Are there any associated costs with - the program (ie: need to pay for an account on the virtual - platform?){" "} - - - - - This event is completely free for all attendees! All you will - need is a computer and stable internet to access the event. - - - - - - What prior background is needed? - - - - No experience needed! This event is meant to help you explore - the different fields of CS and tech! - - - - {/* - - What if I can only make part of the event (only one day, missing a few hours, etc)? - - - Let us know in the registration form! - - */} - - - How would virtual format work? (boxes, platform, etc) - - - - The event will be hosted completely online. exploretech.boxes - will be sent to each attendee (while supplies last) as well as - instructions on how to access the virtual platform prior to the - event. Attendees will receive more information once registration - is closed. - - - - - - What is an exploretech.box? - - - - An exploretech.box is a box that will hold all of the - supplemental workshop materials and swag for the event. We will - ship these to our attendees prior to the event. Supplies will be - limited, but not having a box will not prevent you from - attending our event. - - - - {/* - - What happens if I don’t get my box in time? - - - - We plan to ship the boxes well ahead of time so you should have them by event day. - In the case your box does not arrive on time, you will still be able to participate in the event. - - - */} - - - What is the College Buddy Chat? - - - - Each day you will have the opportunity to talk with one to two - UCLA undergraduates about their life at UCLA and in STEM. Feel - free to ask them any questions you may have about going from - high school to college and pursuing a STEM degree! - - - - {/* - - If I'm not an LA County resident, can I still participate? - - - - If you do not reside in LA County and would like to attend the event, you can fill out the registration - form and we will place you on a waitlist. Depending on availability, we may open up the event to non-LA - County residents. However, you would not be eligible to receive an exploretech.box, as we can only ship within LA County. - - - */} - {/* - - When is the deadline to register for the event? - - - - Please see the Registration Deadlines section for more information about deadlines. - - - */} - {/* - - Who can participate/register for the event? - - - - Please see the Eligibility section for more information about who can participate in this event. - - - */} - {/* - - I am homeschooled, can I still attend the event? - - - - Yes you can! Just denote on the registration form that you are homeschooled where it asks for your school name. - - - */} - -
- ); - } - - _getTeacherFAQ() { - return ( -
-
-

Teacher/Educator FAQs

-
-
- - - - This sounds great! How do I sign up my students? - - - - Have each of your students fill out the registration form - separately so that they can attend the event! - - - - - - Can I attend with my student(s)? - - - - Yes you can! Just fill out the registration form for - Educators/Teachers. - - - - - - Can I attend this event even without any of my students? - - - - Of course! We want to offer this event to teachers so you can - bring back some of the activities to teach to your own students. - - - - - - Will I receive supplies for the event? - - - - Unfortunately we can only provide supplies for the students who - attend our event. - - - - -
- ); - } - - _getParentFAQ() { - return ( -
-
-

Parent FAQs

-
-
- - - - Can I attend with my child/children? - - - - Parents will not be able to attend this event with their - children. This is meant to be an experience for the student - only. Teachers are allowed at this event so they can get ideas - for activities for their students. - - - - - - How will my child receive the supplies for the event? - - - - We will be mailing boxes containing any necessary supplies to - each attendee in the days leading up to our event. We have a - limited number of boxes, so please have your child register as - soon as possible. - - - - - - My child is homeschooled. Can they still attend the event? - - - - Of course! Just please make sure you follow all instructions - pertaining to homeschooled students in the registration form. - - - - -
- ); - } -} diff --git a/src/components/Registration/FAQ.scss b/src/components/Registration/FAQ.scss deleted file mode 100644 index 4ebe331..0000000 --- a/src/components/Registration/FAQ.scss +++ /dev/null @@ -1,63 +0,0 @@ -.FAQ { - background-color: white; - - .faq-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .faq-column-container { - flex-direction: row; - justify-content: space-between; - } - - .faq-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - &.text { - padding-top: 32px; - } - } - } - - .faq-student, - .faq-teacher, - .faq-parent { - padding-bottom: 32px; - } -} - -@include media-breakpoint-up(md) { - .FAQ { - .faq-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.faq-column-container { - flex-direction: row; - } - - .left-column, - .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} diff --git a/src/components/Registration/ImportantLinks.jsx b/src/components/Registration/ImportantLinks.jsx deleted file mode 100644 index 4c0c33f..0000000 --- a/src/components/Registration/ImportantLinks.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Button from "react-bootstrap/Button"; - -export default class ImportantLinks extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("ImportantLinks", this.props.className); - - return ( -
-
-

Important Links

-
-

Below are some of the important links you need for the event!

-
-
- -
-
-
- -
-
- -
-
- -
-
-
-

Event Support

-
-

- If you are experiencing any technical difficulties or have any - questions about the event, please first refer to the FAQs below.{" "} -

-

- {" "} - If your question is not answered below, please reach out to us at - exploretechla@cs.ucla.edu for assistance!{" "} -

-
-

Live Event Day Support

-

- For the duration of our event, attendees will be able to join a Zoom - meeting (link below) to ask any questions live to an exploretech.la - 2021 Staff Member. -

-
- -
-
-

- Zoom Meeting Hours: Saturday and Sunday, 9:30AM to 1:30PM -

-
-
- ); - } -} diff --git a/src/components/Registration/ImportantLinks.scss b/src/components/Registration/ImportantLinks.scss deleted file mode 100644 index d8f87c2..0000000 --- a/src/components/Registration/ImportantLinks.scss +++ /dev/null @@ -1,47 +0,0 @@ -.ImportantLinks { - background-color: #f5f5f5; - display: flex; - flex-direction: column; - align-items: center; - margin: auto; - padding-bottom: 32px; - - .links-title { - display: flex; - flex-direction: column; - align-items: center; - } - - .links-text { - display: flex; - flex-direction: column; - align-items: center; - padding-top: 30px; - } - - .buttons { - padding-top: 20px; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } -} - -@include media-breakpoint-up(md) { - .ImportantLinks { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Registration/Register.jsx b/src/components/Registration/Register.jsx deleted file mode 100644 index 88ba5a8..0000000 --- a/src/components/Registration/Register.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; - -import RegistrationAlert from "./RegistrationAlert"; -// import RegistrationTop from './RegistrationTop'; -// import Eligibility from './Eligibility'; -import Waivers from "./Waivers"; -import ImportantLinks from "./ImportantLinks"; -import FAQ from "./FAQ"; -import Footer from "../Footer"; -import Schedule from "./Schedule"; - -import images from "constants/optimizedImages"; - -function Register() { - return ( -
-
- {/* - The banner is the first thing on the page, so it stays eager and - keeps its intrinsic dimensions to reserve the space it needs. - */} - banner -
- -
-

Event Information

-
- {/* */} - - {/* */} - - - -
-
- ); -} - -Register.displayName = "Register"; -export default Register; diff --git a/src/components/Registration/Register.scss b/src/components/Registration/Register.scss deleted file mode 100644 index 4e93959..0000000 --- a/src/components/Registration/Register.scss +++ /dev/null @@ -1,26 +0,0 @@ -// Register Component Styles -@import "components/Registration/RegistrationAlert.scss"; -@import "components/Registration/RegistrationTop.scss"; -@import "components/Registration/Eligibility.scss"; -@import "components/Registration/Waivers.scss"; -@import "components/Registration/ImportantLinks.scss"; -@import "components/Registration/FAQ.scss"; -@import "components/Footer.scss"; -@import "components/Registration/Schedule.scss"; - -.Register { - .registration-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .banner { - width: 100vw; - height: auto; - } -} diff --git a/src/components/Registration/RegistrationAlert.jsx b/src/components/Registration/RegistrationAlert.jsx deleted file mode 100644 index c739bdd..0000000 --- a/src/components/Registration/RegistrationAlert.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -// import ReactGA from 'react-ga'; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Button from "react-bootstrap/Button"; -import GA from "util/GoogleAnalytics"; - -export default class RegistrationAlert extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("RegistrationAlert", this.props.className); - - return ( -
-

- Missed our event? Want to rewatch workshops and panels? -

-
-
- Checkout our past workshops and resources from previous - exploretech.la main events on our Resources Page. -
-
-
-
- -
-
-
- ); - } - - _trackResourcesClick() { - GA.trackEvent({ - category: "RegistrationAlert", - action: "Click", - label: "Resources", - }); - } -} diff --git a/src/components/Registration/RegistrationAlert.scss b/src/components/Registration/RegistrationAlert.scss deleted file mode 100644 index 4036523..0000000 --- a/src/components/Registration/RegistrationAlert.scss +++ /dev/null @@ -1,47 +0,0 @@ -.RegistrationAlert { - background-color: $color-muted-blue; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - - h3, - h5 { - color: white; - padding-right: 10px; - padding-left: 10px; - } - - .buttons { - padding-top: 10px; - display: flex; - flex-direction: column; - align-items: center; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } - - .notes { - padding-top: 10px; - } -} - -@include media-breakpoint-up(md) { - .RegistrationAlert { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Registration/RegistrationTop.jsx b/src/components/Registration/RegistrationTop.jsx deleted file mode 100644 index 4880b72..0000000 --- a/src/components/Registration/RegistrationTop.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Badge from "react-bootstrap/Badge"; - -export default class RegistrationTop extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "RegistrationTop", - this.props.className - ); - - return ( -
-
-
-
-
-

About the Event

-
-
-
-

- exploretech.la is an annual event hosted by UCLA students that - aims to inspire high school students from underserved - communities in the Greater Los Angeles Area to explore - computer science, engineering, and technology. -

-

- With the ongoing pandemic, our team has decided to move - exploretech.la 2021 to a virtual format for the safety of our - attendees, partners, and staff members. -

-
    -
  • - Event Dates: April 10-11, 2021 -
  • -
  • - Time: 10:00AM - 1:30PM PDT -
  • -
  • - Location: Held virtually -
  • -
-

- Our event will include: -

    -
  • Interactive workshops
  • -
  • Panels with industry guests and college students
  • -
  • - Opportunity to chat with current UCLA undergraduates -
  • -
  • - An exploretech.box for all students (while supplies last) -
  • -
-

-
-
-
-
-

Registration Deadlines

-
-
-
-
-

- Early Registration -{" "} - Closed -

-
-

- Deadline: January 24, 2021 at 11:59PM PST -

-

- - *As long as space permits and all eligibility requirements - are met, you will be accepted and guaranteed a box. - -

-
-

- General Registration -{" "} - Closed -

-
-

- Deadline: February 28, 2021 at 11:59PM PST -

-

- NOTE: Keep an eye out for a confirmation email from us - by early March if you are a general registration applicant! -

-
-

- Late Registration -{" "} - By Email Only -

-
-

- - *If you are still interested in attending our event, please - email us at exploretechla@cs.ucla.edu - -

-
-
-
-
-
- ); - } -} diff --git a/src/components/Registration/RegistrationTop.scss b/src/components/Registration/RegistrationTop.scss deleted file mode 100644 index 25b83f7..0000000 --- a/src/components/Registration/RegistrationTop.scss +++ /dev/null @@ -1,71 +0,0 @@ -.RegistrationTop { - .registration-content { - display: flex; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.registration-event-info { - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - } - - .registration-title { - padding-bottom: 32px; - } - - .registration-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - &.text { - padding-top: 32px; - } - } - - ul { - &.no-style { - list-style-type: none; - margin-left: -40px; - } - - &.no-margin { - margin-left: -24px; - } - } - } -} - -@include media-breakpoint-up(md) { - .RegistrationTop { - .registration-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.registration-event-info { - flex-direction: row; - } - - .left-column, - .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} diff --git a/src/components/Registration/Schedule.jsx b/src/components/Registration/Schedule.jsx deleted file mode 100644 index b20b1b6..0000000 --- a/src/components/Registration/Schedule.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -// import { SCHEDULE } from 'constants/schedule'; - -export default class Schedule extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Schedule", this.props.className); - - return ( -
-
-
-

Schedule

-
-
-
-
-
-

Day 1 (Saturday, April 10th)

-
-
-
-

-

- 9:30 AM: Virtual Platform Opens -

-

- 10:00 AM: Opening Ceremony -

-

- 10:30 AM: Session 1 -

-

- 11:40 AM: College Buddy Chat -

-

- 12:20 PM: Session 2 -

-

- 1:20 PM: Closing Briefing -

-

-
-
-
-
-

Day 2 (Sunday, April 11th)

-
-
-
-

-

- 9:30 AM: Virtual Platform Opens -

-

- 10:00 AM: Opening Briefing -

-

- 10:10 AM: Session 3 -

-

- 11:20 AM: College Buddy Chat -

-

- 12:00 PM: Session 4 -

-

- 1:00 PM: Closing Ceremony -

-

-
-
-
-
-
- ); - } -} diff --git a/src/components/Registration/Schedule.scss b/src/components/Registration/Schedule.scss deleted file mode 100644 index 52d7763..0000000 --- a/src/components/Registration/Schedule.scss +++ /dev/null @@ -1,56 +0,0 @@ -.Schedule { - background-color: white; - - .schedule-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.schedule-column-container { - flex-direction: column; - } - - .schedule-title { - align-items: center; - text-align: center; - } - - .left-column, - .right-column { - &.text { - padding-top: 32px; - } - } - } -} - -@include media-breakpoint-up(md) { - .Schedule { - .schedule-content { - justify-content: space-between; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.schedule-column-container { - flex-direction: row; - } - - .left-column, - .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} diff --git a/src/components/Registration/Waivers.jsx b/src/components/Registration/Waivers.jsx deleted file mode 100644 index 2e69e8c..0000000 --- a/src/components/Registration/Waivers.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Button from "react-bootstrap/Button"; - -import LiabilityWaiver from 'static/pdf/exploretech_la_2022_Liability_Waiver.pdf'; -import MultimediaRelease from 'static/pdf/exploretech_la_2022_Multimedia_Release_Form.pdf'; -import StudentVerification from 'static/pdf/exploretech_la_2022_Student_Verification_Form.pdf'; - -export default class Waivers extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Waivers", this.props.className); - - return ( -
-
-

Waivers

-
-

- Please download and sign the following waivers. They are a required - part of your registration. -

-

- - Note: For high school students, the waivers require a - parent/guardian signature. - -

-
-
-
- -
-
- -
-
- -
-
-
-

- - All forms can also be found here, in both PDF and Google Docs - formats (for easier editing): - -

-

- To edit the Google Docs, please make a copy of the waiver into your - own Drive and fill it out there. Simply save as PDF to upload to the - registration form. -

-
- -
-
-
- ); - } -} diff --git a/src/components/Registration/Waivers.scss b/src/components/Registration/Waivers.scss deleted file mode 100644 index 5933684..0000000 --- a/src/components/Registration/Waivers.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Waivers { - background-color: #f5f5f5; - display: flex; - flex-direction: column; - align-items: center; - margin: auto; - padding-bottom: 32px; - - .waivers-title, - .waivers-text { - display: flex; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } -} - -@include media-breakpoint-up(md) { - .Waivers { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Resources/Ceremonies.jsx b/src/components/Resources/Ceremonies.jsx deleted file mode 100644 index 2adeb5a..0000000 --- a/src/components/Resources/Ceremonies.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Ceremonies extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Ceremonies", - this.props.className - ); - - return ( -
-
-
-

Ceremonies

-
-
-
-
-

Opening Ceremony

- -
-
-

Closing Ceremony

- -
-
-
-
- ); - } -} diff --git a/src/components/Resources/Ceremonies.scss b/src/components/Resources/Ceremonies.scss deleted file mode 100644 index 7102ea4..0000000 --- a/src/components/Resources/Ceremonies.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Ceremonies { - .ceremonies-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .ceremonies-videos { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - width: $section-width; - } - - .ceremonies-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - flex-direction: column; - } - } -} - -@include media-breakpoint-up(md) { - .Ceremonies { - .ceremonies-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - .ceremonies-videos { - flex-direction: row; - } - - .left-column, - .right-column { - flex-direction: column; - width: 46%; - } - } - } -} diff --git a/src/components/Resources/FAQ.jsx b/src/components/Resources/FAQ.jsx deleted file mode 100644 index ec3f003..0000000 --- a/src/components/Resources/FAQ.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Accordion from 'react-bootstrap/Accordion'; -import Card from 'react-bootstrap/Card'; - -import { FAQS } from 'constants/faq'; - -export default class FAQ extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'FAQ', this.props.className); - - return ( -
-
- ); - } - - _getStudentFAQ() { - return ( -
-
-

Student FAQs

-
-
- - {FAQS.map(faq => { - return ( - -
{faq.question}
-
- - -
{faq.answer}
- {faq.note && ( -

Note: {faq.note}

- )} -
-
-
- ) - })} -
-
- ); - } -} diff --git a/src/components/Resources/FAQ.scss b/src/components/Resources/FAQ.scss deleted file mode 100644 index dc177e3..0000000 --- a/src/components/Resources/FAQ.scss +++ /dev/null @@ -1,59 +0,0 @@ -.FAQ { - background-color: white; - - .faq-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .faq-column-container { - flex-direction: row; - justify-content: space-between; - } - - .faq-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } - - .faq-student, .faq-teacher, .faq-parent { - padding-bottom: 32px; - } -} - -@include media-breakpoint-up(md) { - .FAQ { - .faq-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.faq-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources/Maps.jsx b/src/components/Resources/Maps.jsx deleted file mode 100644 index 7dfd447..0000000 --- a/src/components/Resources/Maps.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import Button from 'react-bootstrap/Button'; - -export const Maps = (props) => { - const classNames = classnames('Section', 'Maps', props.className); - const { maps } = props - - return ( -
-
-
-

Maps

-
-
-
- Below are links to view all the maps you may need throughout the day! -
-
- {maps.map(map => { - return - // return
- // {map.src} - //
- })} -
-
-
- ); -} - -export default Maps; \ No newline at end of file diff --git a/src/components/Resources/Maps.scss b/src/components/Resources/Maps.scss deleted file mode 100644 index 15edc14..0000000 --- a/src/components/Resources/Maps.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Maps { - background-color: #f5f5f5; - - .maps-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .maps-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 60vw; - } - } -} - -@include media-breakpoint-up(md) { - .Maps { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Resources/Resources.jsx b/src/components/Resources/Resources.jsx deleted file mode 100644 index e0bc4a2..0000000 --- a/src/components/Resources/Resources.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import Button from 'react-bootstrap/Button'; - -import Workshops from './Workshops'; -import FAQ from 'components/Resources/FAQ'; -import Footer from '../Footer'; -import Maps from 'components/Resources/Maps'; -import Schedule from 'components/Resources/Schedule'; -import Waivers from 'components/Resources/Waivers'; - -import busRoutes from "static/maps/bus_routes.jpg" -import ackerman_union_2F from "static/maps/su_map_ack_2.jpeg" -import ackerman_union_3F from "static/maps/su_map_ack_3.jpeg" -import wristbands from "static/maps/wristband_coordination.png" -import EventProgram from "static/pdf/Event_Program.pdf" - -function Resources() { - const maps = [ - { - src: busRoutes, - name: "Bus Routes", - }, { - src: ackerman_union_2F, - name: "Ackerman Union 2F", - }, { - src: ackerman_union_3F, - name: "Ackerman Union 3F", - }, - ] - - return ( -
-
-

Resources

-
- -
- -
- - -
-

Wristbands

-
-
Make sure you have your wristband on the entire time!
-
-
- All high school students are divided into 3 groups, each will get a different colored wristband. - During each session, 1 group will be at the exhibition hall and the other 2 groups will be at the workshops. - Group 1 (green colored-wristband) will be at the exhibition hall during Session 1, - Group 2 (blue) during Session 2, and Group 3 (red) during Session 3. -
-
- Wristband coordination -
- - - -
-

Wristbands

-
-
-
Thanks for coming to our event! Got feedback? Let us know by filling out this form below!
-
-
-
- -
-
-
-
- ); -} - -Resources.displayName = "Resources"; -export default Resources; diff --git a/src/components/Resources/Resources.scss b/src/components/Resources/Resources.scss deleted file mode 100644 index 7de47fc..0000000 --- a/src/components/Resources/Resources.scss +++ /dev/null @@ -1,39 +0,0 @@ -// Resources Component Styles -@import "components/Footer.scss"; -@import "components/Resources/Ceremonies.scss"; -@import "components/Resources/Workshops.scss"; -@import "components/Resources/Maps.scss"; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } - - .full-length-button { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 100%; - } -} diff --git a/src/components/Resources/Schedule.jsx b/src/components/Resources/Schedule.jsx deleted file mode 100644 index c3f1a88..0000000 --- a/src/components/Resources/Schedule.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { SCHEDULE } from 'constants/schedule'; - -export default class Schedule extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Schedule', this.props.className); - - return ( -
-
-
-

Schedule

-
-
-
-

- {SCHEDULE.map(event => { - return

{event.time} {event.name}

- })} -

-
-
-
- ); - } -} \ No newline at end of file diff --git a/src/components/Resources/Schedule.scss b/src/components/Resources/Schedule.scss deleted file mode 100644 index 9aaac1e..0000000 --- a/src/components/Resources/Schedule.scss +++ /dev/null @@ -1,54 +0,0 @@ -.Schedule { - background-color: white; - - .schedule-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.schedule-column-container { - flex-direction: column; - } - - .schedule-title { - align-items: center; - text-align: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } -} - -@include media-breakpoint-up(md) { - .Schedule { - .schedule-content { - justify-content: space-between; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.schedule-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } - } \ No newline at end of file diff --git a/src/components/Resources/Waivers.jsx b/src/components/Resources/Waivers.jsx deleted file mode 100644 index 4fc5f48..0000000 --- a/src/components/Resources/Waivers.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Button from 'react-bootstrap/Button'; - -import LiabilityWaiver from 'static/pdf/exploretech_la_2022_Liability_Waiver.pdf'; -import MultimediaRelease from 'static/pdf/exploretech_la_2022_Multimedia_Release_Form.pdf'; -import StudentVerification from 'static/pdf/exploretech_la_2022_Student_Verification_Form.pdf'; - -export default class Waivers extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Waivers', this.props.className); - - return ( -
-
-

Waivers

-
-

- Please download and sign the following waivers. They are a required part of your registration. -

-

- Note: For high school students, the waivers require a parent/guardian signature. -

-
-
-
- -
-
- -
-
- -
-
-
- ); - } -} diff --git a/src/components/Resources/Waivers.scss b/src/components/Resources/Waivers.scss deleted file mode 100644 index 855912e..0000000 --- a/src/components/Resources/Waivers.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Waivers { - background-color: #F5F5F5; - display: flex; - flex-direction: column; - align-items: center; - margin: auto; - padding-bottom: 32px; - - .waivers-title, .waivers-text { - display: flex; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } - -} - -@include media-breakpoint-up(md) { - .Waivers { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources/Workshops copy.jsx b/src/components/Resources/Workshops copy.jsx deleted file mode 100644 index 45ca9db..0000000 --- a/src/components/Resources/Workshops copy.jsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Card from 'react-bootstrap/Card'; -import CardDeck from 'react-bootstrap/CardDeck'; -import ListGroup from 'react-bootstrap/ListGroup'; -import ListGroupItem from 'react-bootstrap/ListGroupItem'; -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Workshops extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Workshops', this.props.className); - - return ( -
-
-
-

Workshops

-
-
- {this._getWorkshopCards()} -
-
-
-

Panels

-
-
- {this._getPanelCards()} -
-
- ); - } - - _getWorkshopCards() { - return ( -
- - - - Coding 101 - - - (Session 1 Recording) - - - - Session 2 Recording - Student Guide - Slides - Solutions - - - - - ExploreWeb.la: Web Design & Dev - - - - Slides - Review Sheet - Codepen - - - - - - - Operating Systems - - - - Slides - Linux Emulator - - - - - Draw with Code - - - - Slides - Repl.it Project - - - - - - - Less Quantum More Computing - - - - Slides - Definitions Doc - Google Colab - - - - - Crack the Code with Cryptography - - - - Slides - Escape Room Google Form - Escape Room Questions and Solutions - - - - - - - Escaping Reality with Game Development - - - - Slides - Codepen Activity - - - - - Murder Mystery! - - - - Slides - Worksheet - - - - - - - AI Adventures - - - - Slides - Worksheet - - - - - Digital Design - - - - Slides - Miro Whiteboard - - - - - - - Data Science - - - - Slides - Spreadsheet - - - - - Tech & Society - - - - Slides - - - - - - - Bot Buddy - Hosted by DIY Girls - - - (Session 1 Recording) - - - - Session 2 Recording - Skin Templates - Youtube Tutorial - Slides - Follow Up Instructions - - - -
- ); - } - - _getPanelCards() { - return ( -
- - - - Diverse Careers in Computer Science - - - - Slides - Padlet - Panelist Info and Resources - - - - - Representation in Computer Science - - - - Slides - Padlet - - - -
- ); - } -} diff --git a/src/components/Resources/Workshops copy.scss b/src/components/Resources/Workshops copy.scss deleted file mode 100644 index a0b3782..0000000 --- a/src/components/Resources/Workshops copy.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import 'components/common/YoutubeEmbed.scss'; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - } -} \ No newline at end of file diff --git a/src/components/Resources/Workshops.jsx b/src/components/Resources/Workshops.jsx deleted file mode 100644 index 04e792e..0000000 --- a/src/components/Resources/Workshops.jsx +++ /dev/null @@ -1,421 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Card from "react-bootstrap/Card"; -import CardDeck from "react-bootstrap/CardDeck"; -import ListGroup from "react-bootstrap/ListGroup"; -import ListGroupItem from "react-bootstrap/ListGroupItem"; -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Workshops extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Workshops", this.props.className); - - return ( -
-
-
-

Workshops

-
-
- {this._getWorkshopCards()} -
-
-
-

Panels

-
-
- {this._getPanelCards()} -
-
- ); - } - - _getWorkshopCards() { - return ( -
- - - - - Coding 101 - - - (Session 1 Recording) - - - - - Session 2 Recording - - - - - Student Guide - - - - - Slides - - - - - Solutions - - - - - - - - ExploreWeb.la: Web Design & Dev - - - - - - - Slides - - - - - Review Sheet - - - - - Codepen - - - - - - - - - - Operating Systems - - - - - - - Slides - - - - - Linux Emulator - - - - - - - - Draw with Code - - - - - - - Slides - - - - - Repl.it Project - - - - - - - - - - Less Quantum More Computing - - - - - - - Slides - - - - - Definitions Doc - - - - - Google Colab - - - - - - - - Crack the Code with Cryptography - - - - - - - Slides - - - - - Escape Room Google Form - - - - - Escape Room Questions and Solutions - - - - - - - - - - Escaping Reality with Game Development - - - - - - - Slides - - - - - Codepen Activity - - - - - - - - Murder Mystery! - - - - - - - Slides - - - - - Worksheet - - - - - - - - - - AI Adventures - - - - - - - Slides - - - - - Worksheet - - - - - - - - Digital Design - - - - - - - Slides - - - - - Miro Whiteboard - - - - - - - - - - Data Science - - - - - - - Slides - - - - - Spreadsheet - - - - - - - - Tech & Society - - - - - - - Slides - - - - - - - - - - Bot Buddy - - - Hosted by DIY Girls - - - (Session 1 Recording) - - - - - Session 2 Recording - - - - - Skin Templates - - - - - Youtube Tutorial - - - - - Slides - - - - - Follow Up Instructions - - - - - -
- ); - } - - _getPanelCards() { - return ( -
- - - - - Diverse Careers in Computer Science - - - - - - - Slides - - - - - Padlet - - - - - Panelist Info and Resources - - - - - - - - Representation in Computer Science - - - - - - - Slides - - - - - Padlet - - - - - -
- ); - } -} diff --git a/src/components/Resources/Workshops.scss b/src/components/Resources/Workshops.scss deleted file mode 100644 index 225667e..0000000 --- a/src/components/Resources/Workshops.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - .panelists-container { - max-width: $section-width; - margin: auto; - - .panelists-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } - } -} diff --git a/src/components/Resources/exploretech2021.jsx b/src/components/Resources/exploretech2021.jsx deleted file mode 100644 index 429ccf1..0000000 --- a/src/components/Resources/exploretech2021.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import Ceremonies from './Ceremonies'; -import Workshops from './Workshops'; -import Footer from '../Footer'; - -function Resources() { - return ( -
-
-

Resources

-
-
-
- Welcome students! This webpage contains the recordings and resources of all of our - workshops and panels for the exploretech.la 2021 event! For each workshop or panel, - please watch the recording first and check back on this page to access the appropriate - resources for the activity. -
-
- - -
-
-
- ); -} - -Resources.displayName = 'Resources'; -export default Resources; \ No newline at end of file diff --git a/src/components/Resources/exploretech2021.scss b/src/components/Resources/exploretech2021.scss deleted file mode 100644 index 907b4e6..0000000 --- a/src/components/Resources/exploretech2021.scss +++ /dev/null @@ -1,29 +0,0 @@ -// Resources Component Styles -@import 'components/Footer.scss'; -@import 'components/Resources/Ceremonies.scss'; -@import 'components/Resources/Workshops.scss'; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } -} \ No newline at end of file diff --git a/src/components/Resources2023/Ceremonies.jsx b/src/components/Resources2023/Ceremonies.jsx deleted file mode 100644 index 2adeb5a..0000000 --- a/src/components/Resources2023/Ceremonies.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Ceremonies extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Ceremonies", - this.props.className - ); - - return ( -
-
-
-

Ceremonies

-
-
-
-
-

Opening Ceremony

- -
-
-

Closing Ceremony

- -
-
-
-
- ); - } -} diff --git a/src/components/Resources2023/Ceremonies.scss b/src/components/Resources2023/Ceremonies.scss deleted file mode 100644 index 7102ea4..0000000 --- a/src/components/Resources2023/Ceremonies.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Ceremonies { - .ceremonies-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .ceremonies-videos { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - width: $section-width; - } - - .ceremonies-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - flex-direction: column; - } - } -} - -@include media-breakpoint-up(md) { - .Ceremonies { - .ceremonies-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - .ceremonies-videos { - flex-direction: row; - } - - .left-column, - .right-column { - flex-direction: column; - width: 46%; - } - } - } -} diff --git a/src/components/Resources2023/FAQ.jsx b/src/components/Resources2023/FAQ.jsx deleted file mode 100644 index ec3f003..0000000 --- a/src/components/Resources2023/FAQ.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Accordion from 'react-bootstrap/Accordion'; -import Card from 'react-bootstrap/Card'; - -import { FAQS } from 'constants/faq'; - -export default class FAQ extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'FAQ', this.props.className); - - return ( -
-
-
-

FAQs

-
-

- If your question is not answered below, please reach out to us - at exploretechla@cs.ucla.edu for - more information! -

-
- {this._getStudentFAQ()} -
-
- ); - } - - _getStudentFAQ() { - return ( -
-
-

Student FAQs

-
-
- - {FAQS.map(faq => { - return ( - -
{faq.question}
-
- - -
{faq.answer}
- {faq.note && ( -

Note: {faq.note}

- )} -
-
-
- ) - })} -
-
- ); - } -} diff --git a/src/components/Resources2023/FAQ.scss b/src/components/Resources2023/FAQ.scss deleted file mode 100644 index dc177e3..0000000 --- a/src/components/Resources2023/FAQ.scss +++ /dev/null @@ -1,59 +0,0 @@ -.FAQ { - background-color: white; - - .faq-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .faq-column-container { - flex-direction: row; - justify-content: space-between; - } - - .faq-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } - - .faq-student, .faq-teacher, .faq-parent { - padding-bottom: 32px; - } -} - -@include media-breakpoint-up(md) { - .FAQ { - .faq-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.faq-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources2023/Maps.jsx b/src/components/Resources2023/Maps.jsx deleted file mode 100644 index 7dfd447..0000000 --- a/src/components/Resources2023/Maps.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import Button from 'react-bootstrap/Button'; - -export const Maps = (props) => { - const classNames = classnames('Section', 'Maps', props.className); - const { maps } = props - - return ( -
-
-
-

Maps

-
-
-
- Below are links to view all the maps you may need throughout the day! -
-
- {maps.map(map => { - return - // return
- // {map.src} - //
- })} -
-
-
- ); -} - -export default Maps; \ No newline at end of file diff --git a/src/components/Resources2023/Maps.scss b/src/components/Resources2023/Maps.scss deleted file mode 100644 index 15edc14..0000000 --- a/src/components/Resources2023/Maps.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Maps { - background-color: #f5f5f5; - - .maps-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .maps-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 60vw; - } - } -} - -@include media-breakpoint-up(md) { - .Maps { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Resources2023/Resources.jsx b/src/components/Resources2023/Resources.jsx deleted file mode 100644 index b5bb3d5..0000000 --- a/src/components/Resources2023/Resources.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import Button from 'react-bootstrap/Button'; - -import Workshops from './Workshops'; -// import FAQ from 'components/Resources/FAQ'; -import Footer from '../Footer'; -import Maps from 'components/Resources/Maps'; -import Schedule from 'components/Resources/Schedule'; -// import Waivers from 'components/Resources/Waivers'; - -import busRoutes from "static/maps/bus_routes.jpg" -import ackerman_union_2F from "static/maps/su_map_ack_2.jpeg" -import ackerman_union_3F from "static/maps/su_map_ack_3.jpeg" -// import wristbands from "static/maps/wristband_coordination.png" -// import EventProgram from "static/pdf/Event_Program.pdf" - -function Resources() { - const maps = [ - { - src: busRoutes, - name: "Bus Routes", - }, { - src: ackerman_union_2F, - name: "Ackerman Union 2F", - }, { - src: ackerman_union_3F, - name: "Ackerman Union 3F", - }, - ] - - return ( -
-
-

Resources

-
-
-
- Welcome students and teachers! Confused about the schedule? Lost and need a map? - Or maybe just curious about what workshops are happening and when? This webpage contains - important resources you need for the exploretech.la 2023 event! If you can't find what - you need, feel free to - contact exploretechla@cs.ucla.edu for more questions. -
-
-
- {/* */} - -
- - - -
-

Wristbands

-
-
Make sure you have your wristband on the entire time!
-
-
- All high school students are divided into 3 groups, each will get a different colored wristband. - During each session, 1 group will be at the exhibition hall and the other 2 groups will be at the workshops. - Group 1 (green colored-wristband) will be at the exhibition hall during Session 1, - Group 2 (blue) during Session 2, and Group 3 (red) during Session 3. -
-
- {/* Wristband coordination */} -
- {/* */} - {/* */} -
- {/*

Wristbands

*/} -
-
-
Thanks for coming to our event! Got feedback? Let us know by filling out this form below!
-
-
-
- {/* */} - -
-
-
-
- ); -} - -Resources.displayName = "Resources"; -export default Resources; diff --git a/src/components/Resources2023/Resources.scss b/src/components/Resources2023/Resources.scss deleted file mode 100644 index 7de47fc..0000000 --- a/src/components/Resources2023/Resources.scss +++ /dev/null @@ -1,39 +0,0 @@ -// Resources Component Styles -@import "components/Footer.scss"; -@import "components/Resources/Ceremonies.scss"; -@import "components/Resources/Workshops.scss"; -@import "components/Resources/Maps.scss"; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } - - .full-length-button { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 100%; - } -} diff --git a/src/components/Resources2023/Schedule.jsx b/src/components/Resources2023/Schedule.jsx deleted file mode 100644 index c3f1a88..0000000 --- a/src/components/Resources2023/Schedule.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { SCHEDULE } from 'constants/schedule'; - -export default class Schedule extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Schedule', this.props.className); - - return ( -
-
-
-

Schedule

-
-
-
-

- {SCHEDULE.map(event => { - return

{event.time} {event.name}

- })} -

-
-
-
- ); - } -} \ No newline at end of file diff --git a/src/components/Resources2023/Schedule.scss b/src/components/Resources2023/Schedule.scss deleted file mode 100644 index 9aaac1e..0000000 --- a/src/components/Resources2023/Schedule.scss +++ /dev/null @@ -1,54 +0,0 @@ -.Schedule { - background-color: white; - - .schedule-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.schedule-column-container { - flex-direction: column; - } - - .schedule-title { - align-items: center; - text-align: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } -} - -@include media-breakpoint-up(md) { - .Schedule { - .schedule-content { - justify-content: space-between; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.schedule-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } - } \ No newline at end of file diff --git a/src/components/Resources2023/Waivers.jsx b/src/components/Resources2023/Waivers.jsx deleted file mode 100644 index 2754dab..0000000 --- a/src/components/Resources2023/Waivers.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Button from 'react-bootstrap/Button'; - -import LiabilityWaiver from 'static/pdf/exploretech_la_2022_Liability_Waiver.pdf'; -import MultimediaRelease from 'static/pdf/exploretech_la_2022_Multimedia_Release_Form.pdf'; -import StudentVerification from 'static/pdf/exploretech_la_2022_Student_Verification_Form.pdf'; - -export default class Waivers extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - // render() { - // const classNames = classnames('Waivers', this.props.className); - - // return ( - //
- //
- //

Waivers

- //
- //

- // Please download and sign the following waivers. They are a required part of your registration. - //

- //

- // Note: For high school students, the waivers require a parent/guardian signature. - //

- //
- //
- //
- // - //
- //
- // - //
- //
- // - //
- //
- //
- // ); - // } -} diff --git a/src/components/Resources2023/Waivers.scss b/src/components/Resources2023/Waivers.scss deleted file mode 100644 index 855912e..0000000 --- a/src/components/Resources2023/Waivers.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Waivers { - background-color: #F5F5F5; - display: flex; - flex-direction: column; - align-items: center; - margin: auto; - padding-bottom: 32px; - - .waivers-title, .waivers-text { - display: flex; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } - -} - -@include media-breakpoint-up(md) { - .Waivers { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources2023/Workshops copy.jsx b/src/components/Resources2023/Workshops copy.jsx deleted file mode 100644 index 45ca9db..0000000 --- a/src/components/Resources2023/Workshops copy.jsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Card from 'react-bootstrap/Card'; -import CardDeck from 'react-bootstrap/CardDeck'; -import ListGroup from 'react-bootstrap/ListGroup'; -import ListGroupItem from 'react-bootstrap/ListGroupItem'; -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Workshops extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Workshops', this.props.className); - - return ( -
-
-
-

Workshops

-
-
- {this._getWorkshopCards()} -
-
-
-

Panels

-
-
- {this._getPanelCards()} -
-
- ); - } - - _getWorkshopCards() { - return ( -
- - - - Coding 101 - - - (Session 1 Recording) - - - - Session 2 Recording - Student Guide - Slides - Solutions - - - - - ExploreWeb.la: Web Design & Dev - - - - Slides - Review Sheet - Codepen - - - - - - - Operating Systems - - - - Slides - Linux Emulator - - - - - Draw with Code - - - - Slides - Repl.it Project - - - - - - - Less Quantum More Computing - - - - Slides - Definitions Doc - Google Colab - - - - - Crack the Code with Cryptography - - - - Slides - Escape Room Google Form - Escape Room Questions and Solutions - - - - - - - Escaping Reality with Game Development - - - - Slides - Codepen Activity - - - - - Murder Mystery! - - - - Slides - Worksheet - - - - - - - AI Adventures - - - - Slides - Worksheet - - - - - Digital Design - - - - Slides - Miro Whiteboard - - - - - - - Data Science - - - - Slides - Spreadsheet - - - - - Tech & Society - - - - Slides - - - - - - - Bot Buddy - Hosted by DIY Girls - - - (Session 1 Recording) - - - - Session 2 Recording - Skin Templates - Youtube Tutorial - Slides - Follow Up Instructions - - - -
- ); - } - - _getPanelCards() { - return ( -
- - - - Diverse Careers in Computer Science - - - - Slides - Padlet - Panelist Info and Resources - - - - - Representation in Computer Science - - - - Slides - Padlet - - - -
- ); - } -} diff --git a/src/components/Resources2023/Workshops copy.scss b/src/components/Resources2023/Workshops copy.scss deleted file mode 100644 index a0b3782..0000000 --- a/src/components/Resources2023/Workshops copy.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import 'components/common/YoutubeEmbed.scss'; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - } -} \ No newline at end of file diff --git a/src/components/Resources2023/Workshops.jsx b/src/components/Resources2023/Workshops.jsx deleted file mode 100644 index e72587b..0000000 --- a/src/components/Resources2023/Workshops.jsx +++ /dev/null @@ -1,355 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Card from "react-bootstrap/Card"; -import CardDeck from "react-bootstrap/CardDeck"; -import ListGroup from "react-bootstrap/ListGroup"; -import ListGroupItem from "react-bootstrap/ListGroupItem"; - -export default class Workshops extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Workshops", this.props.className); - - return ( -
-
-
-

Workshops

-
-
- {this._getWorkshopCards()} -
-
-
-

Panels

-
-
- {this._getPanelCards()} -
-
- ); - } - - _getWorkshopCards() { - return ( -
- - - - - Fundamentals of Data Science - - Instructors: Takao Oba and Riya Bhatla - - - - Unlock the power of data and enhance your skills with our comprehensive Data Science workshop. Join us and discover the latest tools and techniques to turn data into actionable insights and drive better decision making. - - - - Slides - - - - - - - - Game Development - - Instructors: Michael Bunte and Grace Mao - - - - Ever wonder how your favorite games are created? How do artists, programmers, composers, and others work together to create an amazing final product? Come find out at the game development workshop! - - - - Slides - - - - - - - - - - Python Turtle - - Instructors: Aparna Hariharan and Isha Rajput - - - - Come learn how to code in Python using Python's fun art library called Python Turtle! By the end, you'll have created a customized digital sticker that you can design! - - - - Slides - - - - - - - - SCRATCHing the Surface of Coding - - Instructors: Michael Bunte and Jason Lozada - - - - Want to learn how to code but don’t know where to start? Come join us as we introduce you to Scratch, a visual programming language that allows you to create your own interactive animations, stories, and games. - - - - Slides - - - - - - - - - - CS Operations in Minecraft - - Instructors: Grace Mao and Kelly Zhang - - - - Logic gates, circuits, game dev...in Minecraft ?!?! Come to our workshop to learn about Minecraft's amazing redstone system and how it connects to engineering :0 - - - - Slides - - - - - - - - Are you Related to a Strawberry? - - Instructors: Wendy Su and Krisha Chokshi - - - - Are you related to a strawberry? Are you the plant parent? Learn about the intersection of biology and computer science in what we call bioinformatics. Come for a hands-on experience in extracting DNA! - - - - Slides - - - - - - - - - - Basics of Encryption - - Instructors: Ashish Basetty and Takao Oba - - - - You’ve heard of hackers, but do you know about the people who stop them? Join us as we learn about how we keep our data secure. Work together to solve codes and puzzles, and learn how we can keep passwords and personal information private. - - - - Slides - - - - - - - - Intro to Web Development - - Instructors: Emily Nham and Jazlin Ong - - - - Have you ever wondered how websites are made? Join us to learn the basics of HTML and CSS, the coding languages of the web. By the end of our session, you’ll walk out with the ability to make your own website! - - - - Slides - - - - - - - - - - Internet of Things - - Instructors: Kunal Patil and Krisha Chokshi - - - - Explore the Internet of Things, its evolution, and its relevance to today’s society! Students will learn about cybersecurity protocols and get a chance to demo and test IoT products. While brainstorming daily uses of IoT, they will discover its widespread impact on each of our lives. - - - - Slides - - - - - - - - Natural Language Proccessing - - Instructors: Ashish Basetty and Jazlin Ong - - - - Every used Siri? ChatGPT? Google Translate? Come to this workshop to learn how a computer thinking in 0’s and 1’s can learn to speak like a human. Learn more about how human language works, and how we can come up with rules to teach it to a machine. - - - - Slides - - - - - - - - - - Machines Learn How to Learn - - Instructors: Michael Song and Kelly Zhang - - - - As technology advances and we get smarter devices, we are also collecting more and more data. All of this data is meaningless without the help of machine learning. In this workshop we’ll delve into how machines use data to predict and answer questions. - - - - Slides - - - - - - - - Making Your Own Shazam - - Instructors: Riya Bhatla and Joe Lin - - - - Ever heard a song at a party that you didn’t recognize? Shazam is the solution. In this workshop, you’ll get hands-on experience to build your own song recognizer. You’ll learn about the basics of audio processing and analysis and much more! - - - - Slides - - - - - - - - - - Introduction to Autonomous Vehicles - - Instructors: Michael Song and Joe Lin - - - - Ever wondered how a Tesla or Waymo car navigates itself through a seemingly countless number of road scenarios? Join us in this workshop to learn about the fundamental building blocks of autonomous vehicle technology and even get a chance to witness object detection models in action! - - - - Slides - - - - - -
- ); - } - - _getPanelCards() { - return ( -
- - - - - Careers in Computer Science - - Moderators: Isha Rajput and Emily Nham - - - - Want to know what it's like working in tech? Meetcurrent students and graduates pursuing Computer Science at places like Google, Facebook, Atlassian, and CreditKarma! Come learn about what they do and how they ended up where they are! - - - - Slides - - - - - Panelist Info - - - - - - - - Divieristy 101 - - Moderators: Aparna Hariharan and Jason Lozada - - - - Join the conversation with our esteemed guests regarding Diversity in Computer Science and what their experiences are like as minorities in this field. - - - - Slides - - - - - Panelist Info - - - - - -
- ); - } -} diff --git a/src/components/Resources2023/Workshops.scss b/src/components/Resources2023/Workshops.scss deleted file mode 100644 index 225667e..0000000 --- a/src/components/Resources2023/Workshops.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - .panelists-container { - max-width: $section-width; - margin: auto; - - .panelists-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } - } -} diff --git a/src/components/Resources2023/exploretech2021.jsx b/src/components/Resources2023/exploretech2021.jsx deleted file mode 100644 index 429ccf1..0000000 --- a/src/components/Resources2023/exploretech2021.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import Ceremonies from './Ceremonies'; -import Workshops from './Workshops'; -import Footer from '../Footer'; - -function Resources() { - return ( -
-
-

Resources

-
-
-
- Welcome students! This webpage contains the recordings and resources of all of our - workshops and panels for the exploretech.la 2021 event! For each workshop or panel, - please watch the recording first and check back on this page to access the appropriate - resources for the activity. -
-
- - -
-
-
- ); -} - -Resources.displayName = 'Resources'; -export default Resources; \ No newline at end of file diff --git a/src/components/Resources2023/exploretech2021.scss b/src/components/Resources2023/exploretech2021.scss deleted file mode 100644 index 907b4e6..0000000 --- a/src/components/Resources2023/exploretech2021.scss +++ /dev/null @@ -1,29 +0,0 @@ -// Resources Component Styles -@import 'components/Footer.scss'; -@import 'components/Resources/Ceremonies.scss'; -@import 'components/Resources/Workshops.scss'; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } -} \ No newline at end of file diff --git a/src/components/Resources2026/Ceremonies.jsx b/src/components/Resources2026/Ceremonies.jsx deleted file mode 100644 index 2adeb5a..0000000 --- a/src/components/Resources2026/Ceremonies.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Ceremonies extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Ceremonies", - this.props.className - ); - - return ( -
-
-
-

Ceremonies

-
-
-
-
-

Opening Ceremony

- -
-
-

Closing Ceremony

- -
-
-
-
- ); - } -} diff --git a/src/components/Resources2026/Ceremonies.scss b/src/components/Resources2026/Ceremonies.scss deleted file mode 100644 index 7102ea4..0000000 --- a/src/components/Resources2026/Ceremonies.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Ceremonies { - .ceremonies-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .ceremonies-videos { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - width: $section-width; - } - - .ceremonies-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .left-column, - .right-column { - flex-direction: column; - } - } -} - -@include media-breakpoint-up(md) { - .Ceremonies { - .ceremonies-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - .ceremonies-videos { - flex-direction: row; - } - - .left-column, - .right-column { - flex-direction: column; - width: 46%; - } - } - } -} diff --git a/src/components/Resources2026/FAQ.jsx b/src/components/Resources2026/FAQ.jsx deleted file mode 100644 index ec3f003..0000000 --- a/src/components/Resources2026/FAQ.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Accordion from 'react-bootstrap/Accordion'; -import Card from 'react-bootstrap/Card'; - -import { FAQS } from 'constants/faq'; - -export default class FAQ extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'FAQ', this.props.className); - - return ( -
-
-
-

FAQs

-
-

- If your question is not answered below, please reach out to us - at exploretechla@cs.ucla.edu for - more information! -

-
- {this._getStudentFAQ()} -
-
- ); - } - - _getStudentFAQ() { - return ( -
-
-

Student FAQs

-
-
- - {FAQS.map(faq => { - return ( - -
{faq.question}
-
- - -
{faq.answer}
- {faq.note && ( -

Note: {faq.note}

- )} -
-
-
- ) - })} -
-
- ); - } -} diff --git a/src/components/Resources2026/FAQ.scss b/src/components/Resources2026/FAQ.scss deleted file mode 100644 index dc177e3..0000000 --- a/src/components/Resources2026/FAQ.scss +++ /dev/null @@ -1,59 +0,0 @@ -.FAQ { - background-color: white; - - .faq-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - .faq-column-container { - flex-direction: row; - justify-content: space-between; - } - - .faq-centered-title { - flex-direction: column; - align-items: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } - - .faq-student, .faq-teacher, .faq-parent { - padding-bottom: 32px; - } -} - -@include media-breakpoint-up(md) { - .FAQ { - .faq-content { - justify-content: center; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.faq-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources2026/Maps.jsx b/src/components/Resources2026/Maps.jsx deleted file mode 100644 index 5ef5128..0000000 --- a/src/components/Resources2026/Maps.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import Button from 'react-bootstrap/Button'; - -export const Maps = (props) => { - const classNames = classnames('Section', 'Maps', props.className); - const { maps } = props - - return ( -
-
-
-

Maps

-
-
-
- Below are links to view all the maps you may need throughout the day! -
-
- {maps.map(map => { - return - // return
- // {map.src} - //
- })} -
-
-
- ); -} - -export default Maps; diff --git a/src/components/Resources2026/Maps.scss b/src/components/Resources2026/Maps.scss deleted file mode 100644 index 15edc14..0000000 --- a/src/components/Resources2026/Maps.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Maps { - background-color: #f5f5f5; - - .maps-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .maps-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 60vw; - } - } -} - -@include media-breakpoint-up(md) { - .Maps { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} diff --git a/src/components/Resources2026/Resources.scss b/src/components/Resources2026/Resources.scss deleted file mode 100644 index 58b8087..0000000 --- a/src/components/Resources2026/Resources.scss +++ /dev/null @@ -1,40 +0,0 @@ -// Resources Component Styles -@import "../Footer.scss"; -@import "./Ceremonies.scss"; -@import "./Schedule.scss"; -@import "./Workshops.scss"; -@import "./Maps.scss"; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } - - .full-length-button { - padding-top: 20px; - display: grid; - row-gap: 10px; - grid-template-columns: auto; - padding-bottom: 20px; - width: 100%; - } -} diff --git a/src/components/Resources2026/Schedule.jsx b/src/components/Resources2026/Schedule.jsx deleted file mode 100644 index cd9b5c5..0000000 --- a/src/components/Resources2026/Schedule.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { SCHEDULE2026 } from 'constants/2026schedule'; - -export default class Schedule extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Schedule', this.props.className); - - return ( -
-
-
-

Schedule

-
-
-
-

- {SCHEDULE2026.map(event => { - return

{event.time} {event.name}

- })} -

-
-
-
- ); - } -} diff --git a/src/components/Resources2026/Schedule.scss b/src/components/Resources2026/Schedule.scss deleted file mode 100644 index 9aaac1e..0000000 --- a/src/components/Resources2026/Schedule.scss +++ /dev/null @@ -1,54 +0,0 @@ -.Schedule { - background-color: white; - - .schedule-content { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: $section-width; - margin: auto; - - &:not(:last-child) { - padding-bottom: 32px; - } - - &.schedule-column-container { - flex-direction: column; - } - - .schedule-title { - align-items: center; - text-align: center; - } - - .left-column, .right-column { - &.text { - padding-top: 32px; - } - } - } -} - -@include media-breakpoint-up(md) { - .Schedule { - .schedule-content { - justify-content: space-between; - - &:not(:last-child) { - padding-bottom: 64px; - } - - &.schedule-column-container { - flex-direction: row; - } - - .left-column, .right-column { - width: 46%; - - &.text { - padding-top: 0px; - } - } - } - } - } \ No newline at end of file diff --git a/src/components/Resources2026/Waivers.jsx b/src/components/Resources2026/Waivers.jsx deleted file mode 100644 index 2754dab..0000000 --- a/src/components/Resources2026/Waivers.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Button from 'react-bootstrap/Button'; - -import LiabilityWaiver from 'static/pdf/exploretech_la_2022_Liability_Waiver.pdf'; -import MultimediaRelease from 'static/pdf/exploretech_la_2022_Multimedia_Release_Form.pdf'; -import StudentVerification from 'static/pdf/exploretech_la_2022_Student_Verification_Form.pdf'; - -export default class Waivers extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - // render() { - // const classNames = classnames('Waivers', this.props.className); - - // return ( - //
- //
- //

Waivers

- //
- //

- // Please download and sign the following waivers. They are a required part of your registration. - //

- //

- // Note: For high school students, the waivers require a parent/guardian signature. - //

- //
- //
- //
- // - //
- //
- // - //
- //
- // - //
- //
- //
- // ); - // } -} diff --git a/src/components/Resources2026/Waivers.scss b/src/components/Resources2026/Waivers.scss deleted file mode 100644 index 855912e..0000000 --- a/src/components/Resources2026/Waivers.scss +++ /dev/null @@ -1,41 +0,0 @@ -.Waivers { - background-color: #F5F5F5; - display: flex; - flex-direction: column; - align-items: center; - margin: auto; - padding-bottom: 32px; - - .waivers-title, .waivers-text { - display: flex; - flex-direction: column; - align-items: center; - } - - .buttons { - padding-top: 20px; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 20px; - - .button { - padding-top: 10px; - padding-bottom: 10px; - } - } - -} - -@include media-breakpoint-up(md) { - .Waivers { - .buttons { - flex-direction: row; - - .button { - padding-left: 10px; - padding-right: 10px; - } - } - } -} \ No newline at end of file diff --git a/src/components/Resources2026/Workshops copy.jsx b/src/components/Resources2026/Workshops copy.jsx deleted file mode 100644 index 45ca9db..0000000 --- a/src/components/Resources2026/Workshops copy.jsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import Card from 'react-bootstrap/Card'; -import CardDeck from 'react-bootstrap/CardDeck'; -import ListGroup from 'react-bootstrap/ListGroup'; -import ListGroupItem from 'react-bootstrap/ListGroupItem'; -import YoutubeEmbed from "components/common/YoutubeEmbed"; - -export default class Workshops extends Component { - - static get propTypes() { - return { - className: PropTypes.string, - } - } - - static get defaultProps() { - return { - className: '', - }; - } - - render() { - const classNames = classnames('Section', 'Workshops', this.props.className); - - return ( -
-
-
-

Workshops

-
-
- {this._getWorkshopCards()} -
-
-
-

Panels

-
-
- {this._getPanelCards()} -
-
- ); - } - - _getWorkshopCards() { - return ( -
- - - - Coding 101 - - - (Session 1 Recording) - - - - Session 2 Recording - Student Guide - Slides - Solutions - - - - - ExploreWeb.la: Web Design & Dev - - - - Slides - Review Sheet - Codepen - - - - - - - Operating Systems - - - - Slides - Linux Emulator - - - - - Draw with Code - - - - Slides - Repl.it Project - - - - - - - Less Quantum More Computing - - - - Slides - Definitions Doc - Google Colab - - - - - Crack the Code with Cryptography - - - - Slides - Escape Room Google Form - Escape Room Questions and Solutions - - - - - - - Escaping Reality with Game Development - - - - Slides - Codepen Activity - - - - - Murder Mystery! - - - - Slides - Worksheet - - - - - - - AI Adventures - - - - Slides - Worksheet - - - - - Digital Design - - - - Slides - Miro Whiteboard - - - - - - - Data Science - - - - Slides - Spreadsheet - - - - - Tech & Society - - - - Slides - - - - - - - Bot Buddy - Hosted by DIY Girls - - - (Session 1 Recording) - - - - Session 2 Recording - Skin Templates - Youtube Tutorial - Slides - Follow Up Instructions - - - -
- ); - } - - _getPanelCards() { - return ( -
- - - - Diverse Careers in Computer Science - - - - Slides - Padlet - Panelist Info and Resources - - - - - Representation in Computer Science - - - - Slides - Padlet - - - -
- ); - } -} diff --git a/src/components/Resources2026/Workshops copy.scss b/src/components/Resources2026/Workshops copy.scss deleted file mode 100644 index a0b3782..0000000 --- a/src/components/Resources2026/Workshops copy.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import 'components/common/YoutubeEmbed.scss'; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - } -} \ No newline at end of file diff --git a/src/components/Resources2026/Workshops.jsx b/src/components/Resources2026/Workshops.jsx deleted file mode 100644 index 67ec717..0000000 --- a/src/components/Resources2026/Workshops.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import Card from "react-bootstrap/Card"; -import CardDeck from "react-bootstrap/CardDeck"; -import ListGroup from "react-bootstrap/ListGroup"; -import ListGroupItem from "react-bootstrap/ListGroupItem"; - -import { WORKSHOPS2026 } from "constants/2026workshop"; - -export default class Workshops extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Workshops", this.props.className); - - const workshopItems = WORKSHOPS2026.filter((item) => item.type === "workshop"); - const panelItems = WORKSHOPS2026.filter((item) => item.type === "panel"); - - return ( -
-
-
-

Workshops

-
-
- {this._renderCards(workshopItems)} -
- -
-
-

Panels

-
-
- {this._renderCards(panelItems)} -
-
- ); - } - - _chunkArray(items, size = 2) { - const rows = []; - for (let i = 0; i < items.length; i += size) { - rows.push(items.slice(i, i + size)); - } - return rows; - } - - _renderCards(items) { - const rows = this._chunkArray(items, 2); - - return ( -
- {rows.map((row, rowIndex) => ( - - {row.map((item) => ( - - - - {item.title} - - - {item.instructors && ( - - {item.type === "panel" ? "Moderators" : "Instructors"}: {item.instructors} - - )} - - - - {item.description && ( - - {item.description} - - )} - - {item.slidesUrl && ( - - Slides - - )} - - {item.panelistInfoUrl && ( - - Panelist Info - - )} - - - ))} - - ))} -
- ); - } -} \ No newline at end of file diff --git a/src/components/Resources2026/Workshops.scss b/src/components/Resources2026/Workshops.scss deleted file mode 100644 index 225667e..0000000 --- a/src/components/Resources2026/Workshops.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import "components/common/YoutubeEmbed.scss"; - -.Workshops { - .workshops-content { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - max-width: $section-width; - margin: auto; - - .workshops-cards { - display: flex; - flex-direction: column; - width: 75vw; - padding-bottom: 50px; - } - - .workshops-title { - display: flex; - padding-bottom: 32px; - flex-direction: column; - align-items: center; - } - - .card-row { - padding-top: 15px; - padding-bottom: 15px; - } - - .panelists-container { - max-width: $section-width; - margin: auto; - - .panelists-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } - } -} diff --git a/src/components/Resources2026/exploretech2021.jsx b/src/components/Resources2026/exploretech2021.jsx deleted file mode 100644 index 429ccf1..0000000 --- a/src/components/Resources2026/exploretech2021.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import Ceremonies from './Ceremonies'; -import Workshops from './Workshops'; -import Footer from '../Footer'; - -function Resources() { - return ( -
-
-

Resources

-
-
-
- Welcome students! This webpage contains the recordings and resources of all of our - workshops and panels for the exploretech.la 2021 event! For each workshop or panel, - please watch the recording first and check back on this page to access the appropriate - resources for the activity. -
-
- - -
-
-
- ); -} - -Resources.displayName = 'Resources'; -export default Resources; \ No newline at end of file diff --git a/src/components/Resources2026/exploretech2021.scss b/src/components/Resources2026/exploretech2021.scss deleted file mode 100644 index 907b4e6..0000000 --- a/src/components/Resources2026/exploretech2021.scss +++ /dev/null @@ -1,29 +0,0 @@ -// Resources Component Styles -@import 'components/Footer.scss'; -@import 'components/Resources/Ceremonies.scss'; -@import 'components/Resources/Workshops.scss'; - -.Resources { - .resources-title { - display: flex; - flex-direction: column; - align-items: center; - max-width: $section-width; - margin: auto; - padding-top: 32px; - padding-bottom: 32px; - } - - .resources-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - } - - .banner { - width: 100vw; - height: auto; - } -} \ No newline at end of file diff --git a/src/components/Schedule.jsx b/src/components/Schedule.jsx deleted file mode 100644 index f9bdbb8..0000000 --- a/src/components/Schedule.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Component } from "react"; - -import Container from "react-bootstrap/Container"; -import Row from "react-bootstrap/Row"; -import Col from "react-bootstrap/Col"; - -import UnorderedItemList from "./common/UnorderedItemList"; - -const scheduleItems = [ - "Opening Ceremony", - "Exhbition Hall", - "Workshops", - "Closing Ceremony", -]; - -export default class Schedule extends Component { - render() { - return ( -
- {/* */} -
- ); - } -} diff --git a/src/components/Speakers.jsx b/src/components/Speakers.jsx deleted file mode 100644 index d3e5c47..0000000 --- a/src/components/Speakers.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import HomePageSections from "constants/HomePageSections"; -import { PastSpeakers } from "constants/speakers"; - -const SpeakersTypeEnum = { - CURRENT: { - id: 1, - name: "Current", - }, - PAST: { - id: 2, - name: "Past", - }, -}; - -export default class Speakers extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Speakers", this.props.className); - return ( -
- {this._renderSpeakers(PastSpeakers, SpeakersTypeEnum.PAST)} -
- ); - } - - _renderSpeakers(speakers, speakerType) { - if (!speakers || speakers.length === 0) { - return null; - } - - return ( -
-
-

{speakerType.name} Speakers

-
-
- -
- ); - } -} diff --git a/src/components/Speakers.scss b/src/components/Speakers.scss deleted file mode 100644 index 191224f..0000000 --- a/src/components/Speakers.scss +++ /dev/null @@ -1,87 +0,0 @@ -.Speakers { - p { - margin-bottom: 0.25rem; - } - - a { - text-decoration: none !important; - } - - .speakers-container { - max-width: $section-width; - margin: auto; - - .speakers-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - - .speakers-list { - display: flex; - flex-direction: row; - overflow-x: scroll; - padding-bottom: 1rem; - transform: scaleX(-1) rotateZ(180deg); - } - - .speaker { - width: 14rem; - height: 100%; - padding: 1rem 0.5rem; - text-align: center; - cursor: pointer; - color: black; - transition: color 0.5s; - transform: scaleX(-1) rotateZ(180deg); - - .speaker-image { - width: 10rem; - height: auto; - border-radius: 50%; - margin-bottom: 1.25rem; - } - - .speaker-title { - font-size: small; - } - } - } - - .speakers-container:not(:last-child) { - padding-bottom: 64px; - } -} - -@include media-breakpoint-up(sm) { - .Speakers { - .speakers-container { - .speaker:hover { - color: $color-muted-blue; - transition: color 0.5s; - } - } - } -} - -@include media-breakpoint-up(md) { - .Speakers { - .speakers-container { - .speakers-header { - margin-bottom: 0; - } - - .speakers-list { - transform: none; - justify-content: center; - flex-wrap: wrap; - padding-bottom: 0; - } - - .speaker { - transform: none; - } - } - } -} diff --git a/src/components/Sponsors.jsx b/src/components/Sponsors.jsx deleted file mode 100644 index 6af64b9..0000000 --- a/src/components/Sponsors.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; -import ReactGA from "react-ga"; - -import { MdKeyboardArrowRight } from "react-icons/md"; - -import HomePageSections from "constants/HomePageSections"; -import { LargeLogoSponsors, SmallLogoSponsors } from "constants/sponsors"; -// import { CommunityPartners } from 'constants/community-partners'; - -export default class Sponsors extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - // Sponsor logos live at the bottom of the home page, so they are all - // lazy; the two rows are rendered at different widths. - const largeLogos = LargeLogoSponsors.map((logo) => - this._renderLogo(logo, "(min-width: 768px) 16rem, 14rem") - ); - const smallLogos = SmallLogoSponsors.map((logo) => - this._renderLogo(logo, "(min-width: 768px) 12rem, 8rem") - ); - - // const commPartnerLogos = CommunityPartners.map(logo => this._renderLogo(logo)); - - const classNames = classnames("Section", "Sponsors", this.props.className); - - return ( -
-
-
-

Our Sponsors

-
-
-
{largeLogos}
-
{smallLogos}
- {/*
-

Our Community Partners

-
-
-
- {commPartnerLogos} -
*/} -
-
Interested in sponsoring?
-
- -

Email Us!

- -
-
-
-
-
- ); - } - - _renderLogo(logo, sizes) { - if (!logo || !logo.name || !logo.src) return null; - // Both raster variants and the retained SVG include intrinsic dimensions. - const { name, website, ...image } = logo; - return ( -
- - {name} - -
- ); - } -} diff --git a/src/components/Sponsors.scss b/src/components/Sponsors.scss deleted file mode 100644 index 3e58b12..0000000 --- a/src/components/Sponsors.scss +++ /dev/null @@ -1,118 +0,0 @@ -.Sponsors { - background-color: #f5f5f5; - - .sponsors-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - max-width: $section-width; - margin: auto; - - .sponsors-header { - display: flex; - flex-direction: column; - align-items: center; - } - - .logo-container { - margin: 1rem; - } - - .large-logos, - .small-logos { - display: flex; - flex-flow: row wrap; - justify-content: center; - align-items: center; - } - - .large-logos, - .small-logos { - padding-bottom: 32px; - } - - .large-logos { - .logo { - width: 14rem; - height: auto; - } - } - - .small-logos { - .logo { - width: 8rem; - height: auto; - } - } - - .sponsor-contact { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - h5 { - margin-bottom: 1rem; - } - - .sponsor-cta { - a { - display: flex; - flex-direction: row; - align-items: center; - text-decoration: none; - color: $color-muted-blue; - } - - &:hover { - .MdKeyboardArrowRight { - fill: white; - background-color: $color-muted-blue; - border: 2px solid $color-muted-blue; - } - } - - p { - margin-bottom: 0; - margin-right: 0.5rem; - } - } - } - } - - .MdKeyboardArrowRight { - fill: $color-muted-blue; - border: 2px solid #e2e2e2; - border-radius: 50%; - width: 1.5rem; - min-width: 1.5rem; - height: 1.5rem; - transition: all 0.3s ease-in-out; - -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ - -moz-transition: all 0.3s ease-in-out; /** Firefox **/ - -o-transition: all 0.3s ease-in-out; /** Opera **/ - } -} - -@include media-breakpoint-up(md) { - .Sponsors { - .sponsors-container { - .logo-container { - margin: 2rem; - } - - .large-logos { - .logo { - width: 16rem; - } - } - - .small-logos { - .logo { - width: 12rem; - } - } - } - } -} diff --git a/src/components/Team/Content.jsx b/src/components/Team/Content.jsx deleted file mode 100644 index f72783e..0000000 --- a/src/components/Team/Content.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import { Content } from "constants/content"; - -export default class ContentComponent extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Content", this.props.className); - return ( -
-
-
-

Content

-
-
- -
-
- ); - } -} diff --git a/src/components/Team/Content.scss b/src/components/Team/Content.scss deleted file mode 100644 index bcadbe7..0000000 --- a/src/components/Team/Content.scss +++ /dev/null @@ -1,23 +0,0 @@ -.Content { - .content-container { - max-width: $section-width; - margin: auto; - - .content-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .Content { - .content-container { - .content-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/Team/Design.jsx b/src/components/Team/Design.jsx deleted file mode 100644 index 72d63bf..0000000 --- a/src/components/Team/Design.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import { Design } from "constants/design"; - -export default class DesignComponent extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "Design", this.props.className); - return ( -
-
-
-

Design

-
-
- -
-
- ); - } -} diff --git a/src/components/Team/Design.scss b/src/components/Team/Design.scss deleted file mode 100644 index 946db64..0000000 --- a/src/components/Team/Design.scss +++ /dev/null @@ -1,25 +0,0 @@ -.Design { - background-color: #f5f5f5; - - .design-container { - max-width: $section-width; - margin: auto; - - .design-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .Design { - .design-container { - .design-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/Team/External.jsx b/src/components/Team/External.jsx deleted file mode 100644 index 29101f6..0000000 --- a/src/components/Team/External.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import { External } from "constants/external"; - -export default class ExternalComponent extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("Section", "External", this.props.className); - return ( -
-
-
-

External

-
-
- -
-
- ); - } -} diff --git a/src/components/Team/External.scss b/src/components/Team/External.scss deleted file mode 100644 index 3df7dd7..0000000 --- a/src/components/Team/External.scss +++ /dev/null @@ -1,23 +0,0 @@ -.External { - .external-container { - max-width: $section-width; - margin: auto; - - .external-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .External { - .external-container { - .external-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/Team/Leadership.jsx b/src/components/Team/Leadership.jsx deleted file mode 100644 index 283a87d..0000000 --- a/src/components/Team/Leadership.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import HomePageSections from "constants/HomePageSections"; -import { Leadership } from "constants/leadership"; - -export default class LeadershipComponent extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Leadership", - this.props.className - ); - return ( -
-
-
-

Leadership

-
-
- -
-
- ); - } -} diff --git a/src/components/Team/Leadership.scss b/src/components/Team/Leadership.scss deleted file mode 100644 index 0a92709..0000000 --- a/src/components/Team/Leadership.scss +++ /dev/null @@ -1,25 +0,0 @@ -.Leadership { - background-color: #f5f5f5; - - .leadership-container { - max-width: $section-width; - margin: auto; - - .leadership-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .Leadership { - .leadership-container { - .leadership-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/Team/Operations.jsx b/src/components/Team/Operations.jsx deleted file mode 100644 index bcac0d9..0000000 --- a/src/components/Team/Operations.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import People from "components/common/People"; - -import { Operations } from "constants/operations"; - -export default class OperationsComponent extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames( - "Section", - "Operations", - this.props.className - ); - return ( -
-
-
-

Operations

-
-
- -
-
- ); - } -} diff --git a/src/components/Team/Operations.scss b/src/components/Team/Operations.scss deleted file mode 100644 index a40a3dd..0000000 --- a/src/components/Team/Operations.scss +++ /dev/null @@ -1,25 +0,0 @@ -.Operations { - background-color: #f5f5f5; - - .operations-container { - max-width: $section-width; - margin: auto; - - .operations-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - } -} - -@include media-breakpoint-up(md) { - .Opertions { - .operations-container { - .operations-header { - margin-bottom: 0; - } - } - } -} diff --git a/src/components/Team/Team.jsx b/src/components/Team/Team.jsx deleted file mode 100644 index 92f31a6..0000000 --- a/src/components/Team/Team.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -import { useLocation, Link } from "react-router-dom"; - -import Leadership from "./Leadership"; -import Content from "./Content"; -import Design from "./Design"; -import Operations from "./Operations"; -import External from "./External"; -import Footer from "../Footer"; -import People from "../common/People"; -import { WebDev } from "constants/webdev"; -import { Marketing } from "constants/marketing"; -import "./Team.scss"; - -function Team() { - const location = useLocation(); - - // Extract section from URL path - const pathParts = location.pathname.split('/').filter(Boolean); - const activeSection = pathParts[pathParts.length - 1] === 'our_team' - ? 'leadership' - : pathParts[pathParts.length - 1] || 'leadership'; - - const renderTeamSection = () => { - switch (activeSection) { - case "leadership": - return ; - case "content": - return ; - case "design": - return ; - case "operations": - return ; - case "external": - return ; - case "web-dev": - return ( -
-
-

Web Dev

-
-
- -
- ); - case "marketing": - return ( -
-
-

Marketing

-
-
- -
- ); - default: - return ; - } - }; - - return ( -
-
-

Meet Our Team

-
-
- -
- - Leadership - - - Content - - - Design - - - Operations - - - External - - - Web Dev - - - Marketing - -
- -
- {renderTeamSection()} -
- -
-
- ); -} - -Team.displayName = "Team"; - -export default Team; diff --git a/src/components/Team/Team.scss b/src/components/Team/Team.scss deleted file mode 100644 index 78f318d..0000000 --- a/src/components/Team/Team.scss +++ /dev/null @@ -1,170 +0,0 @@ -// Team Component Styles -@import "../Footer.scss"; - -.Team { - display: flex; - flex-direction: column; - align-items: center; - min-height: 100vh; - background-color: #f5f5f5; - - .team-title { - text-align: center; - width: 100%; - background-color: white; - padding: 2rem 0; - margin-bottom: 2rem; - - h1 { - font-size: 2.5rem; - color: #333; - margin-bottom: 1rem; - } - - .pill-divider { - width: 60px; - height: 4px; - background-color: #448494; - margin: 0 auto; - border-radius: 2px; - } - } - - .team-navigation { - display: flex; - gap: 1rem; - margin-bottom: 2rem; - flex-wrap: wrap; - justify-content: center; - padding: 0 1rem; - - a { - padding: 0.75rem 1.5rem; - border: 2px solid #448494; - background-color: transparent; - color: #448494; - border-radius: 25px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - text-decoration: none; - display: inline-block; - - &:hover { - background-color: #448494; - color: white; - } - - &.active { - background-color: #448494; - color: white; - } - } - } - - .team-section-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 1rem; - } - - .team-content { - width: 100%; - max-width: 1200px; - padding: 0 1rem; - flex: 1; - position: relative; - - .coming-soon-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(255, 255, 255, 0.9); - display: flex; - justify-content: center; - align-items: center; - z-index: 10; - - h2 { - font-size: 2.5rem; - color: #448494; - font-weight: 500; - } - } - - .coming-soon-section { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 400px; - text-align: center; - padding: 2rem; - - h2 { - font-size: 2.5rem; - color: #448494; - font-weight: 500; - margin-bottom: 1rem; - } - - p { - font-size: 1.2rem; - color: #666; - max-width: 500px; - line-height: 1.6; - } - } - } - - .team-text { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: 30px; - padding-bottom: 30px; - } - - .banner { - width: 100vw; - height: auto; - } - - // Ensure the footer is always full width and not centered - > .Footer { - align-self: stretch; - width: 100%; - } -} - -// Responsive styles -@media (max-width: 768px) { - .Team { - .team-navigation { - a { - padding: 0.5rem 1rem; - font-size: 0.9rem; - } - } - - .team-content { - .coming-soon-section { - min-height: 300px; - padding: 1rem; - - h2 { - font-size: 2rem; - } - - p { - font-size: 1rem; - } - } - } - } -} diff --git a/src/components/common/Alerts.jsx b/src/components/common/Alerts.jsx deleted file mode 100644 index 2100cbe..0000000 --- a/src/components/common/Alerts.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { Component } from "react"; -import ReactGA from "react-ga"; - -import Alert from "react-bootstrap/Alert"; -import Button from "react-bootstrap/Button"; - -export default class Alerts extends Component { - render() { - return ( -
-
{this._getAlert()}
-
- ); - } - - _getAlert() { - return ( - - - {this.props.title} - - {this.props.messages.map((message) => ( -

{message}

- ))} -
-
-
- -
- {this.props.secondaryLink && ( -
- -
- )} -
-
- ); - } - - _trackTeamApplicationClick() { - ReactGA.outboundLink( - { - label: "Team Application", - }, - () => {} - ); - } - - _trackInfoSessionSlidesClick() { - ReactGA.outboundLink( - { - label: "Info Session", - }, - () => {} - ); - } -} diff --git a/src/components/common/Alerts.scss b/src/components/common/Alerts.scss deleted file mode 100644 index dc18638..0000000 --- a/src/components/common/Alerts.scss +++ /dev/null @@ -1,40 +0,0 @@ -.Alerts { - background-color: #d1ecf1; - padding-top: 1em !important; - padding-bottom: 1em !important; - - .alert { - border: 0 solid transparent; - margin-bottom: 0; - } - - .alerts-container { - max-width: $section-width; - margin: auto; - } - - .buttons { - padding-top: 10px; - display: flex; - flex-direction: row; - - .button { - padding-top: 10px; - padding-right: 10px; - padding-bottom: 10px; - } - } - - .MdKeyboardArrowRight { - fill: $color-muted-blue; - border: 2px solid #e2e2e2; - border-radius: 50%; - width: 1.5rem; - min-width: 1.5rem; - height: 1.5rem; - transition: all 0.3s ease-in-out; - -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ - -moz-transition: all 0.3s ease-in-out; /** Firefox **/ - -o-transition: all 0.3s ease-in-out; /** Opera **/ - } -} diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx deleted file mode 100644 index b8116f0..0000000 --- a/src/components/common/Header.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; -import classnames from "classnames"; - -import { Navbar, Nav, NavDropdown } from "react-bootstrap"; - -import HomePageSections from "constants/HomePageSections"; - -import LogoWithIcons from "static/svg/logo-with-icons-navy.svg"; -import CompassLogo from "static/svg/logo-compass.svg"; - -const IGNITE_ENABLED = false; - -function Header() { - // let navItems = []; - let isHomePage = false; - let isIgnitePage = false; - - const location = useLocation(); - const history = useHistory(); - const pendingHash = useRef(null); - const [expanded, setExpanded] = useState(false); - useEffect(() => { - pendingHash.current = history.action === "POP" ? null : location.hash; - setExpanded(false); - }, [location.key, location.hash, history]); - if (location.pathname === "/") { - isHomePage = true; - } - - if (location.pathname === "/ignite" && IGNITE_ENABLED) { - isIgnitePage = true; - } - - // if (location.pathname === '/our_team') { - // navItems = ourTeamNavItems; - // } - // if (location.pathname === '/resources') { - // navItems = resourcesNavItems; - // } - - const classNames = classnames("Section", "Header", { - Home: isHomePage, - IgniteHeader: isIgnitePage, - }); - - const navBarItems = ( - - ); - - return ( - { - pendingHash.current = null; - setExpanded(next); - }} - expand="sm" - > - - logo-compass - logo-with-icons - - - { - const target = - pendingHash.current && - document.getElementById(pendingHash.current.slice(1)); - pendingHash.current = null; - if (target) target.scrollIntoView(); - }} - > - - - - ); -} - -export default Header; diff --git a/src/components/common/Header.scss b/src/components/common/Header.scss deleted file mode 100644 index dbac997..0000000 --- a/src/components/common/Header.scss +++ /dev/null @@ -1,62 +0,0 @@ -.Header { - &.Home { - background-color: $color-aqua; - } - - &.Section { - padding-top: 1rem; - padding-bottom: 1rem; - } - - .navbar-nav { - padding-top: 0.5rem; - - .nav-item, - .nav-link, - .dropdown { - margin-left: 0.5rem; - margin-right: 0.5rem; - } - } - - .logo-compass { - display: block; - width: 2.5rem; - height: auto; - } - - .logo-with-icons { - display: none; - max-width: 14rem; - height: auto; - } -} - -@include media-breakpoint-up(sm) { - .Header { - .navbar-nav { - padding-top: 0; - } - } -} - -@include media-breakpoint-up(md) { - .Header { - .logo-compass { - display: none; - } - - .logo-with-icons { - display: block; - } - } -} - -.Header.IgniteHeader { - background-color: #2da6bc !important; -} - -.Header.IgniteHeader .navbar, -.Header.IgniteHeader.navbar { - background-color: #2da6bc !important; -} diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx new file mode 100644 index 0000000..01ecf49 --- /dev/null +++ b/src/components/common/Header.tsx @@ -0,0 +1,208 @@ +import { useEffect, useRef, useState } from "react"; +import { Link, useLocation, useNavigationType } from "react-router-dom"; +import HomePageSections from "../../content/sections"; +import LogoWithIcons from "../../static/svg/logo-with-icons-navy.svg"; +import CompassLogo from "../../static/svg/logo-compass.svg"; +import Collapse from "../Collapse"; +import { EVENT_ROUTES } from "../../content/events"; + +const menus = [ + { + id: "home", + title: "Home", + links: [ + ["Home", "/"], + ["About", `/#${HomePageSections.ABOUT.name}`], + ["Get Involved", `/#${HomePageSections.GET_INVOLVED.name}`], + ["Speakers", `/#${HomePageSections.SPEAKERS.name}`], + ["Sponsors", `/#${HomePageSections.SPONSORS.name}`], + ], + }, + { id: "events", title: "Events", links: [["Ignite", "/ignite"]] }, + { + id: "resources", + title: "Resources", + links: [...EVENT_ROUTES] + .reverse() + .map(({ route, navLabel }) => [navLabel, route] as const), + }, +] as const; + +type MenuId = (typeof menus)[number]["id"]; +export default function Header() { + const location = useLocation(); + const navigationType = useNavigationType(); + const [expanded, setExpanded] = useState(false); + const [menu, setMenu] = useState(null); + const header = useRef(null); + const pendingHash = useRef(null); + + useEffect(() => { + pendingHash.current = navigationType === "POP" ? null : location.hash; + setMenu(null); + setExpanded(false); + }, [location.key, location.hash, navigationType]); + + useEffect(() => { + if (!menu) return; + const closeOutside = (event: MouseEvent) => { + if ( + event.target instanceof Node && + !header.current?.contains(event.target) + ) + setMenu(null); + }; + document.addEventListener("click", closeOutside); + return () => document.removeEventListener("click", closeOutside); + }, [menu]); + + const selectLink = (href: string) => { + const hash = new URL(href, window.location.href).hash; + pendingHash.current = hash; + setMenu(null); + setExpanded(false); + // Same-hash links may not change location, so scroll even without a route effect. + if (!expanded && hash) + requestAnimationFrame(() => + document.getElementById(hash.slice(1))?.scrollIntoView(), + ); + }; + + const renderMenu = (item: (typeof menus)[number]) => ( +
{ + if (event.key === "Escape") { + setMenu(null); + document.getElementById(`navbar-${item.id}`)?.focus(); + event.preventDefault(); + } + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + if (event.key === "ArrowUp" && menu !== item.id) return; + event.preventDefault(); + setMenu(item.id); + const parent = event.currentTarget; + const backwards = event.key === "ArrowUp"; + requestAnimationFrame(() => { + const links = Array.from( + parent.querySelectorAll(".site-menu-link"), + ); + const index = links.findIndex( + (link) => link === document.activeElement, + ); + const next = Math.max( + 0, + Math.min(links.length - 1, index + (backwards ? -1 : 1)), + ); + links[next]?.focus(); + }); + } + }} + onBlur={(event) => { + if ( + event.relatedTarget instanceof Node && + !event.currentTarget.contains(event.relatedTarget) + ) + setMenu(null); + }} + > + { + event.preventDefault(); + setMenu((current) => (current === item.id ? null : item.id)); + }} + onKeyDown={(event) => { + if (event.key === " ") { + event.preventDefault(); + setMenu((current) => (current === item.id ? null : item.id)); + } + }} + > + {item.title} + + {menu === item.id && ( +
+ {item.links.map(([label, href]) => ( + selectLink(href)} + > + {label} + + ))} +
+ )} +
+ ); + + return ( + + ); +} diff --git a/src/components/common/LinkWithArrow.jsx b/src/components/common/LinkWithArrow.jsx deleted file mode 100644 index 3ad0a13..0000000 --- a/src/components/common/LinkWithArrow.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -import { MdKeyboardArrowRight } from "react-icons/md"; - -const propTypes = { - className: PropTypes.string, - text: PropTypes.string.isRequired, - to: PropTypes.string, -}; - -const defaultProps = { - className: "", - to: null, -}; - -function LinkWithArrow(props) { - const { className, text, to } = props; - const classNames = classnames("LinkWithArrow", className); - return ( -
- - {text} - - -
- ); -} - -LinkWithArrow.displayName = "LinkWithArrow"; -LinkWithArrow.propTypes = propTypes; -LinkWithArrow.defaultProps = defaultProps; -export default LinkWithArrow; diff --git a/src/components/common/LinkWithArrow.scss b/src/components/common/LinkWithArrow.scss deleted file mode 100644 index a61aa3f..0000000 --- a/src/components/common/LinkWithArrow.scss +++ /dev/null @@ -1,41 +0,0 @@ -.LinkWithArrow { - display: flex; - flex-direction: row; - align-items: center; - - a { - display: flex; - flex-direction: row; - align-items: center; - text-decoration: none; - - &:hover { - text-decoration: none; - - .MdKeyboardArrowRight { - fill: white; - background-color: $color-muted-blue; - border: 2px solid $color-muted-blue; - } - } - - .link-text { - margin-bottom: 0; - margin-right: 0.5rem; - color: $color-muted-blue; - } - } - - .MdKeyboardArrowRight { - fill: $color-muted-blue; - border: 2px solid #e2e2e2; - border-radius: 50%; - width: 1.5rem; - min-width: 1.5rem; - height: 1.5rem; - transition: all 0.3s ease-in-out; - -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ - -moz-transition: all 0.3s ease-in-out; /** Firefox **/ - -o-transition: all 0.3s ease-in-out; /** Opera **/ - } -} diff --git a/src/components/common/LinkWithArrow.tsx b/src/components/common/LinkWithArrow.tsx new file mode 100644 index 0000000..12f9b60 --- /dev/null +++ b/src/components/common/LinkWithArrow.tsx @@ -0,0 +1,18 @@ +import { Link } from "react-router-dom"; +import { MdKeyboardArrowRight } from "react-icons/md"; + +interface LinkWithArrowProps { + readonly text: string; + readonly to: string; +} + +export default function LinkWithArrow({ text, to }: LinkWithArrowProps) { + return ( +
+ + {text} + + +
+ ); +} diff --git a/src/components/common/People.jsx b/src/components/common/People.jsx deleted file mode 100644 index a583fbf..0000000 --- a/src/components/common/People.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; -import ReactGA from "react-ga"; - -export default class People extends Component { - static get propTypes() { - return { - className: PropTypes.string, - people: PropTypes.array.isRequired, - eagerCount: PropTypes.number, - }; - } - - static get defaultProps() { - return { - className: "", - people: [], - eagerCount: 2, - }; - } - - render() { - if (!this.props.people || this.props.people.length === 0) { - return null; - } - - const peopleElements = this.props.people.map((person, index) => - this._renderPerson(person, index) - ); - const classNames = classnames("People", this.props.className); - - return
{peopleElements}
; - } - - _renderPerson(person, index) { - if (!person || !person.name) { - return null; - } - - const { name, title, descriptions, image, link } = person; - if (link) { - return ( - -
- {image ? ( - {name} - ) : ( - - - ); - } - return ( -
- {image ? ( - {name} - ) : ( - - ); - } -} diff --git a/src/components/common/People.scss b/src/components/common/People.scss deleted file mode 100644 index 38efd68..0000000 --- a/src/components/common/People.scss +++ /dev/null @@ -1,65 +0,0 @@ -.People { - display: flex; - flex-direction: row; - overflow-x: scroll; - padding-bottom: 1rem; - transform: scaleX(-1) rotateZ(180deg); - - p { - margin-bottom: 0.25rem; - } - - a { - text-decoration: none !important; - } - - .person { - width: 14rem; - height: 100%; - padding: 1rem 0.5rem; - text-align: center; - color: black; - transition: color 0.5s; - transform: scaleX(-1) rotateZ(180deg); - - .person-image { - width: 10rem; - height: 10rem; - border-radius: 50%; - margin-bottom: 1.25rem; - object-fit: cover; - object-position: center; - transition: filter 0.3s ease; - } - - .person-title { - font-size: small; - } - - .person-description { - font-size: small; - } - } -} - -@include media-breakpoint-up(sm) { - .People { - .person:hover { - color: $color-muted-blue; - transition: color 0.5s; - } - } -} - -@include media-breakpoint-up(md) { - .People { - transform: none; - justify-content: center; - flex-wrap: wrap; - padding-bottom: 0; - - .person { - transform: none; - } - } -} diff --git a/src/components/common/People.tsx b/src/components/common/People.tsx new file mode 100644 index 0000000..a1838a3 --- /dev/null +++ b/src/components/common/People.tsx @@ -0,0 +1,76 @@ +import type { PersonCard } from "../../content/types"; +import OutboundLink from "../OutboundLink"; + +interface PeopleProps { + readonly people: readonly PersonCard[]; + readonly className?: string; + /** Number of leading portraits loaded eagerly; the rest load lazily. */ + readonly eagerCount?: number; +} + +/** + * Horizontal-scrolling card list shared by team sections, speakers and + * panelists. Cards with a profile link become tracked outbound anchors. + */ +export default function People({ + people, + className, + eagerCount = 2, +}: PeopleProps) { + if (people.length === 0) { + return null; + } + + return ( +
+ {people.map((person, index) => { + const { name, title, descriptions, image, link } = person; + const body = ( + <> + {image ? ( + {name} + ) : ( + + ); +} diff --git a/src/components/common/UnorderedItemList.jsx b/src/components/common/UnorderedItemList.jsx deleted file mode 100644 index 0a82ea4..0000000 --- a/src/components/common/UnorderedItemList.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { PureComponent } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -export default class UnorderedItemList extends PureComponent { - static get propTypes() { - return { - className: PropTypes.string, - items: PropTypes.arrayOf(PropTypes.string).isRequired, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } - - render() { - const classNames = classnames("UnorderedItemList", this.props.className); - const items = this.props.items.map((item, index) => - this._getItem(index + 1, item) - ); - - return
{items}
; - } - - _getItem(number, content) { - return ( -
-
-
{content}
-
- ); - } -} diff --git a/src/components/common/UnorderedItemList.scss b/src/components/common/UnorderedItemList.scss deleted file mode 100644 index c5ae802..0000000 --- a/src/components/common/UnorderedItemList.scss +++ /dev/null @@ -1,36 +0,0 @@ -.UnorderedItemList { - .item { - display: flex; - align-items: center; - width: 100%; - padding-bottom: 24px; - - .item-indicator { - height: 24px; - width: 24px; - border-radius: 50%; - background-color: #448494; - text-align: center; - line-height: 1.5; - flex-shrink: 0; - margin-right: 16px; - } - - .item-indicator::after { - content: ""; - display: block; - margin-left: 11px; - width: 2px; - height: 50px; - background-color: #448494; - } - - &:last-child { - padding-bottom: 0; - - .item-indicator::after { - display: none; - } - } - } -} diff --git a/src/components/common/YoutubeEmbed.scss b/src/components/common/YoutubeEmbed.scss deleted file mode 100644 index f4bbac0..0000000 --- a/src/components/common/YoutubeEmbed.scss +++ /dev/null @@ -1,61 +0,0 @@ -// Reserve the same 16:9 slot for the button and the activated player. -.video-responsive { - width: 300px; - max-width: 100%; - - .video-frame { - position: relative; - height: 0; - // Percentage padding resolves against the container width. - padding-bottom: 56.25%; - overflow: hidden; - background-color: #000000; - } - - .video-frame iframe, - .video-frame .video-facade { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - } - - .video-facade { - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - cursor: pointer; - background: radial-gradient(circle at center, #2f2f2f 0%, #000000 100%); - - &:hover, - &:focus { - .video-facade-play { - background-color: #ff0000; - } - } - } - - .video-facade-play { - position: relative; - display: block; - width: 54px; - height: 38px; - border-radius: 10px; - background-color: rgba(33, 33, 33, 0.85); - transition: background-color 0.2s ease-in-out; - - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-style: solid; - border-width: 9px 0 9px 15px; - border-color: transparent transparent transparent #ffffff; - } - } -} diff --git a/src/components/common/YoutubeEmbed.jsx b/src/components/common/YoutubeEmbed.tsx similarity index 67% rename from src/components/common/YoutubeEmbed.jsx rename to src/components/common/YoutubeEmbed.tsx index ea4830e..4c64b68 100644 --- a/src/components/common/YoutubeEmbed.jsx +++ b/src/components/common/YoutubeEmbed.tsx @@ -1,8 +1,15 @@ -import React, { useState } from "react"; -import PropTypes from "prop-types"; +import { useState } from "react"; -// No player or remote thumbnail request until the visitor chooses to play. -const YoutubeEmbed = ({ embedId, title }) => { +interface YoutubeEmbedProps { + readonly embedId: string; + readonly title?: string; +} + +/** No player or remote thumbnail request until the visitor chooses to play. */ +export default function YoutubeEmbed({ + embedId, + title = "Embedded youtube", +}: YoutubeEmbedProps) { const [activated, setActivated] = useState(false); return ( @@ -30,15 +37,4 @@ const YoutubeEmbed = ({ embedId, title }) => {
); -}; - -YoutubeEmbed.propTypes = { - embedId: PropTypes.string.isRequired, - title: PropTypes.string, -}; - -YoutubeEmbed.defaultProps = { - title: "Embedded youtube", -}; - -export default YoutubeEmbed; +} diff --git a/src/constants/2026schedule.js b/src/constants/2026schedule.js deleted file mode 100644 index 4e36ea4..0000000 --- a/src/constants/2026schedule.js +++ /dev/null @@ -1,27 +0,0 @@ -export const SCHEDULE2026 = [ - { - key: 1, - name: "Opening Ceremony", - time: "9:55AM - 10:10AM", - }, { - key: 2, - name: "Session 1", - time: "10:15AM - 10:50AM", - }, { - key: 3, - name: "Session 2", - time: "11:00AM - 11:40AM", - }, { - key: 4, - name: "Session 3", - time: "11:50AM - 12:30PM", - }, { - key: 5, - name: "Lunch with a Mentor", - time: "12:30PM - 1:10PM", - }, { - key: 6, - name: "Closing Ceremony", - time: "1:10PM - 1:30PM", - }, -] \ No newline at end of file diff --git a/src/constants/2026workshop.js b/src/constants/2026workshop.js deleted file mode 100644 index ed39e57..0000000 --- a/src/constants/2026workshop.js +++ /dev/null @@ -1,124 +0,0 @@ -export const WORKSHOPS2026 = [ - { - title: "Tower Stacking Game", - type: "workshop", - instructors: "Emma Qiu and Eli Drewry", - description: "Students will compete in groups to see who can create the tallest free-standing tower with limited materials (popsicle sticks, tape, rope, etc). This challenge tests their communication, strategy, and discovering the importance of architecture while getting a chance to win prizes.", - slidesUrl: "https://canva.link/79sw40e0u4vhk4j", - panelistInfoUrl: "", - }, - { - title: "Intro to Game Development", - type: "workshop", - instructors: "Joshua Jin and Paul Macapinlac", - description: "Learn how the snake game works in this fun workshop! Uncover the secrets of Python and learn to customize your snake. Before you know it, you’ll be a pro game dev with a fully customized snake game.", - slidesUrl: "https://canva.link/y27fiyfbq1pfd3t", - panelistInfoUrl: "", - }, - { - title: "Battle Bots", - type: "workshop", - instructors: "Sofia Matos and Benjamin Garcia", - description: "Drive XRP Robots on a field to take down your opponents in a game of robot soccer! After interacting with code, you will be able to drive and dribble past other robots to score using teamwork and strategy.", - slidesUrl: "https://canva.link/6edmsozmzcmhkf2", - panelistInfoUrl: "", - }, - { - title: "Squid Games Several-Legged Pentathalon (STEM Edition)", - type: "workshop", - instructors: "Matthew Chen and Paul Macapinlac", - description: "Inspired by the hit Netflix series “Squid Games,” collaborate with up to 3 of your friends to participate in the STEM Games Several-Legged Pentathlon, where you will be tasked to solve STEM puzzles and activities in an interactive race against time.", - slidesUrl: - "https://docs.google.com/presentation/d/10K0MQv8v8IqyC8qRrrJJanGZpn2YVPQUX6VFvdg_1ww/edit?slide=id.gf6d3ad9fd1_0_1011#slide=id.gf6d3ad9fd1_0_1011", - panelistInfoUrl: "", - }, - { - title: "AI Photobooth", - type: "workshop", - instructors: "Ridhima Seth and Joshua Jin", - description: "Do you want to learn more about AI image generation? Come to AI Photobooth! This workshop will show you the basics of how an AI image generator works, and then we’ll let you try it for yourself!", - slidesUrl: "https://canva.link/p62kuwtzwy1ieb6", - panelistInfoUrl: "", - }, - { - title: "COVID-19 Simulator", - type: "workshop", - instructors: "Clemente Irarrazaval and Eli Drewry", - description: "Students will get an introduction into the field of game development by designing a COVID-19 spread simulator. Using Scratch they’ll explore conditionals, loops, and movement while working through obstacles. Students work in teams to build, test, and modify their own simulation-based game, also encouraging teamwork.", - slidesUrl: "https://canva.link/5noyqe130bqgdqs", - panelistInfoUrl: "", - }, - { - title: "AI Music", - type: "workshop", - instructors: "Matthew Chen and Benjamin Garcia", - description: "The purpose of this workshop is to teach students how to use suno.com, an AI music website that allows users to create any form of music by simply entering into the prompt bar.", - slidesUrl: "https://canva.link/ja5m1l7cy6k1zsh", - panelistInfoUrl: "", - }, - { - title: "Bridge Building Competition", - type: "workshop", - instructors: "Atharv Panditrao and Clemente Irarrazaval", - description: "Students will design and build a small bridge using limited materials. They will learn basic structural concepts, apply problem-solving skills, and work collaboratively to create a bridge that can hold the greatest load. Students compete in teams and test their designs in a fun final challenge.", - slidesUrl: "https://canva.link/6ov265jv8fwkeql", - panelistInfoUrl: "", - }, - { - title: "Live Coding Music", - type: "workshop", - instructors: "Ana Laura Santana and Atharv Panditrao", - description: "Sometimes learning code can seem like it’s exclusively professional or something you will never see in your career. But in truth, coding can be useful for all types of passions beyond a corporate life, which is what gives it its beauty. In this workshop, you will learn how to become a DJ using only code!", - slidesUrl: "https://canva.link/9pwbi4o0ztadleh", - panelistInfoUrl: "", - }, - { - title: "Prompt Engineering", - type: "workshop", - instructors: "Alexander Chien and Arjun Asudani", - description: "Join our prompt engineering workshop to learn how to make the most out of your AI credits. Today’s chat models have vast capabilities, but accessing these skills can be more nuanced than just a straightforward question. We’ll cover prompting methods you can use to ensure you get the results you want. Let’s harness the power of AI!", - slidesUrl: - "https://docs.google.com/presentation/d/1IJsZaSjOSEOXJBQJAjdo4N0Zz3iFHkn5otzM8JBI4do/edit?usp=sharing", - panelistInfoUrl: "", - }, - { - title: "How Machines can learn like humans (Neural Networks)", - type: "workshop", - instructors: "Ana Laura Santana and Emma Qiu", - description: "Students will learn the basics of neural networks, and how they are present in our everyday lives. They will also get hands-on experience with neural networks by loading data and training their own model. In the end, students will gain a conceptual understanding and practical skills in AI, which has become increasingly influential in the tech world.", - slidesUrl: "https://canva.link/t49ze6ojz5edwil", - panelistInfoUrl: "", - }, - { - title: "AI Guess Who", - type: "workshop", - instructors: "Ridhima Seth and Amy Lloyd", - description: "Want to master Guess Who? with logic and strategy? Learn how decision trees and probability help you ask the smartest questions, minimize guesswork, and consistently win. This session makes algorithmic thinking fun and practical—sharpen your problem-solving skills while gaining a competitive edge in both games and real-world decision-making!", - slidesUrl: "https://canva.link/gu84339jr1878xm", - panelistInfoUrl: "", - }, - { - title: "Game Theory", - type: "workshop", - instructors: "Arjun Asudani and Alexander Chien", - description: "What would you do if your success depended on someone else’s choice? This interactive workshop introduces game theory through fast-paced games and real-life scenarios. You’ll explore why people compete, when cooperation fails, and how small incentives can completely change outcomes.", - slidesUrl: "https://www.canva.com/design/DAHDlh96pCw/4F5lGBuqGXyBBO1a2Lyutg/edit", - panelistInfoUrl: "", - }, - { - title: "Careers in Tech Panel", - type: "panel", - instructors: "Meet Modi / Tammy Sujaritchai and Sofia Matos", - description: "Curious about what a career in tech actually looks like? Join us for a discussion with students and professionals as they share their experiences, career paths, and advice for breaking into the industry. Learn about different roles in tech, what skills matter most, and how to start exploring your own path.", - slidesUrl: "https://drive.google.com/drive/folders/1ZBMXid-LFwTB84lA5etUmEHMExT9Ee9W", - panelistInfoUrl: "https://docs.google.com/document/d/1Gaj_cB8Y7RgeDnxmjOmZGS6XhREWHHRYwg-b7EyUBiw/edit?usp=sharing", - }, - { - title: "CS/Tech in College Panel", - type: "panel", - instructors: "Meet Modi / Tammy Sujaritchai and Amy Lloyd", - description: "What is it really like to study computer science or tech in college? In this panel, students will talk about classes, projects, internships, clubs, and how they found their place in tech. Come ask questions, hear honest advice, and get a better sense of what the college experience can look like.", - slidesUrl: "https://docs.google.com/presentation/d/1XCDfJbdINGh4_OCPuUYX9roVqXul3N04L3zM6cUqm5M/edit?usp=sharing", - panelistInfoUrl: "https://docs.google.com/document/d/1Fkc3Ot_d7xpQTU8_oOfYIu_ugJzcenvjnlOOvWkVBBM/edit?usp=sharing", - }, -]; \ No newline at end of file diff --git a/src/constants/HomePageSections.js b/src/constants/HomePageSections.js deleted file mode 100644 index b00b96e..0000000 --- a/src/constants/HomePageSections.js +++ /dev/null @@ -1,22 +0,0 @@ -const HomePageSections = { - ABOUT: { - name: "about", - }, - TEAM: { - name: "our_team", - }, - GET_INVOLVED: { - name: "get-involved", - }, - SPEAKERS: { - name: "speakers", - }, - LEADERSHIP: { - name: "leadership", - }, - SPONSORS: { - name: "sponsors", - }, -}; - -export default HomePageSections; diff --git a/src/constants/community-partners.js b/src/constants/community-partners.js deleted file mode 100644 index ecb03c5..0000000 --- a/src/constants/community-partners.js +++ /dev/null @@ -1,45 +0,0 @@ -import aceMentor from "static/community-partners/ace-mentor.png"; -import csta from "static/community-partners/csta.png"; -import diyGirls from "static/community-partners/diy-girls.png"; -import girlsWhoCode from "static/community-partners/girls-who-code.png"; -import technovation from "static/community-partners/technovation.png"; -import tieIns from "static/community-partners/tie-ins.jpg"; -import txt from "static/community-partners/txt.png"; - -export const CommunityPartners = [ - { - name: "ace-mentor", - src: aceMentor, - website: "https://www.acementor.org/", - }, - { - name: "csta", - src: csta, - website: "https://www.csteachers.org/", - }, - { - name: "diy-girls", - src: diyGirls, - website: "https://www.diygirls.org/", - }, - { - name: "girls-who-code", - src: girlsWhoCode, - website: "https://girlswhocode.com/", - }, - { - name: "technovation", - src: technovation, - website: "https://www.technovation.org/", - }, - { - name: "tie-ins", - src: tieIns, - website: "https://tie-ins.gseis.ucla.edu/", - }, - { - name: "txt", - src: txt, - website: "https://exploringtech.org/home", - }, -]; diff --git a/src/constants/content.js b/src/constants/content.js deleted file mode 100644 index 3240b5f..0000000 --- a/src/constants/content.js +++ /dev/null @@ -1,53 +0,0 @@ -import images from "./optimizedImages"; - -export const Content = [ - { - key: 1, - name: "Eli Drewry", - title: "Content Director", - image: images["team/leadership/2027/eli-drewry.jpg"].src, - link: "", - }, - { - key: 2, - name: "Matthew Chen", - title: "Content Director", - image: images["team/leadership/2027/matthew-chen.jpg"].src, - link: "", - }, - { - key: 3, - name: "Ridhima Seth", - title: "Content Member", - image: images["team/content/ridhima-seth.png"].src, - link: "", - }, - { - key: 4, - name: "Sofia Matos", - title: "Content Member", - image: images["team/content/sofia-matos.jpg"].src, - link: "", - }, - { - key: 5, - name: "Paul Macapinlac", - title: "Content Member", - image: images["team/content/paul-headshot.png"].src, - link: "", - }, - { - key: 6, - name: "Joy Szeto", - title: "Content Member", - image: images["team/operations/joy-szeto-headshot.jpeg"].src, - link: "", - }, - { - key: 7, - name: "Benjamin Garcia", - title: "Content Member", - image: images["team/content/benjamin-garcia.jpg"].src, - link: "", - }, -]; diff --git a/src/constants/design.js b/src/constants/design.js deleted file mode 100644 index f1c792e..0000000 --- a/src/constants/design.js +++ /dev/null @@ -1,37 +0,0 @@ -import images from "./optimizedImages"; - -export const Design = [ - { - key: 1, - name: "Isabelle Chang", - title: "Design Director", - image: images["team/design/isabelle-chang.png"].src, - link: "", - }, - { - key: 2, - name: "Sarah Zhao", - title: "Design Director", - image: images["team/leadership/2027/sarah-zhao.jpg"].src, - link: "", - }, - { - key: 3, - name: "Allison Gao", - title: "Design Member", - link: "", - }, - { - key: 4, - name: "Izabella Chan", - title: "Design Member", - link: "", - }, - { - key: 5, - name: "Ela Defne Erkan", - title: "Design Member", - image: images["team/design/ela_defne_erkan.png"].src, - link: "", - }, -]; diff --git a/src/constants/external.js b/src/constants/external.js deleted file mode 100644 index bd223a9..0000000 --- a/src/constants/external.js +++ /dev/null @@ -1,39 +0,0 @@ -import images from "./optimizedImages"; - -export const External = [ - { - key: 1, - name: "Sadie Scott", - title: "External Director", - image: images["team/leadership/2027/sadie-scott.jpg"].src, - link: "", - }, - { - key: 2, - name: "Jubilee Yul", - title: "External Director", - image: images["team/leadership/2027/jubilee-yul.jpg"].src, - link: "", - }, - { - key: 3, - name: "Shayla Kumaresan", - title: "External Member", - image: images["team/external/shayla-kumaresan-headshot.jpg"].src, - link: "", - }, - { - key: 4, - name: "Luisa Chen", - title: "External Member", - image: images["team/operations/luisa-chen.png"].src, - link: "", - }, - { - key: 5, - name: "Kayla Wai", - title: "External Member", - image: images["team/operations/kayla-wai.jpg"].src, - link: "", - }, -]; diff --git a/src/constants/faq.js b/src/constants/faq.js deleted file mode 100644 index f99bfb4..0000000 --- a/src/constants/faq.js +++ /dev/null @@ -1,68 +0,0 @@ -export const FAQS = [ - { - key: 1, - question: "I'm still interested in attending but registration is closed! Can I still attend?", - answer: "Yes you can! Just fill out all of the waivers under the \"Waivers\" section below and email them to us at exploretechla@cs.ucla.edu ASAP so we can get you registered!", - note: "", - }, { - key: 2, - question: "Is there a registration fee? Are there any associated costs with the program (i.e: need to pay for an account on the virtual platform?)", - answer: "This event is completely free for all attendees! All you will need is a computer and stable internet to access the event.", - note: "", - }, { - key: 3, - question: "What prior background is needed?", - answer: "No experience needed! This event is meant to help you explore the different fields of CS and tech!", - note: "", - }, { - key: 4, - question: "When does the event start/end?", - answer: "The Opening Ceremony will begin at 9AM and the Closing Ceremony will begin at 1:00PM. Refer to the program for more schedule info!", - note: "", - }, { - key: 5, - question: "How does the raffle work?", - answer: "Workshops will give 1 ticket to students if they ask a good question or complete some activity. Panels will also give 1 ticket to all students who attend. Hold onto your ticket until the Closing Ceremony, where a raffle will be held and prizes will be given!", - note: "", - }, { - key: 6, - question: "Where is bus pick-up?", - answer: "Bus pick-up will be at the same place as the drop-off area (across Intramural Field) as detailed in the logistics packet sent to your school.", - note: "", - }, { - key: 7, - question: "How many high school students and how many high schools are participating in our events today?", - answer: "There are 8 schools with a total of 371 students! Woo!", - note: "", - }, { - key: 8, - question: "How does the mentorship session + lunch work?", - answer: "Each student’s wristband has a number on it indicating which mentor group they are in. Mentors will hold up a sign with their group number on it and students will find their mentors accordingly. Mentor groups will take turns getting lunch and they can use any space in the AGB (around the chairs/on the floor/etc) to gather around and start the mentorship session.", - note: "Group 3 students will start finding their mentors 15 minutes prior to the end of Session 3 and will get their lunch first.", - }, { - key: 9, - question: "Can students go to any workshop they want to?", - answer: "Yes! However, some workshops are limited by room size/workshop material capacity so they will be closed once capacity is reached on a first-come, first-served basis, and students will be directed to other workshops instead.", - note: "", - }, { - key: 10, - question: "Are there restrooms in the venue?", - answer: "Restrooms are on the third floor of Ackerman Union.", - note: "", - }, { - key: 12, - question: "Who is organizing this event?", - answer: "This event is being organized by exploretech.la, a group of UCLA undergraduate students! Learn more about us on our website!", - note: "", - }, { - key: 13, - question: "What are the raffle prizes?", - answer: "3 big prizes (including airpods!) are being given out during the Closing Ceremony, while additional digital prizes will be awarded to students who fill out the Feedback Survey.", - note: "", - }, { - key: 14, - question: "How many students are at UCLA?", - answer: "There are over 40,000 students at UCLA", - note: "", - }, -] \ No newline at end of file diff --git a/src/constants/leadership.js b/src/constants/leadership.js deleted file mode 100644 index 58ba8f1..0000000 --- a/src/constants/leadership.js +++ /dev/null @@ -1,102 +0,0 @@ -import images from "./optimizedImages"; - -export const Leadership = [ - { - key: 1, - name: "Spoorthy Nalumachu", - title: "Executive Director", - image: images["team/leadership/2027/spoorthy-nalumachu.jpg"].src, - link: "https://www.linkedin.com/in/spoorthy-nalumachu", - }, - { - key: 2, - name: "Clara Zhang", - title: "Executive Director", - image: images["team/leadership/2027/clara-zhang.jpg"].src, - link: "https://www.linkedin.com/in/clara-zhang-009497197/", - }, - { - key: 3, - name: "Annika Renganathan", - title: "External Advisor", - image: images["team/leadership/annika-renganathan.jpg"].src, - link: "https://www.linkedin.com/in/annika-renganathan-b437022b9", - }, - { - key: 4, - name: "Shaina Grover", - title: "Content Advisor", - image: images["team/leadership/shaina-grover.jpg"].src, - link: "https://www.linkedin.com/in/shaina-grover", - }, - { - key: 5, - name: "Sandra Pan", - title: "Design Advisor", - image: images["team/leadership/sandra-pan.jpg"].src, - link: "https://www.linkedin.com/in/sandra-pan-b994802aa", - }, - { - key: 6, - name: "Eli Drewry", - title: "Content Director", - image: images["team/leadership/2027/eli-drewry.jpg"].src, - link: "", - }, - { - key: 7, - name: "Matthew Chen", - title: "Content Director", - image: images["team/leadership/2027/matthew-chen.jpg"].src, - link: "", - }, - { - key: 8, - name: "Isabelle Chang", - title: "Design Director & UI/UX Design Lead", - image: images["team/design/isabelle-chang.png"].src, - link: "", - }, - { - key: 9, - name: "Sarah Zhao", - title: "Design Director", - image: images["team/leadership/2027/sarah-zhao.jpg"].src, - link: "", - }, - { - key: 10, - name: "Eric Chen", - title: "Operations Director", - image: images["team/leadership/2027/eric-chen.jpg"].src, - link: "", - }, - { - key: 11, - name: "Christina Uong", - title: "Operations Director", - image: images["team/leadership/2027/christina-uong.jpg"].src, - link: "", - }, - { - key: 12, - name: "Sadie Scott", - title: "External Director", - image: images["team/leadership/2027/sadie-scott.jpg"].src, - link: "", - }, - { - key: 13, - name: "Jubilee Yul", - title: "External Director", - image: images["team/leadership/2027/jubilee-yul.jpg"].src, - link: "", - }, - { - key: 14, - name: "Benjamin Garcia", - title: "Web Dev Lead", - image: images["team/content/benjamin-garcia.jpg"].src, - link: "", - }, -]; diff --git a/src/constants/marketing.js b/src/constants/marketing.js deleted file mode 100644 index d796158..0000000 --- a/src/constants/marketing.js +++ /dev/null @@ -1,11 +0,0 @@ -import images from "./optimizedImages"; - -export const Marketing = [ - { - key: 1, - name: "Luisa Chen", - title: "Marketing Member", - image: images["team/operations/luisa-chen.png"].src, - link: "", - }, -]; diff --git a/src/constants/navigation.js b/src/constants/navigation.js deleted file mode 100644 index a6de640..0000000 --- a/src/constants/navigation.js +++ /dev/null @@ -1,80 +0,0 @@ -export const homeNavItems = [ - { - key: 1, - name: "About", - href: "#about", - }, { - key: 2, - name: "Get Involved", - href: "#get-involved", - }, { - key: 3, - name: "Speakers", - href: "#speakers", - }, { - key: 4, - name: "Sponsors", - href: "#sponsors", - }, { - key: 5, - name: "Our Team", - href: "/our_team", - }, { - key: 6, - name: "Resources", - href: '/resources', - }, - { - key: 7, - name: "Ignite", - href: "/ignite" - } -] - -export const ourTeamNavItems = [ - { - key: 1, - name: "Home", - href: "/", - }, { - key: 2, - name: "Leadership", - href: "#leadership", - }, { - key: 3, - name: "Content", - href: "#content", - }, { - key: 4, - name: "Design", - href: "#design", - }, { - key: 5, - name: "External", - href: "#external", - }, { - key: 6, - name: "Operations", - href: "#operations", - }, { - key: 7, - name: "Resources", - href: "/resources", - } -] - -export const resourcesNavItems = [ - { - key: 1, - name: "Home", - href: "/", - }, { - key: 2, - name: "Our Team", - href: "/our_team", - }, { - key: 3, - name: "Resources", - href: "/resources", - }, -] \ No newline at end of file diff --git a/src/constants/operations.js b/src/constants/operations.js deleted file mode 100644 index b532a12..0000000 --- a/src/constants/operations.js +++ /dev/null @@ -1,39 +0,0 @@ -import images from "./optimizedImages"; - -export const Operations = [ - { - key: 1, - name: "Eric Chen", - title: "Operations Director", - image: images["team/leadership/2027/eric-chen.jpg"].src, - link: "", - }, - { - key: 2, - name: "Christina Uong", - title: "Operations Director", - image: images["team/leadership/2027/christina-uong.jpg"].src, - link: "", - }, - { - key: 3, - name: "Angeleena Poothavelil", - title: "Operations Member", - image: images["team/operations/angeleena-poothavelil.png"].src, - link: "", - }, - { - key: 4, - name: "Aansh Singh", - title: "Operations Member", - image: images["team/operations/aansh-singh.jpeg"].src, - link: "", - }, - { - key: 5, - name: "Esther Yao", - title: "Operations Member", - image: images["team/operations/esther-yao-headshot.jpg"].src, - link: "", - }, -]; diff --git a/src/constants/optimizedImages.js b/src/constants/optimizedImages.ts similarity index 55% rename from src/constants/optimizedImages.js rename to src/constants/optimizedImages.ts index 9973ceb..e3ef45d 100644 --- a/src/constants/optimizedImages.js +++ b/src/constants/optimizedImages.ts @@ -3,97 +3,97 @@ // Optimized WebP variants keyed by the ORIGINAL src/static-relative path. // Each value spreads straight onto a native : // -// import images from "constants/optimizedImages"; +// import images from "../constants/optimizedImages"; // Sandra Pan // // Regenerate with `npm run images` after editing scripts/image-sources.json. -import img_images_banner_1280w from "static/optimized/images/banner-1280w.webp"; -import img_images_banner_1920w from "static/optimized/images/banner-1920w.webp"; -import img_images_banner_640w from "static/optimized/images/banner-640w.webp"; -import img_images_demo_microsoft_vr_1280w from "static/optimized/images/demo-microsoft-vr-1280w.webp"; -import img_images_demo_microsoft_vr_640w from "static/optimized/images/demo-microsoft-vr-640w.webp"; -import img_images_explore_tech_2022_1280w from "static/optimized/images/explore-tech-2022-1280w.webp"; -import img_images_explore_tech_2022_640w from "static/optimized/images/explore-tech-2022-640w.webp"; -import img_images_ignite_ML_1052w from "static/optimized/images/ignite/ML-1052w.webp"; -import img_images_ignite_ML_640w from "static/optimized/images/ignite/ML-640w.webp"; -import img_images_ignite_PXL_20250215_212102361_MP_1280w from "static/optimized/images/ignite/PXL_20250215_212102361.MP-1280w.webp"; -import img_images_ignite_PXL_20250215_212102361_MP_640w from "static/optimized/images/ignite/PXL_20250215_212102361.MP-640w.webp"; -import img_images_ignite_cllab2_1280w from "static/optimized/images/ignite/cllab2-1280w.webp"; -import img_images_ignite_cllab2_640w from "static/optimized/images/ignite/cllab2-640w.webp"; -import img_images_ignite_collab1_1280w from "static/optimized/images/ignite/collab1-1280w.webp"; -import img_images_ignite_collab1_640w from "static/optimized/images/ignite/collab1-640w.webp"; -import img_images_ignite_gamdev_1280w from "static/optimized/images/ignite/gamdev-1280w.webp"; -import img_images_ignite_gamdev_640w from "static/optimized/images/ignite/gamdev-640w.webp"; -import img_images_ignite_gamedevroblox_1280w from "static/optimized/images/ignite/gamedevroblox-1280w.webp"; -import img_images_ignite_gamedevroblox_640w from "static/optimized/images/ignite/gamedevroblox-640w.webp"; -import img_images_ignite_python_1280w from "static/optimized/images/ignite/python-1280w.webp"; -import img_images_ignite_python_640w from "static/optimized/images/ignite/python-640w.webp"; -import img_images_ignite_webdev_1280w from "static/optimized/images/ignite/webdev-1280w.webp"; -import img_images_ignite_webdev_640w from "static/optimized/images/ignite/webdev-640w.webp"; -import img_images_speaker_connie_1280w from "static/optimized/images/speaker-connie-1280w.webp"; -import img_images_speaker_connie_640w from "static/optimized/images/speaker-connie-640w.webp"; -import img_images_taboola_booth_1280w from "static/optimized/images/taboola-booth-1280w.webp"; -import img_images_taboola_booth_640w from "static/optimized/images/taboola-booth-640w.webp"; -import img_images_teals_vr_640w from "static/optimized/images/teals-vr-640w.webp"; -import img_images_teals_vr_721w from "static/optimized/images/teals-vr-721w.webp"; -import img_images_workshop_audience_1280w from "static/optimized/images/workshop-audience-1280w.webp"; -import img_images_workshop_audience_640w from "static/optimized/images/workshop-audience-640w.webp"; -import img_images_workshop_dev_tools_1280w from "static/optimized/images/workshop-dev-tools-1280w.webp"; -import img_images_workshop_dev_tools_640w from "static/optimized/images/workshop-dev-tools-640w.webp"; -import img_images_workshop_kaylie_1280w from "static/optimized/images/workshop-kaylie-1280w.webp"; -import img_images_workshop_kaylie_640w from "static/optimized/images/workshop-kaylie-640w.webp"; -import img_images_workshop_scratch_1280w from "static/optimized/images/workshop-scratch-1280w.webp"; -import img_images_workshop_scratch_640w from "static/optimized/images/workshop-scratch-640w.webp"; -import img_speakers_alfredo_ayala_320w from "static/optimized/speakers/alfredo-ayala-320w.webp"; -import img_speakers_carey_nachenberg_320w from "static/optimized/speakers/carey-nachenberg-320w.webp"; -import img_speakers_jayathi_murthy_320w from "static/optimized/speakers/jayathi-murthy-320w.webp"; -import img_speakers_justin_brezhnev_320w from "static/optimized/speakers/justin-brezhnev-320w.webp"; -import img_speakers_nick_corral_320w from "static/optimized/speakers/nick-corral-320w.webp"; -import img_speakers_ruth_johnson_320w from "static/optimized/speakers/ruth-johnson-320w.webp"; -import img_speakers_solomon_russell_200w from "static/optimized/speakers/solomon-russell-200w.webp"; -import img_speakers_tyler_menezes_320w from "static/optimized/speakers/tyler-menezes-320w.webp"; -import img_sponsors_balsamiq_192w from "static/optimized/sponsors/balsamiq-192w.webp"; -import img_sponsors_balsamiq_384w from "static/optimized/sponsors/balsamiq-384w.webp"; -import img_sponsors_cloud_girls_192w from "static/optimized/sponsors/cloud-girls-192w.webp"; -import img_sponsors_cloud_girls_384w from "static/optimized/sponsors/cloud-girls-384w.webp"; -import img_sponsors_disney_192w from "static/optimized/sponsors/disney-192w.webp"; -import img_sponsors_disney_384w from "static/optimized/sponsors/disney-384w.webp"; -import img_sponsors_google_logo_256w from "static/optimized/sponsors/google-logo-256w.webp"; -import img_sponsors_google_logo_512w from "static/optimized/sponsors/google-logo-512w.webp"; -import img_sponsors_northrop_grumman_192w from "static/optimized/sponsors/northrop-grumman-192w.webp"; -import img_sponsors_northrop_grumman_384w from "static/optimized/sponsors/northrop-grumman-384w.webp"; -import img_sponsors_ucla_cs_256w from "static/optimized/sponsors/ucla-cs-256w.webp"; -import img_sponsors_ucla_cs_512w from "static/optimized/sponsors/ucla-cs-512w.webp"; -import img_sponsors_ucla_samueli_engineering_192w from "static/optimized/sponsors/ucla-samueli-engineering-192w.webp"; -import img_sponsors_ucla_samueli_engineering_384w from "static/optimized/sponsors/ucla-samueli-engineering-384w.webp"; -import img_sponsors_yubico_192w from "static/optimized/sponsors/yubico-192w.webp"; -import img_sponsors_yubico_384w from "static/optimized/sponsors/yubico-384w.webp"; -import img_team_content_benjamin_garcia_320w from "static/optimized/team/content/benjamin-garcia-320w.webp"; -import img_team_content_paul_headshot_320w from "static/optimized/team/content/paul-headshot-320w.webp"; -import img_team_content_ridhima_seth_320w from "static/optimized/team/content/ridhima-seth-320w.webp"; -import img_team_content_sofia_matos_320w from "static/optimized/team/content/sofia-matos-320w.webp"; -import img_team_design_ela_defne_erkan_320w from "static/optimized/team/design/ela_defne_erkan-320w.webp"; -import img_team_design_isabelle_chang_320w from "static/optimized/team/design/isabelle-chang-320w.webp"; -import img_team_external_shayla_kumaresan_headshot_320w from "static/optimized/team/external/shayla-kumaresan-headshot-320w.webp"; -import img_team_leadership_2027_christina_uong_320w from "static/optimized/team/leadership/2027/christina-uong-320w.webp"; -import img_team_leadership_2027_clara_zhang_320w from "static/optimized/team/leadership/2027/clara-zhang-320w.webp"; -import img_team_leadership_2027_eli_drewry_320w from "static/optimized/team/leadership/2027/eli-drewry-320w.webp"; -import img_team_leadership_2027_eric_chen_320w from "static/optimized/team/leadership/2027/eric-chen-320w.webp"; -import img_team_leadership_2027_jubilee_yul_320w from "static/optimized/team/leadership/2027/jubilee-yul-320w.webp"; -import img_team_leadership_2027_matthew_chen_320w from "static/optimized/team/leadership/2027/matthew-chen-320w.webp"; -import img_team_leadership_2027_sadie_scott_320w from "static/optimized/team/leadership/2027/sadie-scott-320w.webp"; -import img_team_leadership_2027_sarah_zhao_320w from "static/optimized/team/leadership/2027/sarah-zhao-320w.webp"; -import img_team_leadership_2027_spoorthy_nalumachu_320w from "static/optimized/team/leadership/2027/spoorthy-nalumachu-320w.webp"; -import img_team_leadership_annika_renganathan_320w from "static/optimized/team/leadership/annika-renganathan-320w.webp"; -import img_team_leadership_sandra_pan_320w from "static/optimized/team/leadership/sandra-pan-320w.webp"; -import img_team_leadership_shaina_grover_320w from "static/optimized/team/leadership/shaina-grover-320w.webp"; -import img_team_operations_aansh_singh_320w from "static/optimized/team/operations/aansh-singh-320w.webp"; -import img_team_operations_angeleena_poothavelil_320w from "static/optimized/team/operations/angeleena-poothavelil-320w.webp"; -import img_team_operations_esther_yao_headshot_320w from "static/optimized/team/operations/esther-yao-headshot-320w.webp"; -import img_team_operations_joy_szeto_headshot_320w from "static/optimized/team/operations/joy-szeto-headshot-320w.webp"; -import img_team_operations_kayla_wai_320w from "static/optimized/team/operations/kayla-wai-320w.webp"; -import img_team_operations_luisa_chen_320w from "static/optimized/team/operations/luisa-chen-320w.webp"; +import img_images_banner_1280w from "../static/optimized/images/banner-1280w.webp"; +import img_images_banner_1920w from "../static/optimized/images/banner-1920w.webp"; +import img_images_banner_640w from "../static/optimized/images/banner-640w.webp"; +import img_images_demo_microsoft_vr_1280w from "../static/optimized/images/demo-microsoft-vr-1280w.webp"; +import img_images_demo_microsoft_vr_640w from "../static/optimized/images/demo-microsoft-vr-640w.webp"; +import img_images_explore_tech_2022_1280w from "../static/optimized/images/explore-tech-2022-1280w.webp"; +import img_images_explore_tech_2022_640w from "../static/optimized/images/explore-tech-2022-640w.webp"; +import img_images_ignite_ML_1052w from "../static/optimized/images/ignite/ML-1052w.webp"; +import img_images_ignite_ML_640w from "../static/optimized/images/ignite/ML-640w.webp"; +import img_images_ignite_PXL_20250215_212102361_MP_1280w from "../static/optimized/images/ignite/PXL_20250215_212102361.MP-1280w.webp"; +import img_images_ignite_PXL_20250215_212102361_MP_640w from "../static/optimized/images/ignite/PXL_20250215_212102361.MP-640w.webp"; +import img_images_ignite_cllab2_1280w from "../static/optimized/images/ignite/cllab2-1280w.webp"; +import img_images_ignite_cllab2_640w from "../static/optimized/images/ignite/cllab2-640w.webp"; +import img_images_ignite_collab1_1280w from "../static/optimized/images/ignite/collab1-1280w.webp"; +import img_images_ignite_collab1_640w from "../static/optimized/images/ignite/collab1-640w.webp"; +import img_images_ignite_gamdev_1280w from "../static/optimized/images/ignite/gamdev-1280w.webp"; +import img_images_ignite_gamdev_640w from "../static/optimized/images/ignite/gamdev-640w.webp"; +import img_images_ignite_gamedevroblox_1280w from "../static/optimized/images/ignite/gamedevroblox-1280w.webp"; +import img_images_ignite_gamedevroblox_640w from "../static/optimized/images/ignite/gamedevroblox-640w.webp"; +import img_images_ignite_python_1280w from "../static/optimized/images/ignite/python-1280w.webp"; +import img_images_ignite_python_640w from "../static/optimized/images/ignite/python-640w.webp"; +import img_images_ignite_webdev_1280w from "../static/optimized/images/ignite/webdev-1280w.webp"; +import img_images_ignite_webdev_640w from "../static/optimized/images/ignite/webdev-640w.webp"; +import img_images_speaker_connie_1280w from "../static/optimized/images/speaker-connie-1280w.webp"; +import img_images_speaker_connie_640w from "../static/optimized/images/speaker-connie-640w.webp"; +import img_images_taboola_booth_1280w from "../static/optimized/images/taboola-booth-1280w.webp"; +import img_images_taboola_booth_640w from "../static/optimized/images/taboola-booth-640w.webp"; +import img_images_teals_vr_640w from "../static/optimized/images/teals-vr-640w.webp"; +import img_images_teals_vr_721w from "../static/optimized/images/teals-vr-721w.webp"; +import img_images_workshop_audience_1280w from "../static/optimized/images/workshop-audience-1280w.webp"; +import img_images_workshop_audience_640w from "../static/optimized/images/workshop-audience-640w.webp"; +import img_images_workshop_dev_tools_1280w from "../static/optimized/images/workshop-dev-tools-1280w.webp"; +import img_images_workshop_dev_tools_640w from "../static/optimized/images/workshop-dev-tools-640w.webp"; +import img_images_workshop_kaylie_1280w from "../static/optimized/images/workshop-kaylie-1280w.webp"; +import img_images_workshop_kaylie_640w from "../static/optimized/images/workshop-kaylie-640w.webp"; +import img_images_workshop_scratch_1280w from "../static/optimized/images/workshop-scratch-1280w.webp"; +import img_images_workshop_scratch_640w from "../static/optimized/images/workshop-scratch-640w.webp"; +import img_speakers_alfredo_ayala_320w from "../static/optimized/speakers/alfredo-ayala-320w.webp"; +import img_speakers_carey_nachenberg_320w from "../static/optimized/speakers/carey-nachenberg-320w.webp"; +import img_speakers_jayathi_murthy_320w from "../static/optimized/speakers/jayathi-murthy-320w.webp"; +import img_speakers_justin_brezhnev_320w from "../static/optimized/speakers/justin-brezhnev-320w.webp"; +import img_speakers_nick_corral_320w from "../static/optimized/speakers/nick-corral-320w.webp"; +import img_speakers_ruth_johnson_320w from "../static/optimized/speakers/ruth-johnson-320w.webp"; +import img_speakers_solomon_russell_200w from "../static/optimized/speakers/solomon-russell-200w.webp"; +import img_speakers_tyler_menezes_320w from "../static/optimized/speakers/tyler-menezes-320w.webp"; +import img_sponsors_balsamiq_192w from "../static/optimized/sponsors/balsamiq-192w.webp"; +import img_sponsors_balsamiq_384w from "../static/optimized/sponsors/balsamiq-384w.webp"; +import img_sponsors_cloud_girls_192w from "../static/optimized/sponsors/cloud-girls-192w.webp"; +import img_sponsors_cloud_girls_384w from "../static/optimized/sponsors/cloud-girls-384w.webp"; +import img_sponsors_disney_192w from "../static/optimized/sponsors/disney-192w.webp"; +import img_sponsors_disney_384w from "../static/optimized/sponsors/disney-384w.webp"; +import img_sponsors_google_logo_256w from "../static/optimized/sponsors/google-logo-256w.webp"; +import img_sponsors_google_logo_512w from "../static/optimized/sponsors/google-logo-512w.webp"; +import img_sponsors_northrop_grumman_192w from "../static/optimized/sponsors/northrop-grumman-192w.webp"; +import img_sponsors_northrop_grumman_384w from "../static/optimized/sponsors/northrop-grumman-384w.webp"; +import img_sponsors_ucla_cs_256w from "../static/optimized/sponsors/ucla-cs-256w.webp"; +import img_sponsors_ucla_cs_512w from "../static/optimized/sponsors/ucla-cs-512w.webp"; +import img_sponsors_ucla_samueli_engineering_192w from "../static/optimized/sponsors/ucla-samueli-engineering-192w.webp"; +import img_sponsors_ucla_samueli_engineering_384w from "../static/optimized/sponsors/ucla-samueli-engineering-384w.webp"; +import img_sponsors_yubico_192w from "../static/optimized/sponsors/yubico-192w.webp"; +import img_sponsors_yubico_384w from "../static/optimized/sponsors/yubico-384w.webp"; +import img_team_content_benjamin_garcia_320w from "../static/optimized/team/content/benjamin-garcia-320w.webp"; +import img_team_content_paul_headshot_320w from "../static/optimized/team/content/paul-headshot-320w.webp"; +import img_team_content_ridhima_seth_320w from "../static/optimized/team/content/ridhima-seth-320w.webp"; +import img_team_content_sofia_matos_320w from "../static/optimized/team/content/sofia-matos-320w.webp"; +import img_team_design_ela_defne_erkan_320w from "../static/optimized/team/design/ela_defne_erkan-320w.webp"; +import img_team_design_isabelle_chang_320w from "../static/optimized/team/design/isabelle-chang-320w.webp"; +import img_team_external_shayla_kumaresan_headshot_320w from "../static/optimized/team/external/shayla-kumaresan-headshot-320w.webp"; +import img_team_leadership_2027_christina_uong_320w from "../static/optimized/team/leadership/2027/christina-uong-320w.webp"; +import img_team_leadership_2027_clara_zhang_320w from "../static/optimized/team/leadership/2027/clara-zhang-320w.webp"; +import img_team_leadership_2027_eli_drewry_320w from "../static/optimized/team/leadership/2027/eli-drewry-320w.webp"; +import img_team_leadership_2027_eric_chen_320w from "../static/optimized/team/leadership/2027/eric-chen-320w.webp"; +import img_team_leadership_2027_jubilee_yul_320w from "../static/optimized/team/leadership/2027/jubilee-yul-320w.webp"; +import img_team_leadership_2027_matthew_chen_320w from "../static/optimized/team/leadership/2027/matthew-chen-320w.webp"; +import img_team_leadership_2027_sadie_scott_320w from "../static/optimized/team/leadership/2027/sadie-scott-320w.webp"; +import img_team_leadership_2027_sarah_zhao_320w from "../static/optimized/team/leadership/2027/sarah-zhao-320w.webp"; +import img_team_leadership_2027_spoorthy_nalumachu_320w from "../static/optimized/team/leadership/2027/spoorthy-nalumachu-320w.webp"; +import img_team_leadership_annika_renganathan_320w from "../static/optimized/team/leadership/annika-renganathan-320w.webp"; +import img_team_leadership_sandra_pan_320w from "../static/optimized/team/leadership/sandra-pan-320w.webp"; +import img_team_leadership_shaina_grover_320w from "../static/optimized/team/leadership/shaina-grover-320w.webp"; +import img_team_operations_aansh_singh_320w from "../static/optimized/team/operations/aansh-singh-320w.webp"; +import img_team_operations_angeleena_poothavelil_320w from "../static/optimized/team/operations/angeleena-poothavelil-320w.webp"; +import img_team_operations_esther_yao_headshot_320w from "../static/optimized/team/operations/esther-yao-headshot-320w.webp"; +import img_team_operations_joy_szeto_headshot_320w from "../static/optimized/team/operations/joy-szeto-headshot-320w.webp"; +import img_team_operations_kayla_wai_320w from "../static/optimized/team/operations/kayla-wai-320w.webp"; +import img_team_operations_luisa_chen_320w from "../static/optimized/team/operations/luisa-chen-320w.webp"; const images = { "images/banner.png": { diff --git a/src/constants/panelists.js b/src/constants/panelists.js deleted file mode 100644 index 7edfec5..0000000 --- a/src/constants/panelists.js +++ /dev/null @@ -1,39 +0,0 @@ -export const PANELS = [ - { - key: 1, - name: "Diversity 101", - panelists: [ - { - name: "Danielle Jaffe", - image: "", - description: "Daniel (he/him) is a 3rd-year transfer student at UCLA studying Computer Science after an extensive career in the television industry, including roles at Hell’s Kitchen and The Bachelorette! Ask him about working at NASA, Viasat, or transitioning into CS from another field.", - }, { - name: "Evan Zhong", - image: "", - description: "Evan (he/him) is a 3rd-year Computer Science student at UCLA. Ask him what Computer Vision is, how to get involved with CS research, and landing an internship at Facebook/Meta!", - }, { - name: "Colleen Li", - image: "", - description: "Colleen (she/her) is a 3rd-year Cognitive Science student at UCLA with a specialization in coding. Ask her what Product Design is, how she landed her summer internships at Slack and Salesforce, and her experience as a woman in STEM!", - } - ] - }, { - key: 2, - name: "Careers in CS", - panelists: [ - { - name: "Daniel Jaffe", - image: "", - description: "Daniel (he/him) is a 3rd-year transfer student at UCLA studying Computer Science after an extensive career in the television industry, including roles at Hell’s Kitchen and The Bachelorette! Ask him about working at NASA, Viasat, or transitioning into CS from another field.", - }, { - name: "Evan Zhong", - image: "", - description: "Evan (he/him) is a 3rd-year Computer Science student at UCLA. Ask him what Computer Vision is, how to get involved with CS research, and landing an internship at Facebook/Meta!", - }, { - name: "Colleen Li", - image: "", - description: "Colleen (she/her) is a 3rd-year Cognitive Science student at UCLA with a specialization in coding. Ask her what Product Design is, how she landed her summer internships at Slack and Salesforce, and her experience as a woman in STEM!", - } - ] - } -] \ No newline at end of file diff --git a/src/constants/schedule.js b/src/constants/schedule.js deleted file mode 100644 index 76d8666..0000000 --- a/src/constants/schedule.js +++ /dev/null @@ -1,27 +0,0 @@ -export const SCHEDULE = [ - { - key: 1, - name: "Opening Ceremony", - time: "9:00AM - 9:30AM", - }, { - key: 2, - name: "Session 1", - time: "9:30AM - 10:15AM", - }, { - key: 3, - name: "Session 2", - time: "10:30AM - 11:15AM", - }, { - key: 4, - name: "Session 3", - time: "11:30AM - 12:15PM", - }, { - key: 5, - name: "Lunch with a Mentor", - time: "12:15PM - 1:00PM", - }, { - key: 6, - name: "Closing Ceremony", - time: "1:00PM - 1:30PM", - }, -] \ No newline at end of file diff --git a/src/constants/speakers.js b/src/constants/speakers.js deleted file mode 100644 index 7640387..0000000 --- a/src/constants/speakers.js +++ /dev/null @@ -1,54 +0,0 @@ -import images from "./optimizedImages"; - -export const CurrentSpeakers = []; - -export const PastSpeakers = [ - { - name: "Alfredo Ayala", - title: "Walt Disney Executive R&D Imagineer at Walt Disney Imagineering", - image: images["speakers/alfredo-ayala.jpg"].src, - link: "https://www.linkedin.com/in/alfredo-ayala-b0094426/", - }, - { - name: "Carey Nachenberg", - title: "Adjunct Professor at UCLA, Principal SWE Lyft Level 5", - image: images["speakers/carey-nachenberg.png"].src, - link: "https://www.linkedin.com/in/carey-nachenberg-14bbb03/", - }, - { - name: "Jayathi Murthy", - title: "Dean, UCLA Samueli School of Engineering", - image: images["speakers/jayathi-murthy.png"].src, - link: "https://samueli.ucla.edu/jayathi-y-murthy/", - }, - { - name: "Justin Brezhnev", - title: "President & CEO, Hacker Fund", - image: images["speakers/justin-brezhnev.png"].src, - link: "https://www.linkedin.com/in/brezh/", - }, - { - name: "Tyler Menezes", - title: "Executive Director, CodeDay", - image: images["speakers/tyler-menezes.png"].src, - link: "https://www.linkedin.com/in/tylermenezes/", - }, - { - name: "Solomon Russell", - title: "Assistant Professor, El Camino College", - image: images["speakers/solomon-russell.jpeg"].src, - link: "https://www.linkedin.com/in/solomon-russell-220aa014/", - }, - { - name: "Ruth Johnson", - title: "Computer Science PhD Student, UCLA", - image: images["speakers/ruth-johnson.jpeg"].src, - link: "https://www.linkedin.com/in/ruthjohnson13/", - }, - { - name: "Nick Corral", - title: "Techincal Program Manager, Oculus", - image: images["speakers/nick-corral.jpg"].src, - link: "https://www.linkedin.com/in/nick-corral-595723106/", - }, -]; diff --git a/src/constants/webdev.js b/src/constants/webdev.js deleted file mode 100644 index 3833d3b..0000000 --- a/src/constants/webdev.js +++ /dev/null @@ -1,59 +0,0 @@ -import images from "./optimizedImages"; - -export const WebDev = [ - { - key: 1, - name: "Benjamin Garcia", - title: "Web Dev Lead", - image: images["team/content/benjamin-garcia.jpg"].src, - link: "", - }, - { - key: 2, - name: "Shayla Kumaresan", - title: "Web Dev Member", - image: images["team/external/shayla-kumaresan-headshot.jpg"].src, - link: "", - }, - { - key: 3, - name: "Aansh Singh", - title: "Web Dev Member", - image: images["team/operations/aansh-singh.jpeg"].src, - link: "", - }, - { - key: 4, - name: "Paul Macapinlac", - title: "Web Dev Member", - image: images["team/content/paul-headshot.png"].src, - link: "", - }, - { - key: 5, - name: "Angeleena Poothavelil", - title: "Web Dev Member", - image: images["team/operations/angeleena-poothavelil.png"].src, - link: "", - }, - { - key: 6, - name: "Isabelle Chang", - title: "UI/UX Design Lead", - image: images["team/design/isabelle-chang.png"].src, - link: "", - }, - { - key: 7, - name: "Ela Defne Erkan", - title: "UI/UX Designer", - image: images["team/design/ela_defne_erkan.png"].src, - link: "", - }, - { - key: 8, - name: "Allison Gao", - title: "UI/UX Designer", - link: "", - }, -]; diff --git a/src/constants/workshops.js b/src/constants/workshops.js deleted file mode 100644 index f0654d9..0000000 --- a/src/constants/workshops.js +++ /dev/null @@ -1,408 +0,0 @@ -import coding101Logo from "static/workshop-logos/coding-101.png"; -import bioinformaticsLogo from "static/workshop-logos/bioinformatics.png"; -import uiUxLogo from "static/workshop-logos/ui-ux.png"; -import iotLogo from "static/workshop-logos/iot.png"; -import nftBlockchainLogo from "static/workshop-logos/nft-blockchain.png"; -import pearDeckLogo from "static/workshop-logos/pear-deck.png"; -import computerVisionLogo from "static/workshop-logos/computer-vision.png"; -import webDevLogo from "static/workshop-logos/web-dev.png"; -import aiMlLogo from "static/workshop-logos/ai-ml.png"; -import diversity101Logo from "static/workshop-logos/diversity-101.png"; -import passwordSecurityLogo from "static/workshop-logos/password-security.png"; -import cryptographyLogo from "static/workshop-logos/cryptography.png"; -import arVrLogo from "static/workshop-logos/ar-vr.png"; -import gameDevLogo from "static/workshop-logos/game-dev.png"; -import pythonTurtlesLogo from "static/workshop-logos/python-turtles.png"; -import nlpLogo from "static/workshop-logos/nlp.png"; -import careersInCsLogo from "static/workshop-logos/careers-in-cs.png"; -import yoshinoGotoPhoto from "static/panelists/yoshino-goto.png"; -import christineKimPhoto from "static/panelists/christine-kim.png"; -import ingridTienPhoto from "static/panelists/ingrid-tien.png"; -import sydneySavagePhoto from "static/panelists/sydney-savage.png"; -import danielJaffePhoto from "static/panelists/daniel-jaffe.png"; -import henryTrinhPhoto from "static/panelists/henry-trinh.png"; -import colleenLiPhoto from "static/panelists/colleen-li.png"; -import evanZhongPhoto from "static/panelists/evan-zhong.png"; - -const WORKSHOPS_ALL = [ - { - key: 1, - name: "Coding 101", - logo: coding101Logo, - session: "Session 1", - location: "Room 2408", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1Ua_oBxXy5mz5YjPCWCi3zxClVDzKQOyqTaOnrj3ITe8/edit?usp=sharing", - }, - { - name: "Station 1", - link: "https://scratch.mit.edu/projects/638101091", - }, - { - name: "Station 2", - link: "https://scratch.mit.edu/projects/648458946/", - }, - { - name: "Station 3", - link: "https://scratch.mit.edu/projects/638136677/", - }, - { - name: "Station 4", - link: "https://scratch.mit.edu/projects/648389443/", - }, - { - name: "Station 5 (bonus)", - link: "https://scratch.mit.edu/projects/650445764/", - }, - ], - }, - { - key: 2, - name: "Are You Related to a Strawberry?", - logo: bioinformaticsLogo, - session: "Session 1", - location: "Room 2412", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/167XxMzyj2Zlx3UCcJCajfznXUzEYzhp_mRFxMZ1nq2U/edit?usp=sharing", - }, - { - name: "Strawberry DNA Activity", - link: "https://www.genome.gov/Pages/Education/Modules/StrawberryExtractionInstructions.pdf", - }, - { - name: "Bioinformatics Video", - link: "https://www.youtube.com/watch?v=W-Ov2cUaYQY", - }, - { - name: "More About Next Generation Sequencing", - link: "https://www.illumina.com/science/technology/next-generation-sequencing/beginners.html", - }, - ], - }, - { - key: 3, - name: "UI/UX", - logo: uiUxLogo, - session: "Session 1", - location: "Room 3508", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1xvJgQ2KqWKiCEmYr3Ea0IMGcjpHNSTecKWG_VTcq-mM/edit?usp=sharing", - }, - ], - }, - { - key: 4, - name: "Internet of Things", - logo: iotLogo, - session: "Session 1", - location: "Room 3517", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1sYEp47_nqLz-8BqVgSwz1DlkoS_uwpBgfIIl4FALN04/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1nWZPfzaNvEq43ZflmGaHD_suWqYXZCwdcN1rzxLRo6o/edit?usp=sharing", - }, - ], - }, - { - key: 5, - name: "Inside Blockchain", - logo: nftBlockchainLogo, - session: "Session 1", - location: "Room BRR", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1i0ASCMTT1k2pRxd-2ilYqi3E1587Rt5DpHaJcPKU57A/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1Lu7-zxKIxXzutHIE-JI5yiiEO2qU9qlPdDbmw6VLfcM/edit?usp=sharing", - }, - ], - }, - { - key: 6, - name: "Pear Deck", - logo: pearDeckLogo, - session: "Sessions 1 & 2", - location: "Room 3508", - resources: [ - { - name: "Website", - link: "https://www.peardeck.com/?utm_source=google&utm_medium=cpc&utm_campaign=15840444475&utm_content=135652187681&utm_term=Pear%20Deck&aid=573893289485&dev=c&gclid=Cj0KCQjw_4-SBhCgARIsAAlegrVjNPmOjo_iVHT0pINs2J2dObfE1DToD7o1E0uf3-TY1LW5lYJsJtcaAhaQEALw_wcB", - }, - ], - }, - { - key: 7, - name: "Teaching a Computer to See", - logo: computerVisionLogo, - session: "Session 2", - location: "Room 2412", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1zSx-i_plBCiCC9t8xiD8fm-n2GTK9Zo_widxSmL0ufI/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1w_IW6smjdKwp3nDFL97-UWee2Lib5UV5Ooy5-MVd8fQ/edit?usp=sharing", - }, - ], - }, - { - key: 8, - name: "exploreWeb.la: Intro to Web Dev", - logo: webDevLogo, - session: "Session 2", - location: "Room 2408", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1OKKO_WLaWZQBqxTG6vVIomwo_JGSlF1sWWBraHu5l44/edit?usp=sharing", - }, - { - name: "Codepen", - link: "https://codepen.io/pen?template=MWOjyeb", - }, - ], - }, - { - key: 9, - name: "Artificial Intelligence and Machine Learning", - logo: aiMlLogo, - session: "Session 2", - location: "Room 3508", - resources: [ - { - name: "Slides", - link: "https://www.hacksplaining.com/exercises/sql-injection#/start", - }, - { - name: "Neural Networks Video", - link: "https://www.youtube.com/watch?v=oV3ZY6tJiA0&t=324s", - }, - { - name: "Learn more about machine learning", - link: "https://github.com/kjaisingh/high-school-guide-to-machine-learning", - }, - { - name: "Self-driving car moral activity", - link: "https://www.moralmachine.net/", - }, - { - name: "More About the Facebook Scandal", - link: "https://www.techrepublic.com/article/facebook-data-privacy-scandal-a-cheat-sheet/", - }, - ], - }, - { - key: 10, - name: "Diversity 101 Panel", - logo: diversity101Logo, - session: "Session 2", - location: "Room 3517", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1adtKaYiW1GIzEVGnB6r7-h1rJ57uxBQ5yQmhYUMmKp0/edit?usp=sharing", - }, - { - name: "Wordcloud Activity", - link: "https://www.menti.com/4yj9zwxn6p", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1exbMLRhw2emX7HhpKa54j_nvonISsXTb-jel7tau6oU/edit?usp=sharing", - }, - ], - }, - { - key: 11, - name: "HacKeD!", - logo: passwordSecurityLogo, - session: "Session 3", - location: "Room 3517", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1a7RlA6L7_cbLU8lx3anCyUGMBD3xmKp0Zzj0Pkgckps/edit?usp=sharing", - }, - { - name: "Passwords Interactive Activity", - link: "https://passworks.uclaacm.com/", - }, - { - name: "SQL Injection", - link: "https://www.hacksplaining.com/exercises/sql-injection#/start", - }, - ], - }, - { - key: 12, - name: "Spill the Tea with Cryptography!", - logo: cryptographyLogo, - session: "Session 2", - location: "Room BRR", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1uB5g2TzUee4Ssro2Uu8h0TtPigAdXZQGRNH2S9xEOUE/edit?usp=sharing", - }, - ], - }, - { - key: 13, - name: "Augment Your Reality", - logo: arVrLogo, - session: "Session 3", - location: "Room BRR", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/12pszxNemyy2sJAgxKFg9aZl4ADPBGj_mfeuJs5vkOxM/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1gERLaBktQB6VIhcvfZjr84FGWjtPn7JeSZE7BCkI5Ug/edit?usp=sharing", - }, - ], - }, - { - key: 14, - name: "Game Dev", - logo: gameDevLogo, - session: "Session 3", - location: "Room 2412", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1zyhW6E3vVFzBytB-aFcW9nb0mZw2MsAHIMcX9lLNMH0/edit?usp=sharing", - }, - ], - }, - { - key: 15, - name: "Python Turtles", - logo: pythonTurtlesLogo, - session: "Session 3", - location: "Room 2408", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1xfCi4U7kR3-oSFo65zyp0QRPls0LuzV63QaDCvOxXKE/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1K6BEInJ1x5gBkApxqeI1j5Zh6uKeJWKROblz40wzl_M/edit?usp=sharing", - }, - ], - }, - { - key: 16, - name: "Natural Language Processing", - logo: nlpLogo, - session: "Session 3", - location: "Room 3408", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/16LflAJt277QGr_u-XAIf_9aWm4G1vYfHs7d_O5wfJpI/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1ajAsqe11G9DqtYD_u_h1KKq2by2ssFU8VpUOoTfgtsE/edit", - }, - ], - }, - { - key: 17, - name: "Careers in CS Panel", - logo: careersInCsLogo, - session: "Session 3", - location: "Room 2410", - resources: [ - { - name: "Slides", - link: "https://docs.google.com/presentation/d/1OeMMIp4exQBKwLqFuXVBj-C39M20g4W0R40me-vNcVg/edit?usp=sharing", - }, - { - name: "Resources", - link: "https://docs.google.com/document/d/1R-O5r31aqIbyPn5n8EJPRXtFOjMYr91sn6Bg5rRtCJ8/edit?usp=sharing", - }, - ], - }, -]; - -// group every workshop by two -export const WORKSHOPS = WORKSHOPS_ALL.reduce( - (accumulator, currentValue, currentIndex, array) => { - if (currentIndex % 2 === 0) - accumulator.push(array.slice(currentIndex, currentIndex + 2)); - return accumulator; - }, - [], -); - -export const PANELS = [ - { - name: "Diversity 101", - panelists: [ - { - name: "Yoshino Goto", - image: yoshinoGotoPhoto, - link: "https://www.linkedin.com/in/ygoto/", - }, - { - name: "Christine Kim", - image: christineKimPhoto, - link: "https://www.linkedin.com/in/minjeong-christine-kim/", - }, - { - name: "Ingrid Tien", - image: ingridTienPhoto, - link: "https://www.linkedin.com/in/ingrid-tien-8981631a5/", - }, - { - name: "Sydney Savage", - image: sydneySavagePhoto, - link: "https://www.linkedin.com/in/sydney-savage/", - }, - ], - }, - { - name: "Careers in CS", - panelists: [ - { - name: "Daniel Jaffe", - image: danielJaffePhoto, - link: "https://www.linkedin.com/in/dgjaffe/", - }, - { - name: "Henry Trinh", - image: henryTrinhPhoto, - link: "https://www.linkedin.com/in/thenry3/", - }, - { - name: "Colleen Li", - image: colleenLiPhoto, - link: "https://www.linkedin.com/in/colleenli2/", - }, - { - name: "Evan Zhong", - image: evanZhongPhoto, - link: "https://www.linkedin.com/in/evan-zhong/", - }, - ], - }, -]; diff --git a/src/content/events/2021.ts b/src/content/events/2021.ts new file mode 100644 index 0000000..f7f9cf1 --- /dev/null +++ b/src/content/events/2021.ts @@ -0,0 +1,177 @@ +import type { DocumentLink } from "../types"; +import type { ScheduleDay, FaqContent } from "./types"; +import { WAIVER_FORMS, WAIVER_INTRO, WAIVER_NOTE } from "./waivers"; + +/** + * exploretech.la 2021, the virtual event still served at /register. + * It is the only two-day event and the only page with the Socio platform links, + * so it keeps its own shape instead of pretending to be an EventContent. + */ +export interface RegistrationContent { + readonly title: string; + readonly alert: { + readonly heading: string; + readonly note: string; + readonly linkLabel: string; + readonly to: string; + /** Analytics label recorded when the alert link is followed. */ + readonly trackingLabel: string; + }; + readonly schedule: readonly [ScheduleDay, ScheduleDay]; + readonly importantLinks: { + readonly intro: string; + readonly platform: DocumentLink; + readonly documents: readonly DocumentLink[]; + readonly support: { + readonly heading: string; + readonly paragraphs: readonly string[]; + readonly liveHeading: string; + readonly liveBody: string; + readonly zoom: DocumentLink; + readonly hoursLabel: string; + readonly hours: string; + }; + }; + readonly faq: FaqContent; + readonly waivers: { + readonly intro: string; + readonly note: string; + readonly forms: readonly DocumentLink[]; + readonly allFormsHeading: string; + readonly allFormsBody: string; + readonly allForms: DocumentLink; + }; +} + +export const REGISTRATION_2021: RegistrationContent = { + title: "Event Information", + alert: { + heading: "Missed our event? Want to rewatch workshops and panels?", + note: "Checkout our past workshops and resources from previous exploretech.la main events on our Resources Page.", + linkLabel: "exploretech.la Recordings and Resources", + to: "/resources", + trackingLabel: "Resources", + }, + schedule: [ + { + title: "Day 1 (Saturday, April 10th)", + items: [ + { key: 1, time: "9:30 AM", name: "Virtual Platform Opens" }, + { key: 2, time: "10:00 AM", name: "Opening Ceremony" }, + { key: 3, time: "10:30 AM", name: "Session 1" }, + { key: 4, time: "11:40 AM", name: "College Buddy Chat" }, + { key: 5, time: "12:20 PM", name: "Session 2" }, + { key: 6, time: "1:20 PM", name: "Closing Briefing" }, + ], + }, + { + title: "Day 2 (Sunday, April 11th)", + items: [ + { key: 1, time: "9:30 AM", name: "Virtual Platform Opens" }, + { key: 2, time: "10:00 AM", name: "Opening Briefing" }, + { key: 3, time: "10:10 AM", name: "Session 3" }, + { key: 4, time: "11:20 AM", name: "College Buddy Chat" }, + { key: 5, time: "12:00 PM", name: "Session 4" }, + { key: 6, time: "1:00 PM", name: "Closing Ceremony" }, + ], + }, + ], + importantLinks: { + intro: "Below are some of the important links you need for the event!", + platform: { + name: "Socio Virtual Platform Access", + src: "https://tinyurl.com/exploretechla2021", + }, + documents: [ + { + name: "exploretech.la 2021 Program", + src: "https://drive.google.com/file/d/1icRfJae9wy8Kld8qSUxsY0pbsyIu9nh2/view?usp=sharing", + }, + { + name: "Student Event Day Guide", + src: "https://docs.google.com/document/d/14_eHYyuf4V9BnUUEUijrYEYVxUTPfAUFZrey7blTc9s/edit?usp=sharing", + }, + { + name: "Workshop/Panel Schedule", + src: "https://tinyurl.com/contentschedule2021", + }, + ], + support: { + heading: "Event Support", + paragraphs: [ + "If you are experiencing any technical difficulties or have any questions about the event, please first refer to the FAQs below. ", + " If your question is not answered below, please reach out to us at exploretechla@cs.ucla.edu for assistance! ", + ], + liveHeading: "Live Event Day Support", + liveBody: + "For the duration of our event, attendees will be able to join a Zoom meeting (link below) to ask any questions live to an exploretech.la 2021 Staff Member.", + zoom: { + name: "Zoom Help Room Link", + src: "https://tinyurl.com/exploretechla2021help", + }, + hoursLabel: "Zoom Meeting Hours:", + hours: "Saturday and Sunday, 9:30AM to 1:30PM", + }, + }, + faq: { + kind: "registration", + initialOpenKey: 11, + intro: { + heading: "Still interested in attending?", + body: 'If you are still interested in attending, fill out the waivers at the bottom of the page (under "Waivers") and email them to us at exploretechla@cs.ucla.edu ASAP so we can get you registered!', + }, + items: [ + { + key: 11, + question: + "I'm still interested in attending but registration is closed! Can I still attend? ", + answer: + 'Yes you can! Just fill out all of the waivers under the "Waivers" secrtion below and email them to us at exploretechla@cs.ucla.edu ASAP so we can get you registered!', + }, + { + key: 0, + question: + "Is there a registration fee? Are there any associated costs with the program (ie: need to pay for an account on the virtual platform?) ", + answer: + "This event is completely free for all attendees! All you will need is a computer and stable internet to access the event.", + }, + { + key: 1, + question: "What prior background is needed? ", + answer: + "No experience needed! This event is meant to help you explore the different fields of CS and tech!", + }, + { + key: 3, + question: "How would virtual format work? (boxes, platform, etc) ", + answer: + "The event will be hosted completely online. exploretech.boxes will be sent to each attendee (while supplies last) as well as instructions on how to access the virtual platform prior to the event. Attendees will receive more information once registration is closed.", + }, + { + key: 4, + question: "What is an exploretech.box?", + answer: + "An exploretech.box is a box that will hold all of the supplemental workshop materials and swag for the event. We will ship these to our attendees prior to the event. Supplies will be limited, but not having a box will not prevent you from attending our event.", + }, + { + key: 6, + question: "What is the College Buddy Chat?", + answer: + "Each day you will have the opportunity to talk with one to two UCLA undergraduates about their life at UCLA and in STEM. Feel free to ask them any questions you may have about going from high school to college and pursuing a STEM degree!", + }, + ], + }, + waivers: { + intro: WAIVER_INTRO, + note: WAIVER_NOTE, + forms: WAIVER_FORMS, + allFormsHeading: + "All forms can also be found here, in both PDF and Google Docs formats (for easier editing):", + allFormsBody: + "To edit the Google Docs, please make a copy of the waiver into your own Drive and fill it out there. Simply save as PDF to upload to the registration form.", + allForms: { + name: "All Forms (PDFs and Google Docs)", + src: "https://tinyurl.com/exploretechla2021-waivers", + }, + }, +}; diff --git a/src/content/events/2022.ts b/src/content/events/2022.ts new file mode 100644 index 0000000..776b278 --- /dev/null +++ b/src/content/events/2022.ts @@ -0,0 +1,396 @@ +import busRoutes from "../../static/maps/bus_routes.jpg"; +import ackermanUnion2F from "../../static/maps/su_map_ack_2.jpeg"; +import ackermanUnion3F from "../../static/maps/su_map_ack_3.jpeg"; +import wristbands from "../../static/maps/wristband_coordination.png"; +import eventProgram from "../../static/pdf/Event_Program.pdf"; + +import type { EventContent } from "./types"; +import { WAIVER_FORMS } from "./waivers"; + +/** + * exploretech.la 2022, served at /resources. + * Its workshop cards are archived recordings, so every card carries a video facade. + */ +export const EVENT_2022: EventContent = { + year: "2022", + programHref: eventProgram, + schedule: [ + { key: 1, name: "Opening Ceremony", time: "9:00AM - 9:30AM" }, + { key: 2, name: "Session 1", time: "9:30AM - 10:15AM" }, + { key: 3, name: "Session 2", time: "10:30AM - 11:15AM" }, + { key: 4, name: "Session 3", time: "11:30AM - 12:15PM" }, + { key: 5, name: "Lunch with a Mentor", time: "12:15PM - 1:00PM" }, + { key: 6, name: "Closing Ceremony", time: "1:00PM - 1:30PM" }, + ], + maps: [ + { name: "Bus Routes", src: busRoutes }, + { name: "Ackerman Union 2F", src: ackermanUnion2F }, + { name: "Ackerman Union 3F", src: ackermanUnion3F }, + ], + workshopsPlacement: "after-wristbands", + wristbandImage: { src: wristbands, alt: "Wristband coordination" }, + workshops: [ + { + heading: "Workshops", + cards: [ + { + kind: "archivedVideo", + title: "Coding 101", + embedId: "WaZ2bN8t2cc", + caption: "(Session 1 Recording)", + links: [ + { + name: "Session 2 Recording", + src: "https://youtu.be/-NN-O42OleY", + }, + { + name: "Student Guide", + src: "https://docs.google.com/document/d/1ElU9F8cRtpw7QWjIj3QkJsBVG8xmE2Ywpd2b8Gxi75w/edit", + }, + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1sZ5aDpGpKbjd5CuhOd5hTCljncScBwf3m7FWMb6_jd4/edit#slide=id.gbbd58c2398_0_15", + }, + { + name: "Solutions", + src: "https://docs.google.com/document/d/1UDakvA2lzu5px7Ox20s7E8ZYN17WtcPTcmtoTANRL5o/edit", + }, + ], + }, + { + kind: "archivedVideo", + title: "ExploreWeb.la: Web Design & Dev", + embedId: "jP8itIcFFa4", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/u/0/d/1QB4seIW1s97Aqrf-Wn8n9UQq6YNM3opvqzt3ZUZtGOM/edit", + }, + { + name: "Review Sheet", + src: "https://docs.google.com/document/d/1zAbuNQzRgHYhH9E9sdJudYQ96FJYfXcqfO4qJv736K4/edit", + }, + { + name: "Codepen", + src: "https://codepen.io/raphaeling/pen/VwmjxzJ", + }, + ], + }, + { + kind: "archivedVideo", + title: "Operating Systems", + embedId: "u-X1K9Rz75U", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/17Dl1hE1kUmffXtlF8aC_CMuXNvFgTp3IgLADvT9pJmw/edit#slide=id.g7cb5bbd26d_0_20", + }, + { + name: "Linux Emulator", + src: "http://copy.sh/v86/?profile=linux26", + }, + ], + }, + { + kind: "archivedVideo", + title: "Draw with Code", + embedId: "oTzUKgOTEGE", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1yKOG-eBJveoCHa0XX0-JhGSxVLSJ6x4d993uWdySm_I/edit?usp=sharing", + }, + { + name: "Repl.it Project", + src: "https://repl.it/@AlyssaSchimm/Python-Graphics-Skeleton", + }, + ], + }, + { + kind: "archivedVideo", + title: "Less Quantum More Computing", + embedId: "rC-llUnQt88", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1ceW21j9s6EwD4MtMN8gytIH_ku-sZLy0JnAuQ4pmrlQ/edit#slide=id.gc204a219fe_1_10", + }, + { + name: "Definitions Doc", + src: "https://docs.google.com/document/d/1dbcPxbjI50PYgRMoLolfNP3FGHvBojHuFfYHRtzGNv4/edit?usp=sharing", + }, + { + name: "Google Colab", + src: "https://colab.research.google.com/drive/1jViSXWVgbFK6GeU2z7VrSxx8PjOF8dOk", + }, + ], + }, + { + kind: "archivedVideo", + title: "Crack the Code with Cryptography", + embedId: "HCrLnfs5yqU", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1Z2GZoJtq2IwqjO2Akr9vIv6km_Pc_a1h_0b98HMQNIA/edit?usp=sharing", + }, + { + name: "Escape Room Google Form", + src: "https://forms.gle/bmewxDNqbuXENENf8", + }, + { + name: "Escape Room Questions and Solutions", + src: "https://docs.google.com/document/d/14P2cz0_CLCPlWJFjyXICMdhN_YfoRlUiPSZW0wxvf1w/edit", + }, + ], + }, + { + kind: "archivedVideo", + title: "Escaping Reality with Game Development", + embedId: "PiDWtEGfgiM", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1-FAlSMU2WDh_qZVjUSUV6bNSShXxCP5IAzx9y3guXW8/edit", + }, + { + name: "Codepen Activity", + src: "https://codepen.io/catpls/pen/QWdgpwe", + }, + ], + }, + { + kind: "archivedVideo", + title: "Murder Mystery!", + embedId: "RPZqYD4KX9I", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/17QCnC1LfixiFyfCSMTwE-6VUC-NyznB3UHs3Szrx3HQ/edit#slide=id.g7cb5bbd26d_0_20", + }, + { + name: "Worksheet", + src: "https://docs.google.com/document/d/1R0Ne_90c7aRuzvtkA5HSPAgBw70GJfwiH6i1-1BCxh0/copy", + }, + ], + }, + { + kind: "archivedVideo", + title: "AI Adventures", + embedId: "YemyR8KdDe0", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1rq2lxcW1MK1pSZZP2bl1-kz5JkvRN0kJ-GpzIXxm1UY/edit#slide=id.g7cb5bbd26d_0_20", + }, + { + name: "Worksheet", + src: "https://docs.google.com/document/d/1KQi86CtI4OG2cgEoZLQCDeQk8a_204Ek_yESjdlAAaM/edit?usp=sharing", + }, + ], + }, + { + kind: "archivedVideo", + title: "Digital Design", + embedId: "sjHU3FDty68", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1SzxLUCi8xBJRQC4uGt9fIk-gZvdqeS-_HdIG0ToaQpU/edit?usp=sharing", + }, + { + name: "Miro Whiteboard", + src: "https://miro.com/app/board/o9J_lMyUL1U=/", + }, + ], + }, + { + kind: "archivedVideo", + title: "Data Science", + embedId: "OlKpWAwA32Q", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1WonMzMY2AaReC5yPW94akZaSKOuBV2ci2iYswWIEG5U/edit#slide=id.g7cb5bbd26d_0_20", + }, + { + name: "Spreadsheet", + src: "https://docs.google.com/spreadsheets/d/1E6mFcAMfeSlz1B61lInOuTVO1tvR7ggYzXTeqxCxkQQ/edit#gid=1207580287", + }, + ], + }, + { + kind: "archivedVideo", + title: "Tech & Society", + embedId: "b-bgVV-aEKI", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/15Kcr7BihmCtBo-l2CxGelzLGCglmXhx0-3L-YtonyDo/edit#slide=id.g7cb5bbd26d_0_20", + }, + ], + }, + { + kind: "archivedVideo", + title: "Bot Buddy", + subtitle: "Hosted by DIY Girls", + embedId: "b2ijVF2aLQs", + caption: "(Session 1 Recording)", + links: [ + { + name: "Session 2 Recording", + src: "https://youtu.be/jhGA1TZG9PA", + }, + { + name: "Skin Templates", + src: "https://drive.google.com/drive/folders/1aaPdABZMsORNaRHP7EZKMbfST2x6j_bt", + }, + { + name: "Youtube Tutorial", + src: "https://www.youtube.com/watch?v=JKoLj8lmkfM", + }, + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1XNH37Cw5P7_tEWqWckaev5vxETBVVapTIbJXXr-6uNs/edit#slide=id.p", + }, + { + name: "Follow Up Instructions", + src: "https://docs.google.com/document/d/1xSpGUjHA2qL8DJjGagBPAolmfcHAdcHvuTpl_lC67po/edit", + }, + ], + }, + ], + }, + { + heading: "Panels", + cards: [ + { + kind: "archivedVideo", + title: "Diverse Careers in Computer Science", + embedId: "xzZzhmeo9_Y", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/18H8XdlIscdZdeLiOUCPtAfu0tuhukW1EfDBH8uDkUXE/edit#slide=id.gc5794a6fcc_0_12", + }, + { + name: "Padlet", + src: "https://padlet.com/bonniebonnielee/nm6pc2j59e5hhigl", + }, + { + name: "Panelist Info and Resources", + src: "https://docs.google.com/document/d/1hG6YO9epajDiYqma-j882qRzmUITZdlTFWvvjrTTy44/edit?usp=sharing", + }, + ], + }, + { + kind: "archivedVideo", + title: "Representation in Computer Science", + embedId: "vrPl7EmwV9A", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/16KnUGORLcdWsVqfCFgGzkqFniVPzHvtL2WyoY22Mgv4/edit#slide=id.gc7ccf7fc99_0_0", + }, + { + name: "Padlet", + src: "https://padlet.com/wsu315/a9fa6pk1k3iodi7o", + }, + ], + }, + ], + }, + ], + faq: { + kind: "resources", + initialOpenKey: null, + items: [ + { + key: 1, + question: + "I'm still interested in attending but registration is closed! Can I still attend?", + answer: + 'Yes you can! Just fill out all of the waivers under the "Waivers" section below and email them to us at exploretechla@cs.ucla.edu ASAP so we can get you registered!', + }, + { + key: 2, + question: + "Is there a registration fee? Are there any associated costs with the program (i.e: need to pay for an account on the virtual platform?)", + answer: + "This event is completely free for all attendees! All you will need is a computer and stable internet to access the event.", + }, + { + key: 3, + question: "What prior background is needed?", + answer: + "No experience needed! This event is meant to help you explore the different fields of CS and tech!", + }, + { + key: 4, + question: "When does the event start/end?", + answer: + "The Opening Ceremony will begin at 9AM and the Closing Ceremony will begin at 1:00PM. Refer to the program for more schedule info!", + }, + { + key: 5, + question: "How does the raffle work?", + answer: + "Workshops will give 1 ticket to students if they ask a good question or complete some activity. Panels will also give 1 ticket to all students who attend. Hold onto your ticket until the Closing Ceremony, where a raffle will be held and prizes will be given!", + }, + { + key: 6, + question: "Where is bus pick-up?", + answer: + "Bus pick-up will be at the same place as the drop-off area (across Intramural Field) as detailed in the logistics packet sent to your school.", + }, + { + key: 7, + question: + "How many high school students and how many high schools are participating in our events today?", + answer: "There are 8 schools with a total of 371 students! Woo!", + }, + { + key: 8, + question: "How does the mentorship session + lunch work?", + answer: + "Each student’s wristband has a number on it indicating which mentor group they are in. Mentors will hold up a sign with their group number on it and students will find their mentors accordingly. Mentor groups will take turns getting lunch and they can use any space in the AGB (around the chairs/on the floor/etc) to gather around and start the mentorship session.", + note: "Group 3 students will start finding their mentors 15 minutes prior to the end of Session 3 and will get their lunch first.", + }, + { + key: 9, + question: "Can students go to any workshop they want to?", + answer: + "Yes! However, some workshops are limited by room size/workshop material capacity so they will be closed once capacity is reached on a first-come, first-served basis, and students will be directed to other workshops instead.", + }, + { + key: 10, + question: "Are there restrooms in the venue?", + answer: "Restrooms are on the third floor of Ackerman Union.", + }, + { + key: 12, + question: "Who is organizing this event?", + answer: + "This event is being organized by exploretech.la, a group of UCLA undergraduate students! Learn more about us on our website!", + }, + { + key: 13, + question: "What are the raffle prizes?", + answer: + "3 big prizes (including airpods!) are being given out during the Closing Ceremony, while additional digital prizes will be awarded to students who fill out the Feedback Survey.", + }, + { + key: 14, + question: "How many students are at UCLA?", + answer: "There are over 40,000 students at UCLA", + }, + ], + }, + waivers: WAIVER_FORMS, + // The deployed page reuses the "Wristbands" heading above the feedback form. + feedbackHeading: "Wristbands", + feedback: { + kind: "link", + href: "https://tinyurl.com/exploretechla2022feedback", + }, +}; diff --git a/src/content/events/2023.ts b/src/content/events/2023.ts new file mode 100644 index 0000000..3ce984c --- /dev/null +++ b/src/content/events/2023.ts @@ -0,0 +1,245 @@ +import busRoutes from "../../static/maps/bus_routes.jpg"; +import ackermanUnion2F from "../../static/maps/su_map_ack_2.jpeg"; +import ackermanUnion3F from "../../static/maps/su_map_ack_3.jpeg"; + +import type { EventContent } from "./types"; + +/** + * exploretech.la 2023, served at /resources2023. + * The page reuses the 2022 schedule times verbatim and has no FAQ or waiver section. + */ +export const EVENT_2023: EventContent = { + year: "2023", + programHref: + "https://drive.google.com/file/d/1QBPh_9WEnlVxcw18xKHrCtPD4Wv9tfrj/view?usp=sharing", + schedule: [ + { key: 1, name: "Opening Ceremony", time: "9:00AM - 9:30AM" }, + { key: 2, name: "Session 1", time: "9:30AM - 10:15AM" }, + { key: 3, name: "Session 2", time: "10:30AM - 11:15AM" }, + { key: 4, name: "Session 3", time: "11:30AM - 12:15PM" }, + { key: 5, name: "Lunch with a Mentor", time: "12:15PM - 1:00PM" }, + { key: 6, name: "Closing Ceremony", time: "1:00PM - 1:30PM" }, + ], + maps: [ + { name: "Bus Routes", src: busRoutes }, + { name: "Ackerman Union 2F", src: ackermanUnion2F }, + { name: "Ackerman Union 3F", src: ackermanUnion3F }, + ], + workshopsPlacement: "before-maps", + workshops: [ + { + heading: "Workshops", + cards: [ + { + kind: "described", + title: "Fundamentals of Data Science", + instructors: "Takao Oba and Riya Bhatla", + description: + "Unlock the power of data and enhance your skills with our comprehensive Data Science workshop. Join us and discover the latest tools and techniques to turn data into actionable insights and drive better decision making.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1fxkiS4XMG671dKL1WzmJzBGUzU10INoKsWndY9HnvBc/edit#slide=id.g7cb5bbd26d_0_20", + }, + ], + }, + { + kind: "described", + title: "Game Development", + instructors: "Michael Bunte and Grace Mao", + description: + "Ever wonder how your favorite games are created? How do artists, programmers, composers, and others work together to create an amazing final product? Come find out at the game development workshop!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1p-yarwAOKpz6IMsHkZAFFdjfmZ_G1qHaT_1XrrDNEzY/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Python Turtle", + instructors: "Aparna Hariharan and Isha Rajput", + description: + "Come learn how to code in Python using Python's fun art library called Python Turtle! By the end, you'll have created a customized digital sticker that you can design!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/16Ip1fP3i4CPrJL6g_jn22DaIsZH1nY_a4OiTXoTkMp4/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "SCRATCHing the Surface of Coding", + instructors: "Michael Bunte and Jason Lozada", + description: + "Want to learn how to code but don’t know where to start? Come join us as we introduce you to Scratch, a visual programming language that allows you to create your own interactive animations, stories, and games.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1f8GMGnJuTECpuWZ1JYiYT5e-qf_SXuyY_x3WqQ5VsmQ/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "CS Operations in Minecraft", + instructors: "Grace Mao and Kelly Zhang", + description: + "Logic gates, circuits, game dev...in Minecraft ?!?! Come to our workshop to learn about Minecraft's amazing redstone system and how it connects to engineering :0", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1QgNJpiJEAx07bN88bUnQjo8nLJvXR26ivJB7x3J2p2Q/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Are you Related to a Strawberry?", + instructors: "Wendy Su and Krisha Chokshi", + description: + "Are you related to a strawberry? Are you the plant parent? Learn about the intersection of biology and computer science in what we call bioinformatics. Come for a hands-on experience in extracting DNA!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1dH1imIjo3ZSOoGxk3vyewLHc5G3JQufePPGY-5pxriE/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Basics of Encryption", + instructors: "Ashish Basetty and Takao Oba", + description: + "You’ve heard of hackers, but do you know about the people who stop them? Join us as we learn about how we keep our data secure. Work together to solve codes and puzzles, and learn how we can keep passwords and personal information private.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1Rpj6HeWxVXUPhcaeAA8StKu5Vkv6zR8Di1hQegGkHag/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Intro to Web Development", + instructors: "Emily Nham and Jazlin Ong", + description: + "Have you ever wondered how websites are made? Join us to learn the basics of HTML and CSS, the coding languages of the web. By the end of our session, you’ll walk out with the ability to make your own website!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/139VY0bVt_-fYPsUwpGKRgFs7QJVkpEg1FoHmVEuksiI/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Internet of Things", + instructors: "Kunal Patil and Krisha Chokshi", + description: + "Explore the Internet of Things, its evolution, and its relevance to today’s society! Students will learn about cybersecurity protocols and get a chance to demo and test IoT products. While brainstorming daily uses of IoT, they will discover its widespread impact on each of our lives.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1dv_L1bAyp3XUmvUKLUO70yvJec7yGDDqHjTltZckrMk/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Natural Language Proccessing", + instructors: "Ashish Basetty and Jazlin Ong", + description: + "Every used Siri? ChatGPT? Google Translate? Come to this workshop to learn how a computer thinking in 0’s and 1’s can learn to speak like a human. Learn more about how human language works, and how we can come up with rules to teach it to a machine.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1OUdQO-sLEhkZEgIDItNT-2VPtL_DmPZjz43SiW1GYXo/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Machines Learn How to Learn", + instructors: "Michael Song and Kelly Zhang", + description: + "As technology advances and we get smarter devices, we are also collecting more and more data. All of this data is meaningless without the help of machine learning. In this workshop we’ll delve into how machines use data to predict and answer questions.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1kp-5c-YiCRJOgF6xbcHn7mzWIFt9eBSVZTwLsQVhz7o/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Making Your Own Shazam", + instructors: "Riya Bhatla and Joe Lin", + description: + "Ever heard a song at a party that you didn’t recognize? Shazam is the solution. In this workshop, you’ll get hands-on experience to build your own song recognizer. You’ll learn about the basics of audio processing and analysis and much more!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1DzbhxN7w5s9n2F7jyAOIv4ZvDkF-ILkaamgSqM98gvU/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Introduction to Autonomous Vehicles", + instructors: "Michael Song and Joe Lin", + description: + "Ever wondered how a Tesla or Waymo car navigates itself through a seemingly countless number of road scenarios? Join us in this workshop to learn about the fundamental building blocks of autonomous vehicle technology and even get a chance to witness object detection models in action!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/11jR37NzXGQYdFi_RJgtd7LBzPaSyA31vdjlfSBxEj1k/edit?usp=sharing", + }, + ], + }, + ], + }, + { + heading: "Panels", + cards: [ + { + kind: "described", + title: "Careers in Computer Science", + instructors: "Isha Rajput and Emily Nham", + description: + "Want to know what it's like working in tech? Meetcurrent students and graduates pursuing Computer Science at places like Google, Facebook, Atlassian, and CreditKarma! Come learn about what they do and how they ended up where they are!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1mVDXX-o_0BZO19UC-kFXJVUkMU7wvUfgaNii9bxDxNE/edit?usp=sharing", + }, + { + name: "Panelist Info", + src: "https://docs.google.com/document/d/1_a-basdjuWGzqQe_9g3LwmtLoQhuPX_Bmhpx-llbyZo/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "Divieristy 101", + instructors: "Aparna Hariharan and Jason Lozada", + description: + "Join the conversation with our esteemed guests regarding Diversity in Computer Science and what their experiences are like as minorities in this field.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1LPeMr3cr28CYTaLrrDpeh3EMlx5tyaZ1HzyRoLDaqOw/edit?usp=sharing", + }, + { + name: "Panelist Info", + src: "https://docs.google.com/document/d/1BvEGkB0tGuW9C-pyQFqqC_f-rgnLe8NEEccIaj5-NnU/edit?usp=sharing", + }, + ], + }, + ], + }, + ], + feedback: { kind: "coming-soon", disabled: false }, +}; diff --git a/src/content/events/2026.ts b/src/content/events/2026.ts new file mode 100644 index 0000000..1af2fcf --- /dev/null +++ b/src/content/events/2026.ts @@ -0,0 +1,216 @@ +import busRoutes from "../../static/maps/bus_routes.jpg"; +import ackermanUnion2F from "../../static/maps/su_map_ack_2.jpeg"; +import ackermanUnion3F from "../../static/maps/su_map_ack_3.jpeg"; +import eventProgram from "../../static/pdf/Event_Program_2026.pdf"; + +import type { EventContent } from "./types"; + +/** + * exploretech.la 2026, served at /resources2026. + * Panels are authored after the workshops; both render as described cards. + */ +export const EVENT_2026: EventContent = { + year: "2026", + programHref: eventProgram, + schedule: [ + { key: 1, name: "Opening Ceremony", time: "9:55AM - 10:10AM" }, + { key: 2, name: "Session 1", time: "10:15AM - 10:50AM" }, + { key: 3, name: "Session 2", time: "11:00AM - 11:40AM" }, + { key: 4, name: "Session 3", time: "11:50AM - 12:30PM" }, + { key: 5, name: "Lunch with a Mentor", time: "12:30PM - 1:10PM" }, + { key: 6, name: "Closing Ceremony", time: "1:10PM - 1:30PM" }, + ], + maps: [ + { name: "Bus Routes", src: busRoutes }, + { name: "Ackerman Union 2F", src: ackermanUnion2F }, + { name: "Ackerman Union 3F", src: ackermanUnion3F }, + ], + mapsLinkRel: "noopener noreferrer", + workshopsPlacement: "before-maps", + workshops: [ + { + heading: "Workshops", + cards: [ + { + kind: "described", + title: "Tower Stacking Game", + instructors: "Emma Qiu and Eli Drewry", + description: + "Students will compete in groups to see who can create the tallest free-standing tower with limited materials (popsicle sticks, tape, rope, etc). This challenge tests their communication, strategy, and discovering the importance of architecture while getting a chance to win prizes.", + links: [ + { name: "Slides", src: "https://canva.link/79sw40e0u4vhk4j" }, + ], + }, + { + kind: "described", + title: "Intro to Game Development", + instructors: "Joshua Jin and Paul Macapinlac", + description: + "Learn how the snake game works in this fun workshop! Uncover the secrets of Python and learn to customize your snake. Before you know it, you’ll be a pro game dev with a fully customized snake game.", + links: [ + { name: "Slides", src: "https://canva.link/y27fiyfbq1pfd3t" }, + ], + }, + { + kind: "described", + title: "Battle Bots", + instructors: "Sofia Matos and Benjamin Garcia", + description: + "Drive XRP Robots on a field to take down your opponents in a game of robot soccer! After interacting with code, you will be able to drive and dribble past other robots to score using teamwork and strategy.", + links: [ + { name: "Slides", src: "https://canva.link/6edmsozmzcmhkf2" }, + ], + }, + { + kind: "described", + title: "Squid Games Several-Legged Pentathalon (STEM Edition)", + instructors: "Matthew Chen and Paul Macapinlac", + description: + "Inspired by the hit Netflix series “Squid Games,” collaborate with up to 3 of your friends to participate in the STEM Games Several-Legged Pentathlon, where you will be tasked to solve STEM puzzles and activities in an interactive race against time.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/10K0MQv8v8IqyC8qRrrJJanGZpn2YVPQUX6VFvdg_1ww/edit?slide=id.gf6d3ad9fd1_0_1011#slide=id.gf6d3ad9fd1_0_1011", + }, + ], + }, + { + kind: "described", + title: "AI Photobooth", + instructors: "Ridhima Seth and Joshua Jin", + description: + "Do you want to learn more about AI image generation? Come to AI Photobooth! This workshop will show you the basics of how an AI image generator works, and then we’ll let you try it for yourself!", + links: [ + { name: "Slides", src: "https://canva.link/p62kuwtzwy1ieb6" }, + ], + }, + { + kind: "described", + title: "COVID-19 Simulator", + instructors: "Clemente Irarrazaval and Eli Drewry", + description: + "Students will get an introduction into the field of game development by designing a COVID-19 spread simulator. Using Scratch they’ll explore conditionals, loops, and movement while working through obstacles. Students work in teams to build, test, and modify their own simulation-based game, also encouraging teamwork.", + links: [ + { name: "Slides", src: "https://canva.link/5noyqe130bqgdqs" }, + ], + }, + { + kind: "described", + title: "AI Music", + instructors: "Matthew Chen and Benjamin Garcia", + description: + "The purpose of this workshop is to teach students how to use suno.com, an AI music website that allows users to create any form of music by simply entering into the prompt bar.", + links: [ + { name: "Slides", src: "https://canva.link/ja5m1l7cy6k1zsh" }, + ], + }, + { + kind: "described", + title: "Bridge Building Competition", + instructors: "Atharv Panditrao and Clemente Irarrazaval", + description: + "Students will design and build a small bridge using limited materials. They will learn basic structural concepts, apply problem-solving skills, and work collaboratively to create a bridge that can hold the greatest load. Students compete in teams and test their designs in a fun final challenge.", + links: [ + { name: "Slides", src: "https://canva.link/6ov265jv8fwkeql" }, + ], + }, + { + kind: "described", + title: "Live Coding Music", + instructors: "Ana Laura Santana and Atharv Panditrao", + description: + "Sometimes learning code can seem like it’s exclusively professional or something you will never see in your career. But in truth, coding can be useful for all types of passions beyond a corporate life, which is what gives it its beauty. In this workshop, you will learn how to become a DJ using only code!", + links: [ + { name: "Slides", src: "https://canva.link/9pwbi4o0ztadleh" }, + ], + }, + { + kind: "described", + title: "Prompt Engineering", + instructors: "Alexander Chien and Arjun Asudani", + description: + "Join our prompt engineering workshop to learn how to make the most out of your AI credits. Today’s chat models have vast capabilities, but accessing these skills can be more nuanced than just a straightforward question. We’ll cover prompting methods you can use to ensure you get the results you want. Let’s harness the power of AI!", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1IJsZaSjOSEOXJBQJAjdo4N0Zz3iFHkn5otzM8JBI4do/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "How Machines can learn like humans (Neural Networks)", + instructors: "Ana Laura Santana and Emma Qiu", + description: + "Students will learn the basics of neural networks, and how they are present in our everyday lives. They will also get hands-on experience with neural networks by loading data and training their own model. In the end, students will gain a conceptual understanding and practical skills in AI, which has become increasingly influential in the tech world.", + links: [ + { name: "Slides", src: "https://canva.link/t49ze6ojz5edwil" }, + ], + }, + { + kind: "described", + title: "AI Guess Who", + instructors: "Ridhima Seth and Amy Lloyd", + description: + "Want to master Guess Who? with logic and strategy? Learn how decision trees and probability help you ask the smartest questions, minimize guesswork, and consistently win. This session makes algorithmic thinking fun and practical—sharpen your problem-solving skills while gaining a competitive edge in both games and real-world decision-making!", + links: [ + { name: "Slides", src: "https://canva.link/gu84339jr1878xm" }, + ], + }, + { + kind: "described", + title: "Game Theory", + instructors: "Arjun Asudani and Alexander Chien", + description: + "What would you do if your success depended on someone else’s choice? This interactive workshop introduces game theory through fast-paced games and real-life scenarios. You’ll explore why people compete, when cooperation fails, and how small incentives can completely change outcomes.", + links: [ + { + name: "Slides", + src: "https://www.canva.com/design/DAHDlh96pCw/4F5lGBuqGXyBBO1a2Lyutg/edit", + }, + ], + }, + ], + }, + { + heading: "Panels", + cards: [ + { + kind: "described", + title: "Careers in Tech Panel", + instructors: "Meet Modi / Tammy Sujaritchai and Sofia Matos", + description: + "Curious about what a career in tech actually looks like? Join us for a discussion with students and professionals as they share their experiences, career paths, and advice for breaking into the industry. Learn about different roles in tech, what skills matter most, and how to start exploring your own path.", + links: [ + { + name: "Slides", + src: "https://drive.google.com/drive/folders/1ZBMXid-LFwTB84lA5etUmEHMExT9Ee9W", + }, + { + name: "Panelist Info", + src: "https://docs.google.com/document/d/1Gaj_cB8Y7RgeDnxmjOmZGS6XhREWHHRYwg-b7EyUBiw/edit?usp=sharing", + }, + ], + }, + { + kind: "described", + title: "CS/Tech in College Panel", + instructors: "Meet Modi / Tammy Sujaritchai and Amy Lloyd", + description: + "What is it really like to study computer science or tech in college? In this panel, students will talk about classes, projects, internships, clubs, and how they found their place in tech. Come ask questions, hear honest advice, and get a better sense of what the college experience can look like.", + links: [ + { + name: "Slides", + src: "https://docs.google.com/presentation/d/1XCDfJbdINGh4_OCPuUYX9roVqXul3N04L3zM6cUqm5M/edit?usp=sharing", + }, + { + name: "Panelist Info", + src: "https://docs.google.com/document/d/1Fkc3Ot_d7xpQTU8_oOfYIu_ugJzcenvjnlOOvWkVBBM/edit?usp=sharing", + }, + ], + }, + ], + }, + ], + feedback: { kind: "coming-soon", disabled: true }, +}; diff --git a/src/content/events/index.ts b/src/content/events/index.ts new file mode 100644 index 0000000..eee89c7 --- /dev/null +++ b/src/content/events/index.ts @@ -0,0 +1,29 @@ +import type { EventContent, EventYear } from "./types"; +import { EVENT_2022 } from "./2022"; +import { EVENT_2023 } from "./2023"; +import { EVENT_2026 } from "./2026"; + +export * from "./types"; +export { REGISTRATION_2021 } from "./2021"; +export type { RegistrationContent } from "./2021"; +export { WAIVER_FORMS } from "./waivers"; + +export const EVENTS = { + "2022": EVENT_2022, + "2023": EVENT_2023, + "2026": EVENT_2026, +} satisfies Record; + +/** + * Public routes, in the order they were published. The 2022 event keeps the + * unnumbered /resources path it shipped with. + */ +export const EVENT_ROUTES: readonly { + readonly route: string; + readonly year: EventYear; + readonly navLabel: string; +}[] = [ + { route: "/resources", year: "2022", navLabel: "exploretech 2021" }, + { route: "/resources2023", year: "2023", navLabel: "exploretech 2023" }, + { route: "/resources2026", year: "2026", navLabel: "exploretech 2026" }, +]; diff --git a/src/content/events/types.ts b/src/content/events/types.ts new file mode 100644 index 0000000..43eaf77 --- /dev/null +++ b/src/content/events/types.ts @@ -0,0 +1,64 @@ +import type { DocumentLink } from "../types"; +import type { EVENTS } from "./index"; + +export type EventYear = keyof typeof EVENTS; +export interface ScheduleEntry { + readonly key: number; + readonly time: string; + readonly name: string; +} +export interface ScheduleDay { + readonly title: string; + readonly items: readonly ScheduleEntry[]; +} +export type WorkshopCard = { + readonly title: string; + readonly links: readonly DocumentLink[]; +} & ( + | { + readonly kind: "archivedVideo"; + readonly embedId: string; + readonly subtitle?: string; + readonly caption?: string; + } + | { + readonly kind: "described"; + readonly instructors?: string; + readonly description?: string; + } +); +export interface WorkshopSection { + readonly heading: "Workshops" | "Panels"; + readonly cards: readonly WorkshopCard[]; +} +export interface FaqEntry { + readonly key: number; + readonly question: string; + readonly answer: string; + readonly note?: string; +} +export interface FaqContent { + readonly kind: "registration" | "resources"; + readonly initialOpenKey: number | null; + readonly items: readonly FaqEntry[]; + /** Extra lead-in shown only on the registration page, above the first question. */ + readonly intro?: { readonly heading: string; readonly body: string }; +} +export type Feedback = + | { readonly kind: "link"; readonly href: string } + | { readonly kind: "coming-soon"; readonly disabled: boolean }; +export interface EventContent { + readonly year: string; + readonly programHref: string; + readonly schedule: readonly ScheduleEntry[]; + readonly maps: readonly DocumentLink[]; + /** 2026 added rel="noopener noreferrer" to its map buttons; 2022/2023 did not. */ + readonly mapsLinkRel?: "noopener noreferrer"; + readonly workshops: readonly WorkshopSection[]; + readonly workshopsPlacement: "before-maps" | "after-wristbands"; + readonly wristbandImage?: { readonly src: string; readonly alt: string }; + readonly faq?: FaqContent; + readonly waivers?: readonly DocumentLink[]; + readonly feedbackHeading?: string; + readonly feedback: Feedback; +} diff --git a/src/content/events/waivers.ts b/src/content/events/waivers.ts new file mode 100644 index 0000000..6a8dfcd --- /dev/null +++ b/src/content/events/waivers.ts @@ -0,0 +1,21 @@ +import liabilityWaiver from "../../static/pdf/exploretech_la_2022_Liability_Waiver.pdf"; +import multimediaRelease from "../../static/pdf/exploretech_la_2022_Multimedia_Release_Form.pdf"; +import studentVerification from "../../static/pdf/exploretech_la_2022_Student_Verification_Form.pdf"; + +import type { DocumentLink } from "../types"; + +/** + * The same three signed forms are offered on /register and on /resources. + * Both pages link the identical PDFs, so they are authored once here. + */ +export const WAIVER_FORMS: readonly DocumentLink[] = [ + { name: "Liability Waiver", src: liabilityWaiver }, + { name: "Multimedia Release Form", src: multimediaRelease }, + { name: "Student Verification Form", src: studentVerification }, +]; + +export const WAIVER_INTRO = + "Please download and sign the following waivers. They are a required part of your registration."; + +export const WAIVER_NOTE = + "Note: For high school students, the waivers require a parent/guardian signature."; diff --git a/src/content/people.ts b/src/content/people.ts new file mode 100644 index 0000000..66b100c --- /dev/null +++ b/src/content/people.ts @@ -0,0 +1,170 @@ +import images from "../constants/optimizedImages"; +import type { PersonProfile } from "./types"; + +/** + * The authoritative profile for every team member and past speaker. + * + * One entry per person. Contextual role titles are NOT stored here: a person + * can hold different titles in different teams, so titles live with the + * assignment in `teams.ts`. To update someone's name, portrait or profile + * link, edit exactly one entry below. + */ +export const people = { + "spoorthy-nalumachu": { + name: "Spoorthy Nalumachu", + image: images["team/leadership/2027/spoorthy-nalumachu.jpg"].src, + link: "https://www.linkedin.com/in/spoorthy-nalumachu", + }, + "clara-zhang": { + name: "Clara Zhang", + image: images["team/leadership/2027/clara-zhang.jpg"].src, + link: "https://www.linkedin.com/in/clara-zhang-009497197/", + }, + "annika-renganathan": { + name: "Annika Renganathan", + image: images["team/leadership/annika-renganathan.jpg"].src, + link: "https://www.linkedin.com/in/annika-renganathan-b437022b9", + }, + "shaina-grover": { + name: "Shaina Grover", + image: images["team/leadership/shaina-grover.jpg"].src, + link: "https://www.linkedin.com/in/shaina-grover", + }, + "sandra-pan": { + name: "Sandra Pan", + image: images["team/leadership/sandra-pan.jpg"].src, + link: "https://www.linkedin.com/in/sandra-pan-b994802aa", + }, + "eli-drewry": { + name: "Eli Drewry", + image: images["team/leadership/2027/eli-drewry.jpg"].src, + }, + "matthew-chen": { + name: "Matthew Chen", + image: images["team/leadership/2027/matthew-chen.jpg"].src, + }, + "isabelle-chang": { + name: "Isabelle Chang", + image: images["team/design/isabelle-chang.png"].src, + }, + "sarah-zhao": { + name: "Sarah Zhao", + image: images["team/leadership/2027/sarah-zhao.jpg"].src, + }, + "eric-chen": { + name: "Eric Chen", + image: images["team/leadership/2027/eric-chen.jpg"].src, + }, + "christina-uong": { + name: "Christina Uong", + image: images["team/leadership/2027/christina-uong.jpg"].src, + }, + "sadie-scott": { + name: "Sadie Scott", + image: images["team/leadership/2027/sadie-scott.jpg"].src, + }, + // Spelling is user-authoritative: the source photo is named "jubilee-yu". + "jubilee-yul": { + name: "Jubilee Yul", + image: images["team/leadership/2027/jubilee-yul.jpg"].src, + }, + "benjamin-garcia": { + name: "Benjamin Garcia", + image: images["team/content/benjamin-garcia.jpg"].src, + }, + "ridhima-seth": { + name: "Ridhima Seth", + image: images["team/content/ridhima-seth.png"].src, + }, + "sofia-matos": { + name: "Sofia Matos", + image: images["team/content/sofia-matos.jpg"].src, + }, + "paul-macapinlac": { + name: "Paul Macapinlac", + image: images["team/content/paul-headshot.png"].src, + }, + "joy-szeto": { + name: "Joy Szeto", + image: images["team/operations/joy-szeto-headshot.jpeg"].src, + }, + // Deliberately has no portrait; the card still renders name and role. + "allison-gao": { + name: "Allison Gao", + }, + // Deliberately has no portrait; the card still renders name and role. + "izabella-chan": { + name: "Izabella Chan", + }, + "ela-defne-erkan": { + name: "Ela Defne Erkan", + image: images["team/design/ela_defne_erkan.png"].src, + }, + "angeleena-poothavelil": { + name: "Angeleena Poothavelil", + image: images["team/operations/angeleena-poothavelil.png"].src, + }, + "aansh-singh": { + name: "Aansh Singh", + image: images["team/operations/aansh-singh.jpeg"].src, + }, + "esther-yao": { + name: "Esther Yao", + image: images["team/operations/esther-yao-headshot.jpg"].src, + }, + "shayla-kumaresan": { + name: "Shayla Kumaresan", + image: images["team/external/shayla-kumaresan-headshot.jpg"].src, + }, + "luisa-chen": { + name: "Luisa Chen", + image: images["team/operations/luisa-chen.png"].src, + }, + "kayla-wai": { + name: "Kayla Wai", + image: images["team/operations/kayla-wai.jpg"].src, + }, + "alfredo-ayala": { + name: "Alfredo Ayala", + image: images["speakers/alfredo-ayala.jpg"].src, + link: "https://www.linkedin.com/in/alfredo-ayala-b0094426/", + }, + "carey-nachenberg": { + name: "Carey Nachenberg", + image: images["speakers/carey-nachenberg.png"].src, + link: "https://www.linkedin.com/in/carey-nachenberg-14bbb03/", + }, + "jayathi-murthy": { + name: "Jayathi Murthy", + image: images["speakers/jayathi-murthy.png"].src, + link: "https://samueli.ucla.edu/jayathi-y-murthy/", + }, + "justin-brezhnev": { + name: "Justin Brezhnev", + image: images["speakers/justin-brezhnev.png"].src, + link: "https://www.linkedin.com/in/brezh/", + }, + "tyler-menezes": { + name: "Tyler Menezes", + image: images["speakers/tyler-menezes.png"].src, + link: "https://www.linkedin.com/in/tylermenezes/", + }, + "solomon-russell": { + name: "Solomon Russell", + image: images["speakers/solomon-russell.jpeg"].src, + link: "https://www.linkedin.com/in/solomon-russell-220aa014/", + }, + "ruth-johnson": { + name: "Ruth Johnson", + image: images["speakers/ruth-johnson.jpeg"].src, + link: "https://www.linkedin.com/in/ruthjohnson13/", + }, + "nick-corral": { + name: "Nick Corral", + image: images["speakers/nick-corral.jpg"].src, + link: "https://www.linkedin.com/in/nick-corral-595723106/", + }, +} satisfies Record; + +/** Every valid person reference, derived from the registry keys. */ +export type PersonId = keyof typeof people; diff --git a/src/content/sections.ts b/src/content/sections.ts new file mode 100644 index 0000000..f3f79dd --- /dev/null +++ b/src/content/sections.ts @@ -0,0 +1,22 @@ +export type HomeSectionId = + | "about" + | "our_team" + | "get-involved" + | "speakers" + | "leadership" + | "sponsors"; + +export interface HomePageSection { + readonly name: HomeSectionId; +} + +const HomePageSections = { + ABOUT: { name: "about" }, + TEAM: { name: "our_team" }, + GET_INVOLVED: { name: "get-involved" }, + SPEAKERS: { name: "speakers" }, + LEADERSHIP: { name: "leadership" }, + SPONSORS: { name: "sponsors" }, +} as const satisfies Record; + +export default HomePageSections; diff --git a/src/content/speakers.ts b/src/content/speakers.ts new file mode 100644 index 0000000..0116f4c --- /dev/null +++ b/src/content/speakers.ts @@ -0,0 +1,25 @@ +import type { PersonRole } from "./types"; + +// Titles describe the historical speaking role, not a current team assignment. +export const pastSpeakers: readonly PersonRole[] = [ + { + personId: "alfredo-ayala", + title: "Walt Disney Executive R&D Imagineer at Walt Disney Imagineering", + }, + { + personId: "carey-nachenberg", + title: "Adjunct Professor at UCLA, Principal SWE Lyft Level 5", + }, + { + personId: "jayathi-murthy", + title: "Dean, UCLA Samueli School of Engineering", + }, + { personId: "justin-brezhnev", title: "President & CEO, Hacker Fund" }, + { personId: "tyler-menezes", title: "Executive Director, CodeDay" }, + { + personId: "solomon-russell", + title: "Assistant Professor, El Camino College", + }, + { personId: "ruth-johnson", title: "Computer Science PhD Student, UCLA" }, + { personId: "nick-corral", title: "Techincal Program Manager, Oculus" }, +]; diff --git a/src/content/teams.ts b/src/content/teams.ts new file mode 100644 index 0000000..a8bbf0e --- /dev/null +++ b/src/content/teams.ts @@ -0,0 +1,118 @@ +import { people } from "./people"; +import type { PersonCard, TeamSection, TeamSectionId } from "./types"; + +/** + * Ordered team sections. Each member references a profile in `people.ts` and + * supplies the title that person holds *in this section*; the same person may + * appear in several sections with different titles. + * + * Key order is the order the tabs and cards render in. + */ +export const teamSectionsById = { + leadership: { + id: "leadership", + navLabel: "Leadership", + heading: "Leadership", + members: [ + { personId: "spoorthy-nalumachu", title: "Executive Director" }, + { personId: "clara-zhang", title: "Executive Director" }, + { personId: "annika-renganathan", title: "External Advisor" }, + { personId: "shaina-grover", title: "Content Advisor" }, + { personId: "sandra-pan", title: "Design Advisor" }, + { personId: "eli-drewry", title: "Content Director" }, + { personId: "matthew-chen", title: "Content Director" }, + { + personId: "isabelle-chang", + title: "Design Director & UI/UX Design Lead", + }, + { personId: "sarah-zhao", title: "Design Director" }, + { personId: "eric-chen", title: "Operations Director" }, + { personId: "christina-uong", title: "Operations Director" }, + { personId: "sadie-scott", title: "External Director" }, + { personId: "jubilee-yul", title: "External Director" }, + { personId: "benjamin-garcia", title: "Web Dev Lead" }, + ], + }, + content: { + id: "content", + navLabel: "Content", + heading: "Content", + members: [ + { personId: "eli-drewry", title: "Content Director" }, + { personId: "matthew-chen", title: "Content Director" }, + { personId: "ridhima-seth", title: "Content Member" }, + { personId: "sofia-matos", title: "Content Member" }, + { personId: "paul-macapinlac", title: "Content Member" }, + { personId: "joy-szeto", title: "Content Member" }, + { personId: "benjamin-garcia", title: "Content Member" }, + ], + }, + design: { + id: "design", + navLabel: "Design", + heading: "Design", + members: [ + { personId: "isabelle-chang", title: "Design Director" }, + { personId: "sarah-zhao", title: "Design Director" }, + { personId: "allison-gao", title: "Design Member" }, + { personId: "izabella-chan", title: "Design Member" }, + { personId: "ela-defne-erkan", title: "Design Member" }, + ], + }, + operations: { + id: "operations", + navLabel: "Operations", + heading: "Operations", + members: [ + { personId: "eric-chen", title: "Operations Director" }, + { personId: "christina-uong", title: "Operations Director" }, + { personId: "angeleena-poothavelil", title: "Operations Member" }, + { personId: "aansh-singh", title: "Operations Member" }, + { personId: "esther-yao", title: "Operations Member" }, + ], + }, + external: { + id: "external", + navLabel: "External", + heading: "External", + members: [ + { personId: "sadie-scott", title: "External Director" }, + { personId: "jubilee-yul", title: "External Director" }, + { personId: "shayla-kumaresan", title: "External Member" }, + { personId: "luisa-chen", title: "External Member" }, + { personId: "kayla-wai", title: "External Member" }, + ], + }, + "web-dev": { + id: "web-dev", + navLabel: "Web Dev", + heading: "Web Dev", + members: [ + { personId: "benjamin-garcia", title: "Web Dev Lead" }, + { personId: "shayla-kumaresan", title: "Web Dev Member" }, + { personId: "aansh-singh", title: "Web Dev Member" }, + { personId: "paul-macapinlac", title: "Web Dev Member" }, + { personId: "angeleena-poothavelil", title: "Web Dev Member" }, + { personId: "isabelle-chang", title: "UI/UX Design Lead" }, + { personId: "ela-defne-erkan", title: "UI/UX Designer" }, + { personId: "allison-gao", title: "UI/UX Designer" }, + ], + }, + marketing: { + id: "marketing", + navLabel: "Marketing", + heading: "Marketing", + members: [{ personId: "luisa-chen", title: "Marketing Member" }], + }, +} satisfies Record; + +export const teamSections: readonly TeamSection[] = + Object.values(teamSectionsById); + +/** Resolves a section's assignments into renderable cards, in order. */ +export function teamCards(id: TeamSectionId): readonly PersonCard[] { + return teamSectionsById[id].members.map(({ personId, title }) => ({ + ...people[personId], + title, + })); +} diff --git a/src/content/types.ts b/src/content/types.ts new file mode 100644 index 0000000..145f4e1 --- /dev/null +++ b/src/content/types.ts @@ -0,0 +1,45 @@ +import type { PersonId } from "./people"; + +export interface PersonProfile { + readonly name: string; + readonly image?: string; + readonly link?: string; + readonly descriptions?: readonly string[]; +} + +/** A rendered role combines a profile with its context-specific title. */ +export interface PersonCard extends PersonProfile { + readonly title?: string; +} + +export interface DocumentLink { + readonly name: string; + readonly src: string; +} + +export type TeamSectionId = + | "leadership" + | "content" + | "design" + | "operations" + | "external" + | "web-dev" + | "marketing"; +export interface PersonRole { + readonly personId: PersonId; + readonly title: string; +} +export interface TeamSection { + readonly id: TeamSectionId; + readonly navLabel: string; + readonly heading: string; + readonly members: readonly PersonRole[]; +} + +export interface OptimizedImage { + readonly src: string; + readonly srcSet?: string; + readonly sizes?: string; + readonly width: number; + readonly height: number; +} diff --git a/src/components/Resources2026/Resources.jsx b/src/features/events/EventPage.tsx similarity index 50% rename from src/components/Resources2026/Resources.jsx rename to src/features/events/EventPage.tsx index e07e1df..a95582a 100644 --- a/src/components/Resources2026/Resources.jsx +++ b/src/features/events/EventPage.tsx @@ -1,36 +1,44 @@ -import React from "react"; -import Button from "react-bootstrap/Button"; - -import "./Resources.scss"; -import Workshops from "./Workshops"; -// import FAQ from './FAQ'; -import Footer from "../Footer"; +import Footer from "../../components/Footer"; +import { EVENTS } from "../../content/events"; +import type { EventYear, Feedback } from "../../content/events/types"; +import { WAIVER_INTRO, WAIVER_NOTE } from "../../content/events/waivers"; +import ActionLink from "../../components/ActionLink"; +import Faq from "./Faq"; import Maps from "./Maps"; import Schedule from "./Schedule"; -// import Waivers from './Waivers'; +import Waivers from "./Waivers"; +import Workshops from "./Workshops"; -import busRoutes from "static/maps/bus_routes.jpg"; -import ackerman_union_2F from "static/maps/su_map_ack_2.jpeg"; -import ackerman_union_3F from "static/maps/su_map_ack_3.jpeg"; -// import wristbands from "static/maps/wristband_coordination.png" -// import EventProgram from "static/pdf/Event_Program.pdf" -import EventProgram from "static/pdf/Event_Program_2026.pdf" +function FeedbackButton({ feedback }: { feedback: Feedback }) { + if (feedback.kind === "link") { + return ( + + Feedback Form + + ); + } + return ( + + ); +} -function Resources() { - const maps = [ - { - src: busRoutes, - name: "Bus Routes", - }, - { - src: ackerman_union_2F, - name: "Ackerman Union 2F", - }, - { - src: ackerman_union_3F, - name: "Ackerman Union 3F", - }, - ]; +/** + * One page for every archived annual resources route; the year's differences all + * come from `src/content/events/.ts`. + */ +export default function EventPage({ year }: { year: EventYear }) { + const event = EVENTS[year]; + const workshops = ; return (
@@ -42,8 +50,8 @@ function Resources() { Welcome students and teachers! Confused about the schedule? Lost and need a map? Or maybe just curious about what workshops are happening and when? This webpage contains important resources you need for the - exploretech.la 2026 event! If you can't find what you need, feel - free to contact{" "} + exploretech.la {event.year} event! If you can't find what you need, + feel free to contact{" "}
- - {/* */} +
- - - + + {event.workshopsPlacement === "before-maps" && workshops} +

Wristbands

@@ -94,12 +90,25 @@ function Resources() { and Group 3 (red) during Session 3.
- {/* Wristband coordination */} + {event.wristbandImage && ( + {event.wristbandImage.alt} + )}
- {/* */} - {/* */} + {event.workshopsPlacement === "after-wristbands" && workshops} + {event.faq && } + {event.waivers && ( + + )}
- {/*

Wristbands

*/} + {event.feedbackHeading &&

{event.feedbackHeading}

}
@@ -111,18 +120,10 @@ function Resources() {
- {/* */} - +
); } - -Resources.displayName = "Resources"; -export default Resources; diff --git a/src/features/events/Faq.tsx b/src/features/events/Faq.tsx new file mode 100644 index 0000000..442e162 --- /dev/null +++ b/src/features/events/Faq.tsx @@ -0,0 +1,90 @@ +import { useState } from "react"; + +import type { FaqContent } from "../../content/events/types"; +import Collapse from "../../components/Collapse"; + +/** + * One accordion, two presentations: the registration page uses plain text inside + * smaller headings, the resources pages wrap every question and answer in `h5`. + */ +export default function Faq({ content }: { content: FaqContent }) { + const [openKey, setOpenKey] = useState(content.initialOpenKey); + const registration = content.kind === "registration"; + + return ( +
+
+
+

FAQs

+
+
+
+ {registration ? ( +

Student FAQs

+ ) : ( +

Student FAQs

+ )} +
+
+
+ {content.items.map((item) => ( +
+ {/* + The shipped header is a plain div with a click handler; it was never + focusable or keyboard-operable. Preserved rather than quietly changed. + */} +
+ setOpenKey((current) => + current === item.key ? null : item.key, + ) + } + > + {registration ? ( + {item.question} + ) : ( +
+ {item.question} +
+ )} +
+ +
+ {registration ? item.answer :
{item.answer}
} + {item.note &&

Note: {item.note}

} +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/features/events/Maps.tsx b/src/features/events/Maps.tsx new file mode 100644 index 0000000..51823ce --- /dev/null +++ b/src/features/events/Maps.tsx @@ -0,0 +1,37 @@ +import type { DocumentLink } from "../../content/types"; +import ActionLink from "../../components/ActionLink"; + +export default function Maps({ + maps, + linkRel, +}: { + maps: readonly DocumentLink[]; + linkRel?: string; +}) { + return ( +
+
+
+

Maps

+
+
+
+ Below are links to view all the maps you may need throughout the day! +
+
+ {maps.map((map) => ( + + {map.name} + + ))} +
+
+
+ ); +} diff --git a/src/features/events/Schedule.tsx b/src/features/events/Schedule.tsx new file mode 100644 index 0000000..23713df --- /dev/null +++ b/src/features/events/Schedule.tsx @@ -0,0 +1,28 @@ +import type { ScheduleEntry } from "../../content/events/types"; + +export default function Schedule({ + items, +}: { + items: readonly ScheduleEntry[]; +}) { + return ( +
+
+
+

Schedule

+
+
+
+

+ {items.map((entry) => ( +

+ {entry.time} + {entry.name} +

+ ))} +

+
+
+
+ ); +} diff --git a/src/features/events/Waivers.tsx b/src/features/events/Waivers.tsx new file mode 100644 index 0000000..dc4384b --- /dev/null +++ b/src/features/events/Waivers.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; + +import type { DocumentLink } from "../../content/types"; +import ActionLink from "../../components/ActionLink"; + +/** + * Shared by /resources and /register. `children` carries the extra + * "all forms" block that only the registration page shows. + */ +export default function Waivers({ + forms, + intro, + note, + children, +}: { + forms: readonly DocumentLink[]; + intro: string; + note: string; + children?: ReactNode; +}) { + return ( +
+
+

Waivers

+
+

{intro}

+

+ {note} +

+
+
+ {forms.map((form) => ( +
+ + {form.name} + +
+ ))} +
+ {children} +
+ ); +} diff --git a/src/features/events/Workshops.tsx b/src/features/events/Workshops.tsx new file mode 100644 index 0000000..da0dd1e --- /dev/null +++ b/src/features/events/Workshops.tsx @@ -0,0 +1,97 @@ +import YoutubeEmbed from "../../components/common/YoutubeEmbed"; +import type { WorkshopCard, WorkshopSection } from "../../content/events/types"; + +/** Cards have always been laid out two per deck row, in authored order. */ +function rowsOfTwo( + cards: readonly WorkshopCard[], +): readonly (readonly WorkshopCard[])[] { + const rows: WorkshopCard[][] = []; + for (let index = 0; index < cards.length; index += 2) { + rows.push(cards.slice(index, index + 2)); + } + return rows; +} + +function Card({ + card, + heading, +}: { + card: WorkshopCard; + heading: WorkshopSection["heading"]; +}) { + return ( +
+
+
+ {card.title} +
+ {card.kind === "archivedVideo" ? ( + <> + {card.subtitle && ( +
+ {card.subtitle} +
+ )} + + {card.caption && ( +

{card.caption}

+ )} + + ) : ( + card.instructors && ( +

+ {heading === "Panels" ? "Moderators" : "Instructors"}:{" "} + {card.instructors} +

+ ) + )} +
+
+ {card.kind === "described" && card.description && ( +
+

{card.description}

+
+ )} + {card.links.map((link) => ( + + ))} +
+
+ ); +} + +export default function Workshops({ + sections, +}: { + sections: readonly WorkshopSection[]; +}) { + return ( +
+ {sections.map((section) => ( +
+
+

{section.heading}

+
+
+
+ {rowsOfTwo(section.cards).map((row, index) => ( +
+ {row.map((card) => ( + + ))} +
+ ))} +
+
+ ))} +
+ ); +} diff --git a/src/features/home/Home.tsx b/src/features/home/Home.tsx new file mode 100644 index 0000000..39ec5e2 --- /dev/null +++ b/src/features/home/Home.tsx @@ -0,0 +1,19 @@ +import Footer from "../../components/Footer"; +import About from "./components/About"; +import GetInvolved from "./components/GetInvolved"; +import Hero from "./components/Hero"; +import Speakers from "./components/Speakers"; +import Sponsors from "./components/Sponsors"; + +export default function Home() { + return ( +
+ + + + + +
+
+ ); +} diff --git a/src/components/About.test.jsx b/src/features/home/components/About.test.tsx similarity index 53% rename from src/components/About.test.jsx rename to src/features/home/components/About.test.tsx index 6119505..881fac0 100644 --- a/src/components/About.test.jsx +++ b/src/features/home/components/About.test.tsx @@ -1,20 +1,32 @@ -import { vi } from "vitest"; -import React from "react"; -import ReactDOM from "react-dom"; -import { act, Simulate } from "react-dom/test-utils"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; + import About from "./About"; -let container; -let images; +let container: HTMLDivElement; +let root: Root; +let images: HTMLImageElement[]; + const activeImage = () => container.querySelector(".carousel-item.active img"); -const select = (index) => { + +const click = (element: Element | null | undefined) => { act(() => { - Simulate.click( - container.querySelectorAll(".carousel-indicators li")[index], - ); + element?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); +}; + +const select = (index: number) => { + click(container.querySelectorAll(".carousel-indicators li")[index]); +}; + +const fire = (element: HTMLImageElement, type: "load" | "error") => { + act(() => { + element.dispatchEvent(new Event(type)); }); }; + +/** Longer than the 600ms slide, so the carousel has settled on a slide. */ const settle = () => { act(() => { vi.advanceTimersByTime(700); @@ -25,12 +37,12 @@ beforeEach(() => { vi.useFakeTimers(); container = document.createElement("div"); document.body.appendChild(container); + root = createRoot(container); act(() => { - ReactDOM.render( + root.render( , - container, ); }); images = Array.from(container.querySelectorAll(".carousel-item img")); @@ -38,7 +50,7 @@ beforeEach(() => { afterEach(() => { act(() => { - ReactDOM.unmountComponentAtNode(container); + root.unmount(); }); container.remove(); vi.useRealTimers(); @@ -46,38 +58,28 @@ afterEach(() => { it("skips a failed selected image and keeps backward navigation usable", () => { act(() => { - [0, 1, 3, 4].forEach((index) => Simulate.load(images[index])); + [0, 1, 3, 4].forEach((index) => + images[index].dispatchEvent(new Event("load")), + ); }); select(2); - act(() => { - Simulate.error(images[2]); - }); + fire(images[2], "error"); settle(); expect(activeImage()).toBe(images[3]); - act(() => { - Simulate.click(container.querySelector(".carousel-control-prev")); - }); + click(container.querySelector(".carousel-control-prev")); settle(); expect(activeImage()).toBe(images[1]); }); it("does not let older image events override the newer selection", () => { - act(() => { - Simulate.load(images[0]); - }); + fire(images[0], "load"); select(1); select(3); - act(() => { - Simulate.load(images[1]); - }); + fire(images[1], "load"); settle(); expect(activeImage()).toBe(images[0]); - act(() => { - Simulate.error(images[0]); - }); - act(() => { - Simulate.load(images[3]); - }); + fire(images[0], "error"); + fire(images[3], "load"); settle(); expect(activeImage()).toBe(images[3]); }); diff --git a/src/components/About.jsx b/src/features/home/components/About.tsx similarity index 61% rename from src/components/About.jsx rename to src/features/home/components/About.tsx index 0416b7c..95c3ecc 100644 --- a/src/components/About.jsx +++ b/src/features/home/components/About.tsx @@ -1,16 +1,13 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; +import { Component } from "react"; import { Link } from "react-router-dom"; -import ReactGA from "react-ga"; - -import Carousel from "react-bootstrap/Carousel"; import { MdKeyboardArrowRight } from "react-icons/md"; -import GA from "util/GoogleAnalytics"; -import HomePageSections from "constants/HomePageSections"; -import CaourselImages from "constants/carousel"; -import images from "constants/optimizedImages"; +import GA from "../../../util/GoogleAnalytics"; +import OutboundLink from "../../../components/OutboundLink"; +import images from "../../../constants/optimizedImages"; +import HomePageSections from "../../../content/sections"; +import CarouselSlides from "../content/carousel"; +import Carousel, { type CarouselSlideItem } from "./Carousel"; const FoundingStoryLink = "https://medium.com/techatucla/exploretechla-founding-story-3bb8a947f931"; @@ -19,35 +16,25 @@ const FoundingStoryLink = const AboutImageSizes = "(min-width: 1100px) 480px, (min-width: 768px) 46vw, 100vw"; -export default class About extends Component { - static get propTypes() { - return { - className: PropTypes.string, - }; - } - - static get defaultProps() { - return { - className: "", - }; - } +interface AboutState { + readonly activeIndex: number; + readonly primed: boolean; + readonly requestedIndex: number | null; +} - constructor(props) { - super(props); +export default class About extends Component< + Record, + AboutState +> { + private readonly loadedSlides = new Set(); - this.state = { activeIndex: 0, primed: false, requestedIndex: null }; - this.loadedSlides = new Set(); - this.failedSlides = new Set(); + private readonly failedSlides = new Set(); - this._onSelect = this._onSelect.bind(this); - this._onSlideLoad = this._onSlideLoad.bind(this); - } + state: AboutState = { activeIndex: 0, primed: false, requestedIndex: null }; render() { - const classNames = classnames("Section", "About", this.props.className); - return ( -
+
@@ -78,7 +65,7 @@ export default class About extends Component {

- {this._renderCarousel({ className: "right-column" })} + {this._renderCarousel()}
-

Read about our founding story

-
+
@@ -132,36 +119,41 @@ export default class About extends Component { ); } - _onSelect(index) { - const count = CaourselImages.length; + /** + * Selection is advisory: the carousel only moves once the requested slide has + * loaded, and failed slides are stepped over in the direction of travel. + */ + private _onSelect = (index: number) => { + const count = CarouselSlides.length; if (this.failedSlides.size === count) { this.setState({ requestedIndex: null, primed: false }); return; } const previous = (this.state.activeIndex + count - 1) % count; const direction = index === previous ? -1 : 1; - while (this.failedSlides.has(index)) { - index = (index + direction + count) % count; + let target = index; + while (this.failedSlides.has(target)) { + target = (target + direction + count) % count; } - this.setState( - this.loadedSlides.has(index) - ? { activeIndex: index, requestedIndex: null } - : { requestedIndex: index } - ); - } + if (this.loadedSlides.has(target)) { + this.setState({ activeIndex: target, requestedIndex: null }); + } else { + this.setState({ requestedIndex: target }); + } + }; - _onSlideLoad(index) { + private _onSlideLoad = (index: number) => { this.failedSlides.delete(index); this.loadedSlides.add(index); this.setState((state) => ({ primed: true, - ...(state.requestedIndex === index - ? { activeIndex: index, requestedIndex: null } - : {}), + activeIndex: state.requestedIndex === index ? index : state.activeIndex, + requestedIndex: + state.requestedIndex === index ? null : state.requestedIndex, })); - } + }; - _onSlideError(index) { + private _onSlideError = (index: number) => { this.failedSlides.add(index); this.loadedSlides.delete(index); const { activeIndex, requestedIndex } = this.state; @@ -171,55 +163,47 @@ export default class About extends Component { ) { this._onSelect(index); } - } + }; // Keep the current slide visible until a requested slide has loaded. - _renderCarousel({ className }) { - const carouselItems = CaourselImages.map((img, index) => - this._renderCarouselItem(img, index) + private _renderCarousel() { + const { activeIndex, primed, requestedIndex } = this.state; + const total = CarouselSlides.length; + const items: readonly CarouselSlideItem[] = CarouselSlides.map( + (img, index) => { + const isNeighbour = + index === activeIndex || + index === (activeIndex + 1) % total || + index === (activeIndex + total - 1) % total; + return { + key: img.alt, + content: ( + {img.alt} this._onSlideLoad(index)} + onError={() => this._onSlideError(index)} + /> + ), + }; + }, ); + return ( - {carouselItems} - - ); - } - - _renderCarouselItem(img, index) { - if (!img || !img.src || !img.alt) { - return null; - } - - const { activeIndex, primed, requestedIndex } = this.state; - const total = CaourselImages.length; - const isNeighbour = - index === activeIndex || - index === (activeIndex + 1) % total || - index === (activeIndex + total - 1) % total; - - return ( - - {img.alt} this._onSlideLoad(index)} - onError={() => this._onSlideError(index)} - /> - + /> ); } } diff --git a/src/features/home/components/Carousel.tsx b/src/features/home/components/Carousel.tsx new file mode 100644 index 0000000..3fc34c6 --- /dev/null +++ b/src/features/home/components/Carousel.tsx @@ -0,0 +1,303 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, + type KeyboardEvent, + type ReactNode, + type TouchEvent, +} from "react"; + +/** + * The one carousel this site uses: the About section's photo strip. + * + * It reproduces Bootstrap 4's `.carousel.slide` markup and its class + * choreography (`carousel-item-next` -> reflow -> `carousel-item-left`), so the + * existing stylesheet drives the same 600ms slide. Slide contents are supplied + * by the caller, which also owns which slide is selected; this component owns + * only the transition, autoplay, keyboard, hover and touch behaviour. + */ + +const SWIPE_THRESHOLD = 40; + +/** dom-helpers padded its emulated transition end by 5ms; keep that slack. */ +const TRANSITION_END_PADDING_MS = 5; + +export interface CarouselSlideItem { + readonly key: string; + readonly content: ReactNode; +} + +interface CarouselProps { + readonly className: string; + readonly items: readonly CarouselSlideItem[]; + readonly activeIndex: number; + /** Autoplay delay in milliseconds, or null to leave autoplay off. */ + readonly interval: number | null; + readonly onSelect: (index: number) => void; +} + +type SlideDirection = "next" | "prev"; + +interface SlideState { + readonly from: number; + readonly to: number; + readonly direction: SlideDirection; + /** `start` positions the incoming slide; `active` runs the transition. */ + readonly phase: "start" | "active"; +} + +function useEventCallback( + fn: (...args: A) => void, +): (...args: A) => void { + const ref = useRef(fn); + useLayoutEffect(() => { + ref.current = fn; + }); + return useCallback((...args: A) => ref.current(...args), []); +} + +function transitionDuration(node: Element): number { + const value = window.getComputedStyle(node).transitionDuration || ""; + const multiplier = value.indexOf("ms") === -1 ? 1000 : 1; + return parseFloat(value) * multiplier || 0; +} + +function isVisible(element: HTMLElement | null): boolean { + const parent = element?.parentElement; + if (!element || !parent) return false; + return ( + window.getComputedStyle(element).display !== "none" && + window.getComputedStyle(element).visibility !== "hidden" && + window.getComputedStyle(parent).display !== "none" + ); +} + +export default function Carousel({ + className, + items, + activeIndex, + interval, + onSelect, +}: CarouselProps) { + const elementRef = useRef(null); + const enteringRef = useRef(null); + const pendingDirectionRef = useRef(null); + + const [renderedActiveIndex, setRenderedActiveIndex] = useState(activeIndex); + const [slide, setSlide] = useState(null); + const [pausedOnHover, setPausedOnHover] = useState(false); + const [pausedOnTouch, setPausedOnTouch] = useState(false); + + // Adopt a new selection during render so the outgoing and incoming slides + // commit together, exactly as the Bootstrap carousel did. + if (slide === null && activeIndex !== renderedActiveIndex) { + setSlide({ + from: renderedActiveIndex, + to: activeIndex, + direction: + pendingDirectionRef.current ?? + (activeIndex > renderedActiveIndex ? "next" : "prev"), + phase: "start", + }); + setRenderedActiveIndex(activeIndex); + } + + const isSliding = slide !== null; + const count = items.length; + + const prev = useCallback(() => { + if (isSliding) return; + const target = renderedActiveIndex - 1; + pendingDirectionRef.current = "prev"; + onSelect(target < 0 ? count - 1 : target); + }, [isSliding, renderedActiveIndex, onSelect, count]); + + // Stable so the autoplay interval is not torn down on every render. + const next = useEventCallback(() => { + if (isSliding) return; + const target = renderedActiveIndex + 1; + pendingDirectionRef.current = "next"; + onSelect(target >= count ? 0 : target); + }); + + const nextWhenVisible = useEventCallback(() => { + if (!document.hidden && isVisible(elementRef.current)) next(); + }); + + useLayoutEffect(() => { + if (slide === null || slide.phase !== "start") return; + pendingDirectionRef.current = null; + // Force layout between the positioning class and the moving class so the + // browser animates instead of jumping. + if (enteringRef.current) void enteringRef.current.offsetHeight; + setSlide({ ...slide, phase: "active" }); + }, [slide]); + + useEffect(() => { + if (slide === null || slide.phase !== "active") return undefined; + const node = enteringRef.current; + if (!node) { + setSlide(null); + return undefined; + } + const finish = () => setSlide(null); + const handle = window.setTimeout( + finish, + transitionDuration(node) + TRANSITION_END_PADDING_MS, + ); + node.addEventListener("transitionend", finish); + return () => { + window.clearTimeout(handle); + node.removeEventListener("transitionend", finish); + }; + }, [slide]); + + const shouldPlay = + interval !== null && !pausedOnHover && !pausedOnTouch && !isSliding; + + useEffect(() => { + if (!shouldPlay) return undefined; + const handle = window.setInterval( + document.visibilityState ? nextWhenVisible : next, + interval ?? undefined, + ); + return () => window.clearInterval(handle); + }, [shouldPlay, next, interval, nextWhenVisible]); + + const touchStartXRef = useRef(0); + const touchDeltaXRef = useRef(0); + const touchUnpauseRef = useRef(undefined); + useEffect(() => () => window.clearTimeout(touchUnpauseRef.current), []); + + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.target instanceof HTMLElement && + /input|textarea/i.test(event.target.tagName) + ) + return; + if (event.key === "ArrowLeft") { + event.preventDefault(); + prev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + next(); + } + }; + + const handleTouchStart = (event: TouchEvent) => { + touchStartXRef.current = event.touches[0].clientX; + touchDeltaXRef.current = 0; + setPausedOnTouch(true); + }; + + const handleTouchMove = (event: TouchEvent) => { + touchDeltaXRef.current = + event.touches.length > 1 + ? 0 + : event.touches[0].clientX - touchStartXRef.current; + }; + + const handleTouchEnd = () => { + const touchDeltaX = touchDeltaXRef.current; + // A tap below the swipe threshold leaves autoplay paused, as before. + if (Math.abs(touchDeltaX) <= SWIPE_THRESHOLD) return; + if (touchDeltaX > 0) prev(); + else next(); + window.clearTimeout(touchUnpauseRef.current); + touchUnpauseRef.current = window.setTimeout( + () => setPausedOnTouch(false), + interval ?? undefined, + ); + }; + + const slideClassName = (index: number): string => { + if (slide === null) { + return index === renderedActiveIndex + ? "active carousel-item" + : "carousel-item"; + } + const order = `carousel-item-${slide.direction}`; + const moving = `carousel-item-${slide.direction === "next" ? "left" : "right"}`; + if (index === slide.to) { + return slide.phase === "start" + ? `${order} carousel-item` + : `${order} ${moving} carousel-item`; + } + if (index === slide.from) { + return slide.phase === "start" + ? "active carousel-item" + : `active ${moving} carousel-item`; + } + return "carousel-item"; + }; + + return ( + + ); +} diff --git a/src/features/home/components/GetInvolved.tsx b/src/features/home/components/GetInvolved.tsx new file mode 100644 index 0000000..6a88b13 --- /dev/null +++ b/src/features/home/components/GetInvolved.tsx @@ -0,0 +1,126 @@ +import { MdKeyboardArrowRight } from "react-icons/md"; + +import OutboundLink from "../../../components/OutboundLink"; +import images from "../../../constants/optimizedImages"; +import HomePageSections from "../../../content/sections"; + +// Three cards side by side inside the 1040px section on desktop, a single +// 18rem card in the horizontal scroller below that. +const CardImageSizes = + "(min-width: 1100px) 340px, (min-width: 768px) 33vw, 18rem"; + +const MailingListSignUp = + "https://mailchi.mp/e1197feb6276/ucla-student-mailing-list"; + +export default function GetInvolved() { + return ( +
+
+
+

Get Involved

+
+
+
+
+ + +
+
+
+ For High Schools +
+

Inspire your students

+
+ +

+ Interest Form Out Soon +

+
+
+
+
+ +
+
+ + +
+
+
+ For Companies +
+

+ Become an industry partner +

+

+ Partner with us +

+
+ +

Email us

+
+
+
+
+ +
+
+ + +
+
+
+ For UCLA Students +
+

Join our team

+
+ +

+ Get our newsletter +

+

+ Sign up for our newsletter +

+
+
+
+
+ +
+
+
+
+ ); +} diff --git a/src/features/home/components/Hero.tsx b/src/features/home/components/Hero.tsx new file mode 100644 index 0000000..52ef8cd --- /dev/null +++ b/src/features/home/components/Hero.tsx @@ -0,0 +1,68 @@ +import { Link } from "react-router-dom"; + +import GA from "../../../util/GoogleAnalytics"; +import HomePageSections from "../../../content/sections"; +import LogoWithIconGroup from "../../../static/svg/logo-navy-with-icon-group-white.svg"; +import ActionLink from "../../../components/ActionLink"; + +const trackLearnMore = () => + GA.trackEvent({ category: "Hero", action: "Click", label: "Learn More" }); + +export default function Hero() { + return ( +
+ logo-with-icon-group +
+

Schedule is out now!

+
+ Check out our 2026 schedule page for the latest event details and + updates. +
+

+ Any questions or partnerships? +

+

+ Contact our executive directors Spoorthy Nalumachu and Clara Zhang:{" "} + + exploretechla@cs.ucla.edu + {" "} +

+ +
+ + GA.trackEvent({ + category: "Hero", + action: "Click", + label: "Register Today", + }) + } + > + Event Info + + { + if (event.key !== " ") return; + trackLearnMore(); + }} + > + Learn More + +
+
+
+ ); +} diff --git a/src/features/home/components/Speakers.tsx b/src/features/home/components/Speakers.tsx new file mode 100644 index 0000000..1b98003 --- /dev/null +++ b/src/features/home/components/Speakers.tsx @@ -0,0 +1,24 @@ +import People from "../../../components/common/People"; +import HomePageSections from "../../../content/sections"; +import { pastSpeakers } from "../../../content/speakers"; +import { people } from "../../../content/people"; + +export default function Speakers() { + return ( +
+
+
+

Past Speakers

+
+
+ ({ + ...people[personId], + title, + }))} + eagerCount={0} + /> +
+
+ ); +} diff --git a/src/features/home/components/Sponsors.tsx b/src/features/home/components/Sponsors.tsx new file mode 100644 index 0000000..d49ba39 --- /dev/null +++ b/src/features/home/components/Sponsors.tsx @@ -0,0 +1,63 @@ +import { MdKeyboardArrowRight } from "react-icons/md"; + +import OutboundLink from "../../../components/OutboundLink"; +import HomePageSections from "../../../content/sections"; +import { + LargeLogoSponsors, + SmallLogoSponsors, + type SponsorLogo, +} from "../content/sponsors"; + +// Sponsor logos live at the bottom of the home page, so they are all lazy; the +// two rows are rendered at different widths. +const LargeLogoSizes = "(min-width: 768px) 16rem, 14rem"; +const SmallLogoSizes = "(min-width: 768px) 12rem, 8rem"; + +function renderLogo({ name, website, ...image }: SponsorLogo, sizes: string) { + return ( +
+ + {name} + +
+ ); +} + +export default function Sponsors() { + return ( +
+
+
+

Our Sponsors

+
+
+
+ {LargeLogoSponsors.map((logo) => renderLogo(logo, LargeLogoSizes))} +
+
+ {SmallLogoSponsors.map((logo) => renderLogo(logo, SmallLogoSizes))} +
+
+
Interested in sponsoring?
+
+ +

Email Us!

+ +
+
+
+
+
+ ); +} diff --git a/src/constants/carousel.js b/src/features/home/content/carousel.ts similarity index 53% rename from src/constants/carousel.js rename to src/features/home/content/carousel.ts index f96c42f..11a9291 100644 --- a/src/constants/carousel.js +++ b/src/features/home/content/carousel.ts @@ -1,7 +1,12 @@ -import images from "./optimizedImages"; +import images from "../../../constants/optimizedImages"; +import type { OptimizedImage } from "../../../content/types"; -// Each entry spreads straight onto an : src, srcSet, sizes, width, height. -const CaourselImages = [ +/** Each slide spreads straight onto an : src, srcSet, sizes, width, height. */ +export interface CarouselSlide extends OptimizedImage { + readonly alt: string; +} + +const CarouselSlides: readonly CarouselSlide[] = [ { ...images["images/demo-microsoft-vr.jpg"], alt: "VR Demo", @@ -24,4 +29,4 @@ const CaourselImages = [ }, ]; -export default CaourselImages; +export default CarouselSlides; diff --git a/src/constants/sponsors.js b/src/features/home/content/sponsors.ts similarity index 65% rename from src/constants/sponsors.js rename to src/features/home/content/sponsors.ts index 63cc39e..e210efb 100644 --- a/src/constants/sponsors.js +++ b/src/features/home/content/sponsors.ts @@ -1,9 +1,21 @@ -import images from "./optimizedImages"; -import capcomLogo from "static/sponsors/capcom.svg"; +import images from "../../../constants/optimizedImages"; +import capcomLogo from "../../../static/sponsors/capcom.svg"; -// Raster logos spread the generated metadata (src, srcSet, width, height) so the -// browser can pick a variant and reserve the box. capcom stays vector. -export const LargeLogoSponsors = [ +/** + * Raster logos spread the generated metadata (src, srcSet, width, height) so the + * browser can pick a variant and reserve the box. capcom stays vector, so it has + * no srcSet. + */ +export interface SponsorLogo { + readonly name: string; + readonly website: string; + readonly src: string; + readonly srcSet?: string; + readonly width: number; + readonly height: number; +} + +export const LargeLogoSponsors: readonly SponsorLogo[] = [ { name: "google-logo", ...images["sponsors/google-logo.png"], @@ -16,7 +28,7 @@ export const LargeLogoSponsors = [ }, ]; -export const SmallLogoSponsors = [ +export const SmallLogoSponsors: readonly SponsorLogo[] = [ { name: "balsamiq", ...images["sponsors/balsamiq.png"], diff --git a/src/features/ignite/Ignite.tsx b/src/features/ignite/Ignite.tsx new file mode 100644 index 0000000..df4e398 --- /dev/null +++ b/src/features/ignite/Ignite.tsx @@ -0,0 +1,265 @@ +import Footer from "../../components/Footer"; +import PageNotFound from "../../components/PageNotFound"; +import images from "../../constants/optimizedImages"; + +// Workshop cards sit three-up on desktop; the capstone and apply photos are +// single large images inside their own containers. +const WorkshopImageSizes = "(min-width: 992px) 33vw, 100vw"; +const CapstoneImageSizes = "(min-width: 1000px) 900px, 90vw"; +const ApplyImageSizes = "(min-width: 992px) 50vw, 100vw"; + +const ApplicationForm = "https://forms.gle/DKUZARUPjWoxrUif6"; + +// Every image on this page is below the header section, so they are all lazy. +function lazyPhoto(key: keyof typeof images, sizes: string) { + return { + ...images[key], + sizes, + loading: "lazy", + decoding: "async", + } as const; +} + +const IGNITE_ENABLED: boolean = true; // gate + +export default function Ignite() { + if (!IGNITE_ENABLED) { + return ; + } + + return ( +
+
+
+

Ignite

+
+

+ Ignite is a 4-week series of beginner-friendly workshops offering + an in-depth exploration of current trending topics in technology. + It is designed to inspire and encourage high school students to + pursue a career in the STEM/Tech field. +

+
+
+ + +
+
+
+ +
+
+

Program Structure

+
+
+

Week 1: Foundation

+

+ Building core skills and understanding key concepts. +

+
+
+

Week 2: Development

+

+ Hands-on learning and practical application. +

+
+
+

Week 3: Integration

+

+ Advanced concepts and team collaboration. +

+
+
+

+ Week 4: +
+ Capstone +

+

+ Project development and final presentation. +

+
+
+
+
+ +
+
+

2025 Workshops

+
+
+

Introduction to Python

+

+ Dive into one of the most versatile and widely used programming + languages. This track will teach you essential Python concepts + and guide you through practical projects, perfect for beginners. +

+
+ Introduction to Python +
+
+
+

+ Game Development with Roblox +

+

+ Design and develop an exciting speed run parkour game using + Roblox Studio. From building dynamic levels to scripting game + mechanics with Lua, you'll gain hands-on experience in game + creation. +

+
+ Game Development with Roblox +
+
+
+

Web Development

+

+ Learn how websites are built using HTML and CSS. Build your + skills throughout the sessions and create your very own custom + website as a capstone project. +

+
+ Web Development +
+
+
+
+
+ +
+
+

2024 Workshops

+
+
+

Introduction to Python

+

+ Dive into one of the most versatile and widely used programming + languages. This track will teach you essential Python concepts + and guide you through practical projects, perfect for beginners. +

+
+ Introduction to Python +
+
+
+

+ Game Development with Unity +

+

+ Design and develop an exciting speed run parkour game using + Roblox Studio. From building dynamic levels to scripting game + mechanics with Lua, you'll gain hands-on experience in game + creation. +

+
+ Game Development with Unity +
+
+
+

Machine Learning

+

+ Learn how websites are built using HTML and CSS. Build your + skills throughout the sessions and create your very own custom + website as a capstone project. +

+
+ Machine Learning +
+
+
+
+
+ +
+
+

Capstone Project

+

+ The program culminates in a capstone project... +

+
+ Capstone Project Presentation +
+
+
+ +
+
+
+

Apply Now!

+

+ Applications for the next IGNITE are open... +

+ +
+ +
+
+ Students Workshop +
+
+
+
+ +
+
+ ); +} diff --git a/src/features/registration/ImportantLinks.tsx b/src/features/registration/ImportantLinks.tsx new file mode 100644 index 0000000..0529c6a --- /dev/null +++ b/src/features/registration/ImportantLinks.tsx @@ -0,0 +1,62 @@ +import type { RegistrationContent } from "../../content/events/2021"; +import ActionLink from "../../components/ActionLink"; + +export default function ImportantLinks({ + links, +}: { + links: RegistrationContent["importantLinks"]; +}) { + const { support } = links; + + return ( +
+
+

Important Links

+
+

{links.intro}

+
+
+ + {links.platform.name} + +
+
+ {links.documents.map((document) => ( +
+ + {document.name} + +
+ ))} +
+
+

{support.heading}

+
+ {support.paragraphs.map((paragraph) => ( +

{paragraph}

+ ))} +
+

{support.liveHeading}

+

{support.liveBody}

+
+ + {support.zoom.name} + +
+
+

+ {support.hoursLabel} {support.hours} +

+
+
+ ); +} diff --git a/src/features/registration/RegistrationAlert.tsx b/src/features/registration/RegistrationAlert.tsx new file mode 100644 index 0000000..e555827 --- /dev/null +++ b/src/features/registration/RegistrationAlert.tsx @@ -0,0 +1,38 @@ +import { Link } from "react-router-dom"; + +import type { RegistrationContent } from "../../content/events/2021"; +import GA from "../../util/GoogleAnalytics"; + +export default function RegistrationAlert({ + alert, +}: { + alert: RegistrationContent["alert"]; +}) { + return ( +
+

+ {alert.heading} +

+
+
{alert.note}
+
+
+
+ + GA.trackEvent({ + category: "RegistrationAlert", + action: "Click", + label: alert.trackingLabel, + }) + } + > + {alert.linkLabel} + +
+
+
+ ); +} diff --git a/src/features/registration/RegistrationPage.tsx b/src/features/registration/RegistrationPage.tsx new file mode 100644 index 0000000..96d3988 --- /dev/null +++ b/src/features/registration/RegistrationPage.tsx @@ -0,0 +1,57 @@ +import Footer from "../../components/Footer"; +import images from "../../constants/optimizedImages"; +import { REGISTRATION_2021 } from "../../content/events/2021"; +import ActionLink from "../../components/ActionLink"; +import Faq from "../events/Faq"; +import Waivers from "../events/Waivers"; +import ImportantLinks from "./ImportantLinks"; +import RegistrationAlert from "./RegistrationAlert"; +import TwoDaySchedule from "./TwoDaySchedule"; + +/** The 2021 event information page, served at /register. */ +export default function RegistrationPage() { + const content = REGISTRATION_2021; + const { waivers } = content; + + return ( +
+
+ {/* + The banner is the first thing on the page, so it stays eager and + keeps its intrinsic dimensions to reserve the space it needs. + */} + banner +
+ +
+

{content.title}

+
+ + + + +
+

+ {waivers.allFormsHeading} +

+

{waivers.allFormsBody}

+
+ + {waivers.allForms.name} + +
+
+
+
+
+ ); +} diff --git a/src/features/registration/TwoDaySchedule.tsx b/src/features/registration/TwoDaySchedule.tsx new file mode 100644 index 0000000..5208757 --- /dev/null +++ b/src/features/registration/TwoDaySchedule.tsx @@ -0,0 +1,43 @@ +import type { ScheduleDay } from "../../content/events/types"; + +function Column({ day, side }: { day: ScheduleDay; side: "left" | "right" }) { + return ( +
+
+

{day.title}

+
+
+
+

+ {day.items.map((entry) => ( +

+ {entry.time}: {entry.name} +

+ ))} +

+
+
+ ); +} + +/** The 2021 event ran over two days, so its schedule renders as two columns. */ +export default function TwoDaySchedule({ + days, +}: { + days: readonly [ScheduleDay, ScheduleDay]; +}) { + return ( +
+
+
+

Schedule

+
+
+
+ + +
+
+
+ ); +} diff --git a/src/features/team/Team.tsx b/src/features/team/Team.tsx new file mode 100644 index 0000000..29372b6 --- /dev/null +++ b/src/features/team/Team.tsx @@ -0,0 +1,120 @@ +import { Link, useLocation } from "react-router-dom"; + +import Footer from "../../components/Footer"; +import People from "../../components/common/People"; +import { teamCards, teamSections, teamSectionsById } from "../../content/teams"; +import type { TeamSectionId } from "../../content/types"; + +interface SectionLayout { + /** Second class on the
, after "Section". */ + readonly sectionClass: string; + /** Wrapper div around header + cards; absent for the newest two sections. */ + readonly containerClass?: string; + readonly headerClass: string; + readonly elementId?: string; +} + +/** + * Legacy DOM shape per section. The five original sections wrap their header + * and cards in a "-container" div and use a "-header"; Web Dev and + * Marketing were added later without a container and share a generic header + * class. Existing stylesheets select on these exact names. + */ +const sectionLayouts: Record = { + leadership: { + sectionClass: "Leadership", + containerClass: "leadership-container", + headerClass: "leadership-header", + elementId: "leadership", + }, + content: { + sectionClass: "Content", + containerClass: "content-container", + headerClass: "content-header", + }, + design: { + sectionClass: "Design", + containerClass: "design-container", + headerClass: "design-header", + }, + operations: { + sectionClass: "Operations", + containerClass: "operations-container", + headerClass: "operations-header", + }, + external: { + sectionClass: "External", + containerClass: "external-container", + headerClass: "external-header", + }, + "web-dev": { + sectionClass: "WebDev", + headerClass: "team-section-header", + }, + marketing: { + sectionClass: "Marketing", + headerClass: "team-section-header", + }, +}; + +export default function Team() { + const location = useLocation(); + + // Section comes from the URL: /our_team and /our_team/. + const pathParts = location.pathname.split("/").filter(Boolean); + const lastPart = pathParts[pathParts.length - 1]; + const activeSlug = + lastPart === "our_team" ? "leadership" : lastPart || "leadership"; + + // Unknown slugs render Leadership without marking any tab active. + const section = + teamSections.find((candidate) => candidate.id === activeSlug) ?? + teamSectionsById.leadership; + const layout = sectionLayouts[section.id]; + + const sectionBody = ( + <> +
+

{section.heading}

+
+
+ + + ); + + return ( +
+
+

Meet Our Team

+
+
+ +
+ {teamSections.map((candidate) => ( + + {candidate.navLabel} + + ))} +
+ +
+
+ {layout.containerClass ? ( +
{sectionBody}
+ ) : ( + sectionBody + )} +
+
+ +
+
+ ); +} diff --git a/src/index.jsx b/src/index.jsx deleted file mode 100644 index 0fbff3c..0000000 --- a/src/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { BrowserRouter } from "react-router-dom"; - -import App from "./App"; - -import "./index.scss"; - -ReactDOM.render( - - - , - document.getElementById("root") -); diff --git a/src/styles/app-shell.css b/src/styles/app-shell.css new file mode 100644 index 0000000..19d7cf7 --- /dev/null +++ b/src/styles/app-shell.css @@ -0,0 +1,173 @@ +/* + * Site shell: the divider, document behaviour, section geometry and the two pieces of + * chrome that wrap every route. Section padding is the one piece of layout that is exactly + * a token lookup at every breakpoint, so it is expressed with Tailwind utilities over the + * spacing tokens instead of four hand-written media blocks. + */ + +/* Shared pill divider */ +.pill-divider { + @apply h-2 w-16 mb-6 rounded-sm bg-aqua; +} + +/* Document */ +html { + scroll-behavior: smooth; +} + +/* Section geometry and scrollbars */ +.App section, +.App .Section { + @apply py-section-y px-section-xs sm:px-section-sm md:px-section-md lg:px-section-lg; +} + +.App ::-webkit-scrollbar { + @apply h-[2px]; +} + +.App ::-webkit-scrollbar-track { + background: #ebebeb; + @apply rounded-[1px]; +} + +.App ::-webkit-scrollbar-thumb { + background: var(--color-aqua); + @apply rounded-[1px]; +} + +@media (max-width: 575.98px) { + .App { + overflow-x: hidden; + } +} + +@media (min-width: 576px) { + .App ::-webkit-scrollbar { + @apply h-[4px]; + } + + .App ::-webkit-scrollbar-track { + @apply rounded-[2px]; + } + + .App ::-webkit-scrollbar-thumb { + @apply rounded-[2px]; + } +} + +@media (min-width: 768px) { + .App ::-webkit-scrollbar { + @apply hidden; + } +} + +/* Site header */ +.Header.Home { + background-color: var(--color-aqua); +} + +.Header.Section { + @apply pt-[1rem]; + @apply pb-[1rem]; +} + +.Header .site-nav-items { + @apply pt-[0.5rem]; +} + +.Header .site-nav-items .site-nav-item, +.Header .site-nav-items .site-nav-link, +.Header .site-nav-items .site-menu { + @apply ml-[0.5rem]; + @apply mr-[0.5rem]; +} + +.Header .logo-compass { + @apply block w-[2.5rem] h-[auto]; +} + +.Header .logo-with-icons { + @apply hidden max-w-[14rem] h-[auto]; +} + +@media (min-width: 576px) { + .Header .site-nav-items { + @apply pt-[0]; + } +} + +@media (min-width: 768px) { + .Header .logo-compass { + @apply hidden; + } + + .Header .logo-with-icons { + @apply block; + } +} + +/* Site footer */ +.Footer { + @apply p-[48px_0]; + color: #ffffff; + background-color: var(--color-muted-blue); +} + +.Footer .footer-inner { + @apply flex; + @apply flex-col justify-between items-center max-w-[1200px] m-[auto]; +} + +.Footer .footer-inner .footer-item { + @apply text-center; +} + +.Footer .footer-inner .footer-item:not(:last-child) { + @apply pb-[2.5rem]; +} + +.Footer .footer-inner .footer-item.logo { + @apply hidden; +} + +.Footer .footer-inner .footer-item.email p:last-child { + @apply mb-[0]; +} + +.Footer .footer-inner .footer-item .logo-with-icons { + @apply w-[14rem] h-[auto]; +} + +.Footer .footer-inner .footer-item .social-media-list { + @apply flex justify-center; +} + +.Footer .footer-inner .footer-item .social-media-list .social-media-link { + @apply m-[0_8px]; +} + +.Footer .footer-inner .footer-item a { + color: #ffffff; + text-decoration: none; +} + +.Footer .FaHeart { + fill: red; + @apply mb-[2px]; +} + +@media (min-width: 576px) { + .Footer .footer-inner { + @apply flex-row p-[0_24px]; + } + + .Footer .footer-inner .footer-item { + @apply pb-[0]; + } +} + +@media (min-width: 992px) { + .Footer .footer-inner .footer-item.logo { + @apply block; + } +} diff --git a/src/index.scss b/src/styles/base.css similarity index 56% rename from src/index.scss rename to src/styles/base.css index 8c470ff..161ff89 100644 --- a/src/index.scss +++ b/src/styles/base.css @@ -2,8 +2,8 @@ body { margin: 0; padding: 0; font-family: "Poppins", sans-serif; +} - .in-text-link { - color: #448494; - } +body .in-text-link { + color: var(--color-muted-blue); } diff --git a/src/styles/controls.css b/src/styles/controls.css new file mode 100644 index 0000000..62ee27d --- /dev/null +++ b/src/styles/controls.css @@ -0,0 +1,766 @@ +/* + * Rules in this file are derived from Bootstrap v4.5.2 (https://getbootstrap.com/), + * Copyright 2011-2020 The Bootstrap Authors and Twitter, Inc., MIT licensed + * (https://github.com/twbs/bootstrap/blob/main/LICENSE). + * + * They are the subset the shipped markup actually depended on, extracted from the compiled + * baseline rather than re-imported, so the site keeps its exact reset and control geometry + * without carrying the framework. + */ +/* Buttons */ +.action { + @apply inline-block font-normal text-[#212529] text-center align-middle select-none bg-transparent border border-solid border-transparent px-3 py-[0.375rem] text-[1rem] leading-[1.5] rounded-[0.25rem]; + transition: + color 0.15s ease-in-out, + background-color 0.15s ease-in-out, + border-color 0.15s ease-in-out, + box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .action { + transition: none; + } +} + +.action:hover { + color: #212529; + text-decoration: none; +} + +.action:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.action:disabled { + opacity: 0.65; +} + +.action:not(:disabled) { + cursor: pointer; +} + +.action-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.action-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; +} + +.action-primary:focus { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.action-primary:disabled { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} + +.action-primary:not(:disabled):active { + color: #fff; + background-color: #0062cc; + border-color: #005cbf; +} + +.action-primary:not(:disabled):active:focus { + box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5); +} + +.action-info { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.action-info:hover { + color: #fff; + background-color: #138496; + border-color: #117a8b; +} + +.action-info:focus { + color: #fff; + background-color: #138496; + border-color: #117a8b; + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.action-info:disabled { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.action-info:not(:disabled):active { + color: #fff; + background-color: #117a8b; + border-color: #10707f; +} + +.action-info:not(:disabled):active:focus { + box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5); +} + +.action-warning { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.action-warning:hover { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; +} + +.action-warning:focus { + color: #212529; + background-color: #e0a800; + border-color: #d39e00; + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.action-warning:disabled { + color: #212529; + background-color: #ffc107; + border-color: #ffc107; +} + +.action-warning:not(:disabled):active { + color: #212529; + background-color: #d39e00; + border-color: #c69500; +} + +.action-warning:not(:disabled):active:focus { + box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5); +} + +.action-outline { + color: #17a2b8; + border-color: #17a2b8; +} + +.action-outline:hover { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.action-outline:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.action-outline:disabled { + color: #17a2b8; + background-color: transparent; +} + +.action-outline:not(:disabled):active { + color: #fff; + background-color: #17a2b8; + border-color: #17a2b8; +} + +.action-outline:not(:disabled):active:focus { + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.action-inverse { + color: #f8f9fa; + border-color: #f8f9fa; +} + +.action-inverse:hover { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.action-inverse:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.action-inverse:disabled { + color: #f8f9fa; + background-color: transparent; +} + +.action-inverse:not(:disabled):active { + color: #212529; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.action-inverse:not(:disabled):active:focus { + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.action-large { + @apply p-[0.5rem_1rem] text-[1.25rem] leading-[1.5] rounded-[0.3rem]; +} + +/* Disclosure (menu and accordion) transitions */ +.disclosure:not(.show) { + @apply hidden; +} + +.disclosure-transition { + @apply relative h-[0]; + overflow: hidden; + transition: height 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .disclosure-transition { + transition: none; + } +} + +/* Dropdown */ +.site-menu { + @apply relative; +} + +.site-menu-toggle { + @apply whitespace-nowrap; +} + +.site-menu-toggle::after { + @apply inline-block ml-[0.255em]; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.site-menu-items { + @apply absolute top-[100%] left-[0] z-[1000] hidden; + float: left; + @apply min-w-[10rem] p-[0.5rem_0] m-[0.125rem_0_0] text-[1rem]; + color: #212529; + @apply text-left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + @apply rounded-[0.25rem]; +} + +.site-menu-link { + @apply block w-[100%] p-[0.25rem_1.5rem]; + clear: both; + @apply font-normal; + color: #212529; + text-align: inherit; + @apply whitespace-nowrap; + background-color: transparent; + border: 0; +} + +.site-menu-link:hover, +.site-menu-link:focus { + color: #16181b; + text-decoration: none; + background-color: #f8f9fa; +} + +.site-menu-link.active, +.site-menu-link:active { + color: #fff; + text-decoration: none; + background-color: #007bff; +} + +.site-menu-items.show { + @apply block; +} + +/* Nav */ +.site-nav-link { + @apply block p-[0.5rem_1rem]; +} + +.site-nav-link:hover, +.site-nav-link:focus { + text-decoration: none; +} + +/* Navbar */ +.site-nav { + @apply relative flex; + @apply flex-wrap items-center justify-between p-[0.5rem_1rem]; +} + +.site-brand { + @apply inline-block pt-[0.3125rem]; + @apply pb-[0.3125rem] mr-[1rem] text-[1.25rem] leading-[inherit] whitespace-nowrap; +} + +.site-brand:hover, +.site-brand:focus { + text-decoration: none; +} + +.site-nav-items { + @apply flex; + @apply flex-col pl-[0] mb-[0]; + list-style: none; +} + +.site-nav-items .site-nav-link { + @apply pr-[0]; + @apply pl-[0]; +} + +.site-nav-items .site-menu-items { + @apply static; + float: none; +} + +.site-nav-panel { + flex-basis: 100%; + @apply grow-[1] items-center; +} + +.site-nav-toggle { + @apply p-[0.25rem_0.75rem] text-[1.25rem] leading-[1]; + background-color: transparent; + border: 1px solid transparent; + @apply rounded-[0.25rem]; +} + +.site-nav-toggle:hover, +.site-nav-toggle:focus { + text-decoration: none; +} + +.site-nav-toggle-icon { + @apply inline-block w-[1.5em] h-[1.5em] align-middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; +} + +@media (min-width: 576px) { + .site-nav-responsive { + flex-flow: row nowrap; + @apply justify-start; + } + + .site-nav-responsive .site-nav-items { + @apply flex-row; + } + + .site-nav-responsive .site-nav-items .site-menu-items { + @apply absolute; + } + + .site-nav-responsive .site-nav-items .site-nav-link { + @apply pr-[0.5rem]; + @apply pl-[0.5rem]; + } + + .site-nav-responsive .site-nav-panel { + display: flex !important; + flex-basis: auto; + } + + .site-nav-responsive .site-nav-toggle { + @apply hidden; + } +} + +.site-nav-light .site-brand { + color: rgba(0, 0, 0, 0.9); +} + +.site-nav-light .site-brand:hover, +.site-nav-light .site-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.site-nav-light .site-nav-items .site-nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.site-nav-light .site-nav-items .site-nav-link:hover, +.site-nav-light .site-nav-items .site-nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.site-nav-light .site-nav-items .show > .site-nav-link, +.site-nav-light .site-nav-items .active > .site-nav-link, +.site-nav-light .site-nav-items .site-nav-link.show, +.site-nav-light .site-nav-items .site-nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.site-nav-light .site-nav-toggle { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} + +.site-nav-light .site-nav-toggle-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +/* Card */ +.content-card { + @apply relative flex; + @apply flex-col min-w-[0]; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + @apply rounded-[0.25rem]; +} + +.content-card > .resource-list { + border-top: inherit; + border-bottom: inherit; +} + +.content-card > .resource-list:first-child { + border-top-width: 0; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.content-card > .resource-list:last-child { + border-bottom-width: 0; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.content-card > .content-card-header + .resource-list { + border-top: 0; +} + +.content-card-body { + flex: 1 1 auto; + @apply min-h-[1px] p-[1.25rem]; +} + +.content-card-title { + @apply mb-[0.75rem]; +} + +.content-card-subtitle { + @apply mt-[-0.375rem]; + @apply mb-[0]; +} + +.content-card-text:last-child { + @apply mb-[0]; +} + +.content-card-link:hover { + text-decoration: none; +} + +.content-card-link + .content-card-link { + @apply ml-[1.25rem]; +} + +.content-card-header { + @apply p-[0.75rem_1.25rem] mb-[0]; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.content-card-header:first-child { + @apply rounded-[calc(0.25rem_-_1px)_calc(0.25rem_-_1px)_0_0]; +} + +.content-card-overlay { + @apply absolute top-[0] right-[0] bottom-[0] left-[0] p-[1.25rem] rounded-[calc(0.25rem_-_1px)]; +} + +.content-card-image { + @apply shrink-[0] w-[100%]; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.content-card-row .content-card { + @apply mb-[15px]; +} + +@media (min-width: 576px) { + .content-card-row { + @apply flex; + flex-flow: row wrap; + @apply mr-[-15px]; + @apply ml-[-15px]; + } + + .content-card-row .content-card { + flex: 1 0 0%; + @apply mr-[15px]; + @apply mb-[0]; + @apply ml-[15px]; + } +} + +/* Accordion */ +.accordion { + overflow-anchor: none; +} + +.accordion > .content-card { + overflow: hidden; +} + +.accordion > .content-card:not(:last-of-type) { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.accordion > .content-card:not(:first-of-type) { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.accordion > .content-card > .content-card-header { + @apply rounded-[0] mb-[-1px]; +} + +/* List group */ +.resource-list { + @apply flex; + @apply flex-col pl-[0] mb-[0] rounded-[0.25rem]; +} + +.resource-list-item { + @apply relative block p-[0.75rem_1.25rem]; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.resource-list-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.resource-list-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} + +.resource-list-item + .resource-list-item { + border-top-width: 0; +} + +.resource-list-flush { + @apply rounded-[0]; +} + +.resource-list-flush > .resource-list-item { + border-width: 0 0 1px; +} + +.resource-list-flush > .resource-list-item:last-child { + border-bottom-width: 0; +} + +/* Carousel */ +.carousel { + @apply relative; +} + +.carousel-inner { + @apply relative w-[100%]; + overflow: hidden; +} + +.carousel-inner::after { + @apply block; + clear: both; + content: ""; +} + +.carousel-item { + @apply relative hidden; + float: left; + @apply w-[100%] mr-[-100%]; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + @apply block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + transform: translateX(-100%); +} + +.carousel-control-prev, +.carousel-control-next { + @apply absolute top-[0] bottom-[0] z-[1] flex items-center justify-center w-[15%]; + color: #fff; + @apply text-center; + opacity: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, +.carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + @apply left-[0]; +} + +.carousel-control-next { + @apply right-[0]; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + @apply inline-block w-[20px] h-[20px]; + background: no-repeat 50%/100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + @apply absolute right-[0] bottom-[0] left-[0] z-[15] flex justify-center pl-[0] mr-[15%]; + @apply ml-[15%]; + list-style: none; +} + +.carousel-indicators li { + box-sizing: content-box; + flex: 0 1 auto; + @apply w-[30px] h-[3px] mr-[3px]; + @apply ml-[3px]; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-indicators li { + transition: none; + } +} + +.carousel-indicators .active { + opacity: 1; +} + +/* + * Utility classes the markup still uses and Tailwind does not reproduce identically. + * `mb-2` and `text-center` were dropped: Tailwind generates the same declarations for those + * literals. `sr-only` stays because Tailwind's version hides text with `clip-path` instead of + * `clip`, which is a different computed style from the one this site shipped. + */ +.visually-hidden { + @apply absolute w-[1px] h-[1px] p-[0] m-[-1px]; + overflow: hidden; + clip: rect(0, 0, 0, 0); + @apply whitespace-nowrap; + border: 0; +} + +.nav-align-end { + justify-content: flex-end !important; +} + +.muted-copy { + color: #6c757d !important; +} + +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + box-shadow: none !important; + } + + a:not(.action) { + text-decoration: underline; + } + + img { + page-break-inside: avoid; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + @page { + size: a3; + } + + body { + min-width: 992px !important; + } + + .site-nav { + @apply hidden; + } +} diff --git a/src/styles/events.css b/src/styles/events.css new file mode 100644 index 0000000..014be54 --- /dev/null +++ b/src/styles/events.css @@ -0,0 +1,210 @@ +/* Not found */ +.PageNotFound { + @apply flex; + @apply flex-col items-center text-center; +} + +.PageNotFound h3 { + @apply mb-[2rem]; +} + +/* Registration */ +.RegistrationAlert { + background-color: var(--color-muted-blue); + @apply flex; + @apply flex-col items-center text-center m-[auto] pt-[32px]; + @apply pb-[32px]; +} + +.RegistrationAlert h3, +.RegistrationAlert h5 { + color: white; + @apply pr-[10px]; + @apply pl-[10px]; +} + +.RegistrationAlert .buttons { + @apply pt-[10px] flex; + @apply flex-col items-center; +} + +.RegistrationAlert .buttons .button { + @apply pt-[10px]; + @apply pb-[10px]; +} + +.RegistrationAlert .notes { + @apply pt-[10px]; +} + +.Waivers { + background-color: #f5f5f5; + @apply flex; + @apply flex-col items-center m-[auto] pb-[32px]; +} + +.Waivers .waivers-title, +.Waivers .waivers-text { + @apply flex; + @apply flex-col items-center; +} + +.Waivers .buttons { + @apply pt-[20px] flex; + @apply flex-col items-center pb-[20px]; +} + +.Waivers .buttons .button { + @apply pt-[10px]; + @apply pb-[10px]; +} + +.ImportantLinks { + background-color: #f5f5f5; + @apply flex; + @apply flex-col items-center m-[auto] pb-[32px]; +} + +.ImportantLinks .links-title { + @apply flex; + @apply flex-col items-center; +} + +.ImportantLinks .links-text { + @apply flex; + @apply flex-col items-center pt-[30px]; +} + +.ImportantLinks .buttons { + @apply pt-[20px] flex; + @apply flex-col items-center pb-[20px]; +} + +.ImportantLinks .buttons .button { + @apply pt-[10px]; + @apply pb-[10px]; +} + +.FAQ { + background-color: white; +} + +.FAQ .faq-content { + @apply flex; + @apply flex-col justify-between max-w-[1200px] m-[auto]; +} + +.FAQ .faq-content:not(:last-child) { + @apply pb-[32px]; +} + +.FAQ .faq-content .faq-centered-title { + @apply flex-col items-center; +} + +.FAQ .faq-content .left-column.text, +.FAQ .faq-content .right-column.text { + @apply pt-[32px]; +} + +.FAQ .faq-student { + @apply pb-[32px]; +} + +.Schedule { + background-color: white; +} + +.Schedule .schedule-content { + @apply flex; + @apply flex-col justify-between max-w-[1200px] m-[auto]; +} + +.Schedule .schedule-content:not(:last-child) { + @apply pb-[32px]; +} + +.Schedule .schedule-content.schedule-column-container { + @apply flex-col; +} + +.Schedule .schedule-content .schedule-title { + @apply items-center text-center; +} + +.Schedule .schedule-content .left-column.text, +.Schedule .schedule-content .right-column.text { + @apply pt-[32px]; +} + +.Register .registration-title { + @apply flex; + @apply flex-col items-center max-w-[1200px] m-[auto] pt-[32px]; + @apply pb-[32px]; +} + +.Register .banner { + @apply w-[100vw] h-[auto]; +} + +/* Resources */ +.Workshops .workshops-content { + @apply flex; + @apply flex-col justify-between items-center max-w-[1200px] m-[auto]; +} + +.Workshops .workshops-content .workshops-cards { + @apply flex; + @apply flex-col w-[75vw] pb-[50px]; +} + +.Workshops .workshops-content .workshops-title { + @apply flex pb-[32px]; + @apply flex-col items-center; +} + +.Workshops .workshops-content .card-row { + @apply pt-[15px]; + @apply pb-[15px]; +} + +.Maps { + background-color: #f5f5f5; +} + +.Maps .maps-content { + @apply flex; + @apply flex-col justify-between items-center max-w-[1200px] m-[auto]; +} + +.Maps .maps-content .maps-title { + @apply flex pb-[32px]; + @apply flex-col items-center; +} + +.Maps .maps-content .buttons { + @apply pt-[20px] grid gap-y-[10px]; + grid-template-columns: auto; + @apply pb-[20px] w-[60vw]; +} + +.Resources .resources-title { + @apply flex; + @apply flex-col items-center max-w-[1200px] m-[auto] pt-[32px]; + @apply pb-[32px]; +} + +.Resources .resources-text { + @apply flex; + @apply flex-col items-center text-center pt-[30px]; +} + +.Resources .banner { + @apply w-[100vw] h-[auto]; +} + +.Resources .full-length-button { + @apply pt-[20px] grid gap-y-[10px]; + grid-template-columns: auto; + @apply pb-[20px] w-[100%]; +} diff --git a/src/styles/home.css b/src/styles/home.css new file mode 100644 index 0000000..bf10a7c --- /dev/null +++ b/src/styles/home.css @@ -0,0 +1,564 @@ +/* Hero */ +.Hero { + @apply h-[450px] flex justify-center items-center; + @apply flex-col; + background-color: var(--color-aqua); + color: var(--color-navy); +} + +.Hero .logo-with-icon-group { + @apply w-[20rem] h-[auto] mb-[2rem] pl-[24px]; + @apply pr-[24px]; +} + +.Hero .hero-content { + @apply flex; + @apply flex-col justify-center items-center text-center; +} + +.Hero .hero-content .h1-title { + @apply hidden; +} + +.Hero .hero-content .hero-cta { + @apply mt-[1rem] flex; + @apply flex-row; +} + +.Hero .hero-content .hero-cta .action:not(:last-child) { + @apply mr-[1rem]; +} + +.Hero p { + @apply m-[0]; +} + +@media (min-width: 576px) { + .Hero .logo-with-icon-group { + @apply w-[25rem]; + } + + .Hero .hero-content .h1-title { + @apply block; + } +} + +@media (min-width: 768px) { + .Hero { + @apply flex-row; + } + + .Hero .logo-with-icon-group { + @apply mr-[3rem]; + @apply mb-[0] pl-[0]; + @apply pr-[0]; + } + + .Hero .hero-content { + @apply justify-start items-start text-left max-w-[400px]; + } +} + +@media (min-width: 992px) { + .Hero .logo-with-icon-group { + @apply w-[30rem] mr-[4rem]; + } +} + +/* About */ +.About .about-content { + @apply flex justify-between items-center max-w-[1040px] m-[auto]; +} + +.About .about-content:not(:last-child) { + @apply pb-[32px]; +} + +.About .about-content.about-org { + flex-direction: column-reverse; +} + +.About .about-content.about-team { + @apply flex-col; +} + +.About .about-content .left-column.text, +.About .about-content .right-column.text { + @apply pt-[32px]; +} + +.About .about-content .founding-story a, +.About .about-content .current-team a { + @apply flex; + @apply flex-row items-center; + text-decoration: none; + color: var(--color-muted-blue); +} + +.About .about-content .founding-story a:hover .MdKeyboardArrowRight, +.About .about-content .current-team a:hover .MdKeyboardArrowRight { + fill: white; + background-color: var(--color-muted-blue); + border: 2px solid var(--color-muted-blue); +} + +.About .about-content .founding-story a p, +.About .about-content .current-team a p { + @apply mb-[0]; + @apply mr-[0.5rem]; +} + +.About .about-content img { + @apply max-w-[100%]; + @apply max-h-[100vh] h-[auto]; +} + +.About .MdKeyboardArrowRight { + fill: var(--color-muted-blue); + border: 2px solid #e2e2e2; + @apply rounded-[50%] w-[1.5rem] min-w-[1.5rem] h-[1.5rem]; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ + -moz-transition: all 0.3s ease-in-out; /** Firefox **/ + -o-transition: all 0.3s ease-in-out; /** Opera **/ +} + +@media (min-width: 768px) { + .About .about-content { + @apply justify-between; + } + + .About .about-content:not(:last-child) { + @apply pb-[64px]; + } + + .About .about-content.about-org, + .About .about-content.about-team { + @apply flex-row; + } + + .About .about-content .left-column, + .About .about-content .right-column { + @apply w-[46%]; + } + + .About .about-content .left-column.text, + .About .about-content .right-column.text { + @apply pt-[0px]; + } +} + +/* Get involved cards */ +.GetInvolved { + background-color: #f5f5f5; +} + +.GetInvolved .cards-container { + @apply m-[0_auto] max-w-[1040px]; +} + +.GetInvolved .cards-container .cards-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.GetInvolved .cards-container .content-card-row { + overflow-x: scroll; + @apply whitespace-nowrap pb-[1.5rem]; + transform: scaleX(-1) rotateZ(180deg); +} + +.GetInvolved .cards-container .content-card { + @apply inline-block w-[18rem] mb-[0]; + transform: scaleX(-1) rotateZ(180deg); +} + +@media (max-width: 575.98px) { + .GetInvolved .cards-container .content-card:not(:last-child) { + @apply mr-[0.5rem]; + } +} + +.GetInvolved .cards-container .content-card.high-schools .content-card-image { + object-position: 25% 80%; +} + +.GetInvolved .cards-container .content-card.companies .content-card-image { + object-position: 44% 50%; +} + +.GetInvolved .cards-container .content-card.ucla-students .content-card-image { + object-position: 15% 50%; +} + +.GetInvolved .cards-container .content-card .content-card-image { + @apply h-[512px] object-cover; +} + +.GetInvolved .cards-container .content-card .content-card-overlay { + @apply flex; + @apply flex-col justify-end; + color: #ffffff; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content { + @apply z-[2]; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .content-card-title { + @apply mb-[0.25rem]; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .content-card-text { + @apply mb-[0.4rem]; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .get-involved-cta { + @apply flex; + @apply flex-row items-center; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .get-involved-cta + .cta-text { + opacity: 1; + @apply mb-[0]; + @apply ml-[8px]; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ + -moz-transition: all 0.3s ease-in-out; /** Firefox **/ + -o-transition: all 0.3s ease-in-out; /** Opera **/ +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .short-content { + @apply hidden; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .full-content { + @apply block; +} + +.GetInvolved + .cards-container + .content-card + .content-card-overlay + .gradient-back { + @apply absolute bottom-[0px] left-[0px] w-[100%] h-[224px] z-[1]; + background: -moz-linear-gradient( + top, + rgba(137, 255, 241, 0) 0%, + rgba(0, 0, 0, 0.75) 100% + ); /* FF3.6+ */ + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0%, rgba(137, 255, 241, 0)), + color-stop(100%, rgba(0, 0, 0, 0.75)) + ); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient( + top, + rgba(137, 255, 241, 0) 0%, + rgba(0, 0, 0, 0.75) 100% + ); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient( + top, + rgba(137, 255, 241, 0) 0%, + rgba(0, 0, 0, 0.75) 100% + ); /* Opera 11.10+ */ + background: -ms-linear-gradient( + top, + rgba(137, 255, 241, 0) 0%, + rgba(0, 0, 0, 0.75) 100% + ); /* IE10+ */ + background: linear-gradient( + to bottom, + rgba(137, 255, 241, 0) 0%, + rgba(0, 0, 0, 0.75) 100% + ); /* W3C */ +} + +.GetInvolved .cards-container .content-card .MdKeyboardArrowRight { + fill: #ffffff; + background-color: var(--color-muted-blue); + border: 2px solid var(--color-muted-blue); + @apply rounded-[50%] w-[1.5rem] min-w-[1.5rem] h-[1.5rem]; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ + -moz-transition: all 0.3s ease-in-out; /** Firefox **/ + -o-transition: all 0.3s ease-in-out; /** Opera **/ +} + +@media (min-width: 576px) { + .GetInvolved .cards-container .content-card-row { + @apply block; + } + + .GetInvolved .cards-container .content-card:first-child { + @apply ml-[0]; + } + + .GetInvolved .cards-container .content-card:last-child { + @apply mr-[0]; + } + + .GetInvolved + .cards-container + .content-card:hover + .content-card-overlay + .card-content + .get-involved-cta + .cta-text { + opacity: 1; + } + + .GetInvolved + .cards-container + .content-card:hover + .content-card-overlay + .card-content + .get-involved-cta + .MdKeyboardArrowRight { + background-color: var(--color-muted-blue); + border: 2px solid var(--color-muted-blue); + } + + .GetInvolved .cards-container .content-card.high-schools .content-card-image { + object-position: 20% 80%; + } + + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .get-involved-cta + .cta-text { + opacity: 0; + } + + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .get-involved-cta + .MdKeyboardArrowRight { + background-color: transparent; + border: 2px solid rgba(255, 255, 255, 0.3); + } +} + +@media (min-width: 768px) { + .GetInvolved .cards-container .content-card-row { + @apply flex; + overflow-x: visible; + } + + .GetInvolved .cards-container .content-card { + @apply flex w-[100%]; + } + + .GetInvolved .cards-container .content-card.high-schools .content-card-image { + object-position: 30% 80%; + } + + .GetInvolved .cards-container .content-card.companies .content-card-image { + object-position: 40% 50%; + } + + .GetInvolved + .cards-container + .content-card.ucla-students + .content-card-image { + object-position: 12% 50%; + } + + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .short-content { + @apply block; + } + + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .full-content { + @apply hidden; + } +} + +@media (min-width: 992px) { + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .short-content { + @apply hidden; + } + + .GetInvolved + .cards-container + .content-card + .content-card-overlay + .card-content + .full-content { + @apply block; + } +} + +/* Speakers */ +.Speakers p { + @apply mb-[0.25rem]; +} + +.Speakers a { + text-decoration: none !important; +} + +.Speakers .speakers-container { + @apply max-w-[1040px] m-[auto]; +} + +.Speakers .speakers-container .speakers-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.Speakers .speakers-container:not(:last-child) { + @apply pb-[64px]; +} + +@media (min-width: 768px) { + .Speakers .speakers-container .speakers-header { + @apply mb-[0]; + } +} + +/* Sponsors */ +.Sponsors { + background-color: #f5f5f5; +} + +.Sponsors .sponsors-container { + @apply flex; + @apply flex-col justify-center items-center max-w-[1040px] m-[auto]; +} + +.Sponsors .sponsors-container .sponsors-header { + @apply flex; + @apply flex-col items-center; +} + +.Sponsors .sponsors-container .logo-container { + @apply m-[1rem]; +} + +.Sponsors .sponsors-container .large-logos, +.Sponsors .sponsors-container .small-logos { + @apply flex; + flex-flow: row wrap; + @apply justify-center items-center pb-[32px]; +} + +.Sponsors .sponsors-container .large-logos .logo { + @apply w-[14rem] h-[auto]; +} + +.Sponsors .sponsors-container .small-logos .logo { + @apply w-[8rem] h-[auto]; +} + +.Sponsors .sponsors-container .sponsor-contact { + @apply flex; + @apply flex-col justify-center items-center; +} + +.Sponsors .sponsors-container .sponsor-contact h5 { + @apply mb-[1rem]; +} + +.Sponsors .sponsors-container .sponsor-contact .sponsor-cta a { + @apply flex; + @apply flex-row items-center; + text-decoration: none; + color: var(--color-muted-blue); +} + +.Sponsors + .sponsors-container + .sponsor-contact + .sponsor-cta:hover + .MdKeyboardArrowRight { + fill: white; + background-color: var(--color-muted-blue); + border: 2px solid var(--color-muted-blue); +} + +.Sponsors .sponsors-container .sponsor-contact .sponsor-cta p { + @apply mb-[0]; + @apply mr-[0.5rem]; +} + +.Sponsors .MdKeyboardArrowRight { + fill: var(--color-muted-blue); + border: 2px solid #e2e2e2; + @apply rounded-[50%] w-[1.5rem] min-w-[1.5rem] h-[1.5rem]; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ + -moz-transition: all 0.3s ease-in-out; /** Firefox **/ + -o-transition: all 0.3s ease-in-out; /** Opera **/ +} + +@media (min-width: 768px) { + .Sponsors .sponsors-container .logo-container { + @apply m-[2rem]; + } + + .Sponsors .sponsors-container .large-logos .logo { + @apply w-[16rem]; + } + + .Sponsors .sponsors-container .small-logos .logo { + @apply w-[12rem]; + } +} diff --git a/src/styles/ignite.css b/src/styles/ignite.css new file mode 100644 index 0000000..377258f --- /dev/null +++ b/src/styles/ignite.css @@ -0,0 +1,310 @@ +/* Ignite page shell */ +.Ignite { + @apply w-[100%] min-h-[100vh]; +} + +.Ignite .ignite-section { + @apply w-[100%] p-[80px_20px] flex justify-center; +} + +.Ignite .ignite-section .content-wrapper { + @apply w-[100%] max-w-[1140px] flex; + @apply flex-col items-center text-center; +} + +/* Header */ +.Ignite .header-section { + @apply pt-[100px]; + @apply pb-[80px]; + color: white; + /* NEW — solid blue background */ + background: #2ca6bc; +} + +.Ignite .header-section .ignite-title { + @apply text-[7rem] font-bold; + letter-spacing: -2px; + @apply mb-[42px] leading-[1]; +} + +.Ignite .header-section .ignite-description { + @apply max-w-[800px] text-[1.25rem] leading-[1.5] mb-[40px] font-normal; + color: #d0ecf0; +} + +.Ignite .header-section .header-buttons { + @apply flex gap-[20px]; +} + +.Ignite .header-section .header-buttons .btn-apply { + background-color: #07556f; + border: none; + @apply p-[12px_18px] text-[1.1rem] font-normal rounded-[1rem]; +} + +.Ignite .header-section .header-buttons .btn-previous { + border: 2px solid #06546f; + color: #06546f; + @apply p-[12px_18px] text-[1.1rem]; + background: transparent; + @apply rounded-[1rem] font-normal; +} + +.Ignite .header-section .header-buttons .btn-previous:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* Program structure */ +.Ignite .structure-section { + /* NEW — blue → white gradient */ + background: linear-gradient(to bottom, #2ca6bc 0%, #ddf6f8 100%); +} + +.Ignite .structure-section .section-header { + @apply text-[3.5rem] font-bold mb-[40px]; + color: white; +} + +.Ignite .structure-section .program-cards-container { + @apply flex; + @apply flex-col gap-[20px] w-[100%]; +} + +@media (min-width: 768px) { + .Ignite .structure-section .program-cards-container { + @apply flex-row gap-[14px]; + } +} + +.Ignite .structure-section .program-card { + background: rgba(255, 255, 255, 0.2); + @apply rounded-[16px] p-[20px] flex; + @apply flex-col; + flex: 1; + @apply min-h-[0]; + backdrop-filter: blur(10px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; + opacity: 1; +} + +.Ignite .structure-section .program-card .content-card-title { + @apply text-[1.5rem] font-bold; + color: white; + @apply mb-[10px] leading-[1.2]; +} + +.Ignite .structure-section .program-card .card-description { + @apply text-[1rem]; + color: rgba(255, 255, 255, 0.9); + @apply mb-[8px] leading-[1.5]; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + @apply min-h-[3em]; +} + +.Ignite .structure-section .program-card .card-image { + @apply w-[100%] h-[240px] rounded-[12px]; + overflow: hidden; + @apply mt-[auto]; + background: rgba(0, 0, 0, 0.1); + border: 3px solid white; + @apply shrink-[0] mt-[0]; +} + +.Ignite .structure-section .program-card .card-image img { + @apply w-[100%] h-[100%] object-cover block; +} + +.Ignite .structure-section .program-cards-container:hover .program-card { + opacity: 0.6; + transform: scale(0.98); +} + +.Ignite .structure-section .program-cards-container .program-card:hover { + opacity: 1; + transform: translateY(-8px) scale(1.02); + background: rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + @apply z-[10] relative; +} + +/* Archived workshops */ +.Ignite .workshop-section { + /* NEW — solid light background */ + background: #ddf6f8; +} + +.Ignite .workshop-section .section-header { + @apply text-[3rem] font-bold mb-[40px]; + color: #06546f; +} + +.Ignite .workshop-section .workshop-cards-container { + @apply flex; + @apply flex-col gap-[20px] w-[100%]; +} + +@media (min-width: 768px) { + .Ignite .workshop-section .workshop-cards-container { + @apply flex-row gap-[14px]; + } +} + +.Ignite .workshop-section .workshop-card { + background: #1cb0c6; + @apply rounded-[16px] p-[20px] flex; + @apply flex-col; + flex: 1; + @apply min-h-[450px]; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; + opacity: 1; +} + +.Ignite .workshop-section .workshop-card .content-card-title { + @apply text-[1.5rem] font-bold; + color: white; + @apply mb-[10px] leading-[1.2]; +} + +.Ignite .workshop-section .workshop-card .card-description { + @apply text-[1rem]; + color: rgba(255, 255, 255, 0.9); + @apply mb-[8px] leading-[1.5]; +} + +.Ignite .workshop-section .workshop-card .card-image { + @apply w-[100%] h-[240px] rounded-[12px]; + overflow: hidden; + @apply mt-[auto]; + background: rgba(0, 0, 0, 0.1); + border: 3px solid white; + @apply shrink-[0] mt-[0]; +} + +.Ignite .workshop-section .workshop-card .card-image img { + @apply w-[100%] h-[100%] object-cover block; +} + +.Ignite .workshop-section .workshop-cards-container:hover .workshop-card { + opacity: 0.6; + transform: scale(0.98); +} + +.Ignite .workshop-section .workshop-cards-container .workshop-card:hover { + opacity: 1; + transform: translateY(-8px) scale(1.02); + background: #2ca6bc; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + @apply z-[10] relative; +} + +/* Capstone */ +.Ignite .capstone-section { + /* NEW — light grey background */ + background: #f5f5f5; +} + +.Ignite .capstone-section .section-header { + @apply text-[3rem] font-bold mb-[10px]; + color: #06546f; +} + +.Ignite .capstone-section .section-subtitle { + color: #6796a5; + @apply mb-[30px]; +} + +.Ignite .capstone-section .capstone-image-container { + @apply w-[90%] max-w-[900px] m-[0_auto] rounded-[16px]; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.Ignite .capstone-section .capstone-image-container img { + @apply w-[100%] h-[auto] block; +} + +/* Apply */ +.Ignite .footer-apply-section { + color: white; + @apply pt-[80px]; + @apply pb-[60px]; + /* NEW — gradient from light blue to darker teal */ + background: linear-gradient(to bottom, #a8d8e8 0%, #2ca6bc 100%); +} + +.Ignite .footer-apply-section .content-wrapper { + @apply flex-col; +} + +@media (min-width: 768px) { + .Ignite .footer-apply-section .content-wrapper { + flex-direction: row !important; + @apply items-start justify-between; + } +} + +.Ignite .footer-apply-section .split-layout { + @apply flex; + @apply flex-col gap-[40px] items-stretch; +} + +.Ignite .footer-apply-section .left-text-col { + @apply flex; + @apply flex-col justify-start items-start text-left; +} + +.Ignite .footer-apply-section .left-text-col .align-left { + @apply text-left; + @apply text-[4rem] font-bold mb-[15px] leading-[1]; + color: #ffffff; +} + +.Ignite .footer-apply-section .left-text-col .section-subtitle-small { + @apply text-[1.1rem] mt-[15px]; + @apply mb-[30px]; + color: white; +} + +.Ignite .footer-apply-section .left-text-col .btn-apply-large { + background-color: #07556f; + border: none; + @apply p-[12px_40px] text-[1.1rem] font-normal rounded-[8px] mt-[10px]; + color: white; + cursor: pointer; + @apply w-[100%]; +} + +.Ignite .footer-apply-section .right-image-col { + @apply w-[100%] flex items-center justify-center; +} + +.Ignite .footer-apply-section .right-image-col .apply-image { + @apply w-[100%] rounded-[20px]; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.Ignite .footer-apply-section .right-image-col .apply-image img { + @apply w-[100%] h-[auto] block; +} + +@media (min-width: 768px) { + .Ignite .footer-apply-section .left-text-col { + width: 50% !important; + } + + .Ignite .footer-apply-section .right-image-col { + width: 50% !important; + } + + .Ignite .footer-apply-section .btn-apply-large { + width: 100% !important; + } +} diff --git a/src/styles/reset.css b/src/styles/reset.css new file mode 100644 index 0000000..6a33777 --- /dev/null +++ b/src/styles/reset.css @@ -0,0 +1,179 @@ +/* + * Rules in this file are derived from Bootstrap v4.5.2 (https://getbootstrap.com/), + * Copyright 2011-2020 The Bootstrap Authors and Twitter, Inc., MIT licensed + * (https://github.com/twbs/bootstrap/blob/main/LICENSE). + * + * They are the subset the shipped markup actually depended on, extracted from the compiled + * baseline rather than re-imported, so the site keeps its exact reset and control geometry + * without carrying the framework. + */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, +aside, +footer, +header, +hgroup, +main, +nav, +section { + display: block; +} + +body { + margin: 0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", + Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +ol, +ul { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +b, +strong { + font-weight: bolder; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]):not([class]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +button { + border-radius: 0; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + overflow: visible; + text-transform: none; + -webkit-appearance: button; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +[role="button"] { + cursor: pointer; +} + +button:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner { + padding: 0; + border-style: none; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.heading-five, +.heading-six { + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5, +.heading-five { + font-size: 1.25rem; +} + +h6, +.heading-six { + font-size: 1rem; +} diff --git a/src/styles/shared-ui.css b/src/styles/shared-ui.css new file mode 100644 index 0000000..a6966b7 --- /dev/null +++ b/src/styles/shared-ui.css @@ -0,0 +1,136 @@ +/* Arrow link */ +.LinkWithArrow { + @apply flex; + @apply flex-row items-center; +} + +.LinkWithArrow a { + @apply flex; + @apply flex-row items-center; + text-decoration: none; +} + +.LinkWithArrow a:hover { + text-decoration: none; +} + +.LinkWithArrow a:hover .MdKeyboardArrowRight { + fill: white; + background-color: var(--color-muted-blue); + border: 2px solid var(--color-muted-blue); +} + +.LinkWithArrow a .link-text { + @apply mb-[0]; + @apply mr-[0.5rem]; + color: var(--color-muted-blue); +} + +.LinkWithArrow .MdKeyboardArrowRight { + fill: var(--color-muted-blue); + border: 2px solid #e2e2e2; + @apply rounded-[50%] w-[1.5rem] min-w-[1.5rem] h-[1.5rem]; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; /** Chrome & Safari **/ + -moz-transition: all 0.3s ease-in-out; /** Firefox **/ + -o-transition: all 0.3s ease-in-out; /** Opera **/ +} + +/* Person cards */ +.People { + @apply flex; + @apply flex-row; + overflow-x: scroll; + @apply pb-[1rem]; + transform: scaleX(-1) rotateZ(180deg); +} + +.People p { + @apply mb-[0.25rem]; +} + +.People a { + text-decoration: none !important; +} + +.People .person { + @apply w-[14rem] h-[100%] p-[1rem_0.5rem] text-center; + color: black; + transition: color 0.5s; + transform: scaleX(-1) rotateZ(180deg); +} + +.People .person .person-image { + @apply w-[10rem] h-[10rem] rounded-[50%] mb-[1.25rem] object-cover; + object-position: center; + transition: filter 0.3s ease; +} + +.People .person .person-title { + @apply text-[small]; +} + +.People .person .person-description { + @apply text-[small]; +} + +@media (min-width: 576px) { + .People .person:hover { + color: var(--color-muted-blue); + transition: color 0.5s; + } +} + +@media (min-width: 768px) { + .People { + transform: none; + @apply justify-center flex-wrap pb-[0]; + } + + .People .person { + transform: none; + } +} + +/* Click-to-load video facade */ +.video-responsive { + @apply w-[300px] max-w-[100%]; +} + +.video-responsive .video-frame { + @apply relative h-[0] pb-[56.25%]; + overflow: hidden; + background-color: #000000; +} + +.video-responsive .video-frame iframe, +.video-responsive .video-frame .video-facade { + @apply absolute left-[0] top-[0] w-[100%] h-[100%]; +} + +.video-responsive .video-facade { + @apply flex items-center justify-center p-[0]; + border: 0; + cursor: pointer; + background: radial-gradient(circle at center, #2f2f2f 0%, #000000 100%); +} + +.video-responsive .video-facade:hover .video-facade-play, +.video-responsive .video-facade:focus .video-facade-play { + background-color: #ff0000; +} + +.video-responsive .video-facade-play { + @apply relative block w-[54px] h-[38px] rounded-[10px]; + background-color: rgba(33, 33, 33, 0.85); + transition: background-color 0.2s ease-in-out; +} + +.video-responsive .video-facade-play::after { + content: ""; + @apply absolute top-[50%] left-[50%]; + transform: translate(-50%, -50%); + border-style: solid; + border-width: 9px 0 9px 15px; + border-color: transparent transparent transparent #ffffff; +} diff --git a/src/styles/team.css b/src/styles/team.css new file mode 100644 index 0000000..0e49f2c --- /dev/null +++ b/src/styles/team.css @@ -0,0 +1,131 @@ +/* Team page shell */ +.Team { + @apply flex; + @apply flex-col items-center min-h-[100vh]; + background-color: #f5f5f5; +} + +.Team .team-title { + @apply text-center w-[100%]; + background-color: white; + @apply p-[2rem_0] mb-[2rem]; +} + +.Team .team-title h1 { + @apply text-[2.5rem]; + color: #333; + @apply mb-[1rem]; +} + +.Team .team-title .pill-divider { + @apply w-[60px] h-[4px]; + background-color: var(--color-muted-blue); + @apply m-[0_auto] rounded-[2px]; +} + +.Team .team-navigation { + @apply flex gap-[1rem] mb-[2rem]; + @apply flex-wrap justify-center p-[0_1rem]; +} + +.Team .team-navigation a { + @apply p-[0.75rem_1.5rem]; + border: 2px solid var(--color-muted-blue); + background-color: transparent; + color: var(--color-muted-blue); + @apply rounded-[25px] text-[1rem] font-medium; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + @apply inline-block; +} + +.Team .team-navigation a:hover { + background-color: var(--color-muted-blue); + color: white; +} + +.Team .team-navigation a.active { + background-color: var(--color-muted-blue); + color: white; +} + +.Team .team-section-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.Team .team-content { + @apply w-[100%] max-w-[1200px] p-[0_1rem]; + flex: 1; + @apply relative; +} + +.Team > .Footer { + align-self: stretch; + @apply w-[100%]; +} + +@media (max-width: 768px) { + .Team .team-navigation a { + @apply p-[0.5rem_1rem] text-[0.9rem]; + } +} + +/* Department containers */ +.Leadership { + background-color: #f5f5f5; +} + +.Leadership .leadership-container { + @apply max-w-[1200px] m-[auto]; +} + +.Leadership .leadership-container .leadership-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.Content .content-container { + @apply max-w-[1200px] m-[auto]; +} + +.Content .content-container .content-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.Design { + background-color: #f5f5f5; +} + +.Design .design-container { + @apply max-w-[1200px] m-[auto]; +} + +.Design .design-container .design-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.Operations { + background-color: #f5f5f5; +} + +.Operations .operations-container { + @apply max-w-[1200px] m-[auto]; +} + +.Operations .operations-container .operations-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} + +.External .external-container { + @apply max-w-[1200px] m-[auto]; +} + +.External .external-container .external-header { + @apply flex; + @apply flex-col items-center mb-[1rem]; +} diff --git a/src/styles/theme.css b/src/styles/theme.css new file mode 100644 index 0000000..d48fc5a --- /dev/null +++ b/src/styles/theme.css @@ -0,0 +1,32 @@ +/* + * The single stylesheet entry for the app. Nothing else imports CSS. + * + * Layer order is declared up front so the cascade is explicit: + * theme Tailwind's design tokens plus ./tokens.css + * base the site's element-level reset and typography + * components the site's own rules, in the same order the old Sass emitted them + * utilities Tailwind utilities, which therefore win over component rules + * + * Preflight is deliberately absent. Tailwind's reset is not Bootstrap's Reboot: it makes + * images display:block, strips heading sizes, margins and list styles, and unstyles buttons. + * The site's layout depends on the shipped reset, so reset.css carries those values + * and preflight.css is never imported. Individual Tailwind stylesheet imports + * keep that choice explicit; do not add the full "tailwindcss" import. + */ +@layer theme, base, components, utilities; + +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/utilities.css" layer(utilities); + +@import "./tokens.css"; + +@import "./reset.css" layer(base); +@import "./base.css" layer(base); + +@import "./controls.css" layer(components); +@import "./app-shell.css" layer(components); +@import "./shared-ui.css" layer(components); +@import "./home.css" layer(components); +@import "./team.css" layer(components); +@import "./events.css" layer(components); +@import "./ignite.css" layer(components); diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..e057a03 --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,17 @@ +/* Observed site values, not Tailwind's default breakpoints or palette. */ +@theme { + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --color-aqua: #98d6d6; + --color-muted-blue: #448494; + --color-navy: #0b546f; + --color-teal: #57ada3; + --font-site: "Poppins", sans-serif; + --spacing-section-y: 64px; + --spacing-section-xs: 24px; + --spacing-section-sm: 40px; + --spacing-section-md: 52px; + --spacing-section-lg: 64px; +} diff --git a/src/temp.txt b/src/temp.txt deleted file mode 100644 index b6fc4c6..0000000 --- a/src/temp.txt +++ /dev/null @@ -1 +0,0 @@ -hello \ No newline at end of file diff --git a/src/test-setup.ts b/src/test-setup.ts new file mode 100644 index 0000000..8c4270c --- /dev/null +++ b/src/test-setup.ts @@ -0,0 +1,18 @@ +import { vi } from "vitest"; + +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean; +} +globalThis.IS_REACT_ACT_ENVIRONMENT = true; +// jsdom has no layout or media-query engine; browser smoke owns those contracts. +Element.prototype.scrollIntoView = vi.fn(); +window.matchMedia = (query: string): MediaQueryList => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, +}); diff --git a/src/util/GoogleAnalytics.js b/src/util/GoogleAnalytics.js deleted file mode 100644 index c66f32a..0000000 --- a/src/util/GoogleAnalytics.js +++ /dev/null @@ -1,74 +0,0 @@ -import ReactGA from "react-ga"; - -let initialized = false; -let enabled = false; - -// Queue hits immediately; let analytics.js and its connected tag load after -// rendering, on interaction, or within four seconds on a slow connection. -const scheduleVendorLoad = () => { - const events = ["pointerdown", "keydown", "touchstart"]; - let requested = false; - const load = () => { - if (requested) return; - requested = true; - window.clearTimeout(deadline); - window.removeEventListener("load", whenIdle); - events.forEach((event) => window.removeEventListener(event, load, true)); - const script = document.createElement("script"); - script.async = true; - script.src = "https://www.google-analytics.com/analytics.js"; - document.head.appendChild(script); - }; - const whenIdle = () => { - if (window.requestIdleCallback) { - window.requestIdleCallback(load, { timeout: 1500 }); - } else { - window.setTimeout(load, 1000); - } - }; - const deadline = window.setTimeout(load, 4000); - events.forEach((event) => - window.addEventListener(event, load, { capture: true, passive: true }), - ); - if (document.readyState === "complete") { - whenIdle(); - } else { - window.addEventListener("load", whenIdle, { once: true }); - } -}; - -const init = () => { - if (initialized) return enabled; - initialized = true; - const trackingId = import.meta.env.VITE_GOOGLE_ANALYTICS_TRACKING_ID; - enabled = - import.meta.env.PROD === true && Boolean(trackingId && trackingId.trim()); - if (!enabled) return false; - - if (!window.ga) { - const queue = function () { - (queue.q = queue.q || []).push(arguments); - }; - queue.l = Date.now(); - window.GoogleAnalyticsObject = "ga"; - window.ga = queue; - } - // Preserve the existing tracker and events without react-ga injecting a script. - ReactGA.initialize(trackingId, { standardImplementation: true }); - scheduleVendorLoad(); - return true; -}; - -const trackPageView = (path) => { - if (!enabled) return; - const page = path || window.location.pathname + window.location.search; - ReactGA.set({ page }); - ReactGA.pageview(page); -}; - -const trackEvent = ({ category, action, label }) => { - if (!enabled) return; - ReactGA.event({ category, action, label }); -}; - -export default { init, trackPageView, trackEvent }; diff --git a/src/util/GoogleAnalytics.test.js b/src/util/GoogleAnalytics.test.ts similarity index 59% rename from src/util/GoogleAnalytics.test.js rename to src/util/GoogleAnalytics.test.ts index 2a6dd54..26b5adc 100644 --- a/src/util/GoogleAnalytics.test.js +++ b/src/util/GoogleAnalytics.test.ts @@ -1,8 +1,14 @@ -import { vi } from "vitest"; +import { vi, type MockInstance } from "vitest"; +import type Analytics from "./GoogleAnalytics"; -let GA; -let readyState; +let GA: typeof Analytics; +let readyState: MockInstance<() => DocumentReadyState>; +function queuedCommands() { + const queue = window.ga?.q; + if (!queue) throw new Error("Analytics did not initialize its command queue"); + return queue; +} beforeEach(async () => { vi.resetModules(); vi.useFakeTimers(); @@ -32,7 +38,7 @@ it("keeps early pageviews and events in order with their original route", () => GA.trackPageView("/ignite"); expect(document.querySelectorAll("script")).toHaveLength(0); - const commands = window.ga.q.map((args) => Array.from(args)); + const commands = queuedCommands(); const routeCommands = commands.filter( ([command]) => command === "set" || command === "send", ); @@ -65,7 +71,9 @@ it("loads the vendor once when interaction beats idle and the deadline", () => { 'script[src="https://www.google-analytics.com/analytics.js"]', ), ).toHaveLength(1); - expect(window.ga.q.filter((args) => args[0] === "create")).toHaveLength(1); + expect(queuedCommands().filter((args) => args[0] === "create")).toHaveLength( + 1, + ); }); it("eventually requests analytics even when load and interaction never happen", () => { @@ -91,3 +99,52 @@ it.each(["development", "missing ID"])( expect(document.querySelectorAll("script")).toHaveLength(0); }, ); + +it("preserves title casing while redacting email labels", () => { + GA.init(); + GA.trackEvent({ + category: "Outbound", + action: "Click", + label: "founding_story", + }); + GA.trackEvent({ + category: "Outbound", + action: "Click", + label: "person@example.com", + }); + const hits = queuedCommands() + .filter(([command]) => command === "send") + .map(([, hit]) => hit); + expect(hits).toEqual([ + { + hitType: "event", + eventCategory: "Outbound", + eventAction: "Click", + eventLabel: "Founding_story", + }, + { + hitType: "event", + eventCategory: "Outbound", + eventAction: "Click", + eventLabel: "REDACTED (Potential Email Address)", + }, + ]); +}); + +it("continues outbound navigation exactly once when a late vendor callback follows the timeout", () => { + GA.init(); + const navigate = vi.fn(); + GA.trackOutbound("Profile", navigate); + const hit = queuedCommands().find(([command]) => command === "send")?.[1]; + if ( + !hit || + typeof hit !== "object" || + !("hitCallback" in hit) || + typeof hit.hitCallback !== "function" + ) { + throw new Error("Outbound event is missing its completion callback"); + } + vi.advanceTimersByTime(250); + hit.hitCallback(); + expect(navigate).toHaveBeenCalledTimes(1); +}); diff --git a/src/util/GoogleAnalytics.ts b/src/util/GoogleAnalytics.ts new file mode 100644 index 0000000..45d7aa2 --- /dev/null +++ b/src/util/GoogleAnalytics.ts @@ -0,0 +1,128 @@ +export interface AnalyticsEvent { + category: string; + action: string; + label: string; +} +interface AnalyticsQueue { + (...command: unknown[]): void; + q?: unknown[][]; + l?: number; +} +declare global { + interface Window { + ga?: AnalyticsQueue; + GoogleAnalyticsObject?: string; + } +} +let initialized = false; +let enabled = false; + +// Keep the shipped title-casing and email-redaction semantics when replacing react-ga. +// To Title Case 2.1, Copyright 2008-2013 David Gouch, MIT license. +const smallWords = + /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i; +function formatLabel(value: string): string { + const title = value + .trim() + .replace( + /[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, + (word: string, index: number, text: string) => { + if ( + index > 0 && + index + word.length !== text.length && + smallWords.test(word) && + text.charAt(index - 2) !== ":" && + (text.charAt(index + word.length) !== "-" || + text.charAt(index - 1) === "-") && + /[\s-]/.test(text.charAt(index - 1)) + ) + return word.toLowerCase(); + if (/[A-Z]|\../.test(word.slice(1))) return word; + return word.charAt(0).toUpperCase() + word.slice(1); + }, + ); + return title.includes("@") ? "REDACTED (Potential Email Address)" : title; +} + +// Queue immediately; request the vendor at idle, first interaction, or the deadline. +function scheduleVendorLoad() { + const events = ["pointerdown", "keydown", "touchstart"]; + let requested = false; + const load = () => { + if (requested) return; + requested = true; + window.clearTimeout(deadline); + window.removeEventListener("load", whenIdle); + events.forEach((event) => window.removeEventListener(event, load, true)); + const script = document.createElement("script"); + script.async = true; + script.src = "https://www.google-analytics.com/analytics.js"; + document.head.appendChild(script); + }; + const whenIdle = () => { + if (window.requestIdleCallback) + window.requestIdleCallback(load, { timeout: 1500 }); + else window.setTimeout(load, 1000); + }; + const deadline = window.setTimeout(load, 4000); + events.forEach((event) => + window.addEventListener(event, load, { capture: true, passive: true }), + ); + if (document.readyState === "complete") whenIdle(); + else window.addEventListener("load", whenIdle, { once: true }); +} +function init(): boolean { + if (initialized) return enabled; + initialized = true; + const trackingId = import.meta.env.VITE_GOOGLE_ANALYTICS_TRACKING_ID; + enabled = import.meta.env.PROD === true && Boolean(trackingId?.trim()); + if (!enabled) return false; + if (!window.ga) { + const queue: AnalyticsQueue = (...command) => { + (queue.q ??= []).push(command); + }; + queue.l = Date.now(); + window.GoogleAnalyticsObject = "ga"; + window.ga = queue; + } + window.ga("create", trackingId, "auto"); + scheduleVendorLoad(); + return true; +} +function trackPageView(path: string) { + if (!enabled) return; + const page = path || window.location.pathname + window.location.search; + window.ga?.("set", { page }); + window.ga?.("send", { hitType: "pageview", page }); +} +function trackEvent({ category, action, label }: AnalyticsEvent) { + if (!enabled) return; + window.ga?.("send", { + hitType: "event", + eventCategory: formatLabel(category), + eventAction: formatLabel(action), + eventLabel: formatLabel(label), + }); +} +function trackOutbound(label: string, onSent: () => void) { + if (!enabled) { + onSent(); + return; + } + let sent = false; + const done = () => { + if (sent) return; + sent = true; + window.clearTimeout(deadline); + onSent(); + }; + const deadline = window.setTimeout(done, 250); + window.ga?.("send", { + hitType: "event", + eventCategory: "Outbound", + eventAction: "Click", + eventLabel: formatLabel(label), + hitCallback: done, + }); +} +export default { init, trackPageView, trackEvent, trackOutbound }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..bca9d7b --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,4 @@ +/// +interface ImportMetaEnv { + readonly VITE_GOOGLE_ANALYTICS_TRACKING_ID?: string; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..df0f5a1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "types": ["vite/client", "vitest/globals", "node"], + "forceConsistentCasingInFileNames": true + }, + "include": ["src", "scripts/browser-smoke.spec.ts", "playwright.config.ts"] +} diff --git a/vite.config.mjs b/vite.config.mjs index defada6..6f9b2c6 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -1,43 +1,15 @@ -import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; import { basename, extname } from "node:path"; import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import autoprefixer from "autoprefixer"; - -const src = fileURLToPath(new URL("./src/", import.meta.url)); +import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ - plugins: [ - react({ jsxRuntime: "classic" }), - { - name: "react-16-classic-dependencies", - apply: "serve", - configResolved(config) { - // The plugin includes these even in classic mode; React 16.13 has neither. - config.optimizeDeps.include = config.optimizeDeps.include.filter( - (id) => id !== "react/jsx-runtime" && id !== "react/jsx-dev-runtime", - ); - }, - }, - ], + plugins: [react(), tailwindcss()], server: { strictPort: true }, - optimizeDeps: { - rolldownOptions: { transform: { jsx: { runtime: "classic" } } }, - }, - resolve: { - alias: [ - { - find: /^(components|constants|static|util)\//, - replacement: `${src}$1/`, - }, - ], - }, css: { postcss: { plugins: [autoprefixer()] }, - preprocessorOptions: { - scss: { loadPaths: [src] }, - }, }, build: { outDir: "build", @@ -62,6 +34,7 @@ export default defineConfig({ test: { globals: true, environment: "jsdom", - include: ["src/**/*.test.{js,jsx}"], + include: ["src/**/*.test.{ts,tsx}"], + setupFiles: ["./src/test-setup.ts"], }, });