diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbec7dd4..4f022dda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: name: Format, Typecheck, Build, and Test runs-on: ubuntu-latest timeout-minutes: 30 + env: + DEVHUB_BACKEND_URL: ${{ vars.DEVHUB_BACKEND_URL }} steps: - name: Checkout diff --git a/.gitignore b/.gitignore index d108325a..9b4d3d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ public/js/home-hero-player.js # Misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/docs/community-backend.md b/docs/community-backend.md new file mode 100644 index 00000000..ec210511 --- /dev/null +++ b/docs/community-backend.md @@ -0,0 +1,141 @@ +# Community backend integration + +> Student Fellows routes and their dynamic sitemap are deferred to the +> `feat/student-fellows` branch. The backend client and shared data contract are +> intentionally retained for that later release, but the current MVP directory +> does not call the backend. + +Backend repository: [pixel-point/devhub-backend](https://github.com/pixel-point/devhub-backend). +Follow its [Vercel setup guide](https://github.com/pixel-point/devhub-backend/blob/main/docs/vercel.md) +to deploy the API/admin and initialize Neon. In the Vercel project connected to +`pixel-point/devhub`, set `DEVHUB_BACKEND_URL` to the backend's stable HTTPS +origin for **Preview**, then redeploy this PR. Configure **Production** separately. +The API and `/headshots` must be publicly reachable; no browser CORS wildcard +or auth/database/email secrets are needed in the frontend. + +The MVP directory reads the checked-in snapshot at +`src/lib/community/data/mvps.json`. Its roster, public profile fields, images, +and links were reconciled against the official +[Databricks MVP page](https://www.databricks.com/discover/mvps), which is the +source of truth. The existing `devhub-backend` client remains available for +Student Fellows and future administrative workflows, but it is not used to +render MVPs. + +MVP portraits are checked in under `public/img/community/mvps`. Each image is a +768×768 JPEG generated from the supplied source portrait with face-aware square +cropping and conservative compression. This keeps the 352px desktop card sharp +on high-density screens without a runtime dependency on the Databricks image +host. + +Start the backend with `pnpm dev:local` on port 3001 and this website with +`pnpm dev` on port 3000. The backend's explicitly enabled local mode uses the +imported source data and keeps local edits separate from the source snapshot. + +For production or a different local endpoint, set the server-only environment +variable `DEVHUB_BACKEND_URL` to the backend origin, for example +`https://backend.example.com`. Restart the website after changing configuration. +The development default is `http://127.0.0.1:3001`. Production requires an +explicit URL. Never expose database or authentication secrets through +`NEXT_PUBLIC_` variables. + +## Public API + +- `GET /api/v1/people`: `kind=student|mvp`, `q`, `country`, `city`, `cohort`, + `expertise`, `featured`, `page`, `pageSize` (1–100, default 24). +- `GET /api/v1/people/:slug`: a published profile or 404. +- `GET /api/v1/facets?kind=student|mvp`: countries, cities, cohorts, expertise. + +The client in `src/lib/community/people.server.ts` validates response schemas +and strips undeclared fields before sending data to React. Private email, +account identifiers, and internal notes are never part of the website DTO. +Only `published` profiles are accepted. Relative `/headshots/` URLs are resolved +against the backend origin; ensure that origin and its media files are publicly +reachable over HTTPS in production. + +`additionalLinks` preserves named HTTPS destinations beyond the four primary +`links` keys, such as YouTube, Medium, and Databricks Community. Its shape is +`[{ label, url }]` (up to 20 items); the SDK defaults missing arrays to `[]` for +older responses. Cards expose these destinations through an accessible More +links menu. An MVP with only an additional link uses that destination for its +card; student profiles also include these URLs in their Connect menu and +`sameAs` metadata. Empty labels, unsafe URLs, and private extra fields are not +accepted into the rendered contract. + +## Rendering and freshness + +The MVP snapshot contains 87 official members and was verified on 2026-09-16. +All 87 matched records in the admin export. Twenty-two admin-only records were +excluded because they were absent from the official page, and the official +“Director I Author” title was used for Dr. Alan L. Dennis instead of the +conflicting backend title. Country casing and admin-only city metadata remain +normalized for the existing filters. + +`src/lib/community/mvps.ts` validates the snapshot at module load and projects +only public card/search fields into React. The base directory and all paginated +paths are generated statically at build time; malformed and out-of-range paths +return 404, and `/page/1` permanently redirects to the base route while retaining +query parameters. Neither production builds nor page requests need +`DEVHUB_BACKEND_URL` for the MVP experience. Updating the official roster +requires a deliberate snapshot refresh and redeploy. + +The preserved backend loader still batches records in groups of 100, validates +complete responses, strips private fields, and rejects inconsistent snapshots. +When the deferred Student Fellows routes are restored, they can continue using +that loader and its ISR behavior without changing the static MVP boundary. + +Search, City/Country multiselect and pagination use this local snapshot, without +API requests or Next.js server navigations. Values within a facet use OR; +different facets and search combine with AND. Filtering resets to page 1. +The dependency-free search matches all query words across name, expertise, +headline, organization, location and bio, regardless of word order. Accents, +case, apostrophes and whitespace are normalized; `C++` and `C#` remain distinct. +Exact words rank above prefixes, which match from the first character; one insertion, +deletion, replacement or adjacent transposition is allowed only when both words +have at least five characters. Results needing fewer typo corrections come first. +Exact full names get a bonus; name and expertise matches outweigh biography +matches. Ties and empty queries preserve the original directory order. City/Country +dropdown search uses the same normalization, but remains a strict substring match. +Country URLs use comma-separated names, for example +`?country=Belgium,Brazil,Canada`; older repeated `country` parameters are still +accepted. City values remain repeated parameters because city names can contain +commas. Page numbers use `/page/N`, with page 1 at the base path; for example +`/mvps/directory/page/2?q=data&country=Belgium,Brazil`. +Legacy `?page=N` links remain readable in the browser, but generated links use +path pagination. A page in the path takes precedence over a legacy query page. +Native History API updates observe both pathname and search, preserving shareable +URLs and back/forward navigation without data requests; typing replaces the +current history entry. Reloaded query URLs apply filters after hydration; the +initial static HTML is the unfiltered slice for the requested path page. +Each page path has its own title and canonical; filtered URLs canonicalize to +the same path without query parameters and share its static metadata. +Local pagination keeps the document and social titles/canonical in sync without +fetching a route, including Back/Forward. Activating a pagination link moves focus +and scroll to the directory so the newly selected cards are immediately visible. +Query-specific `X-Robots-Tag: noindex, follow` headers keep filtered URLs out of +the index without making the page dynamically rendered. +Published student detail URLs remain discoverable through the community sitemap. + +Student profiles are generated on demand on their first visit and cached for +an hour, including metadata. New slugs work without a rebuild. Public API calls +time out after eight seconds. After an hour, the next visit can receive the old +page while Next.js regenerates it in the background; this is not a scheduled job +or a hard one-hour freshness guarantee. An already open tab keeps its snapshot +until navigation/reload. No webhook or immediate invalidation is configured. + +Failed regeneration throws, retaining the last successful page rather than +caching an unavailable panel or an incomplete list as successful content. +A first-render failure uses the route error boundary. Actual detail 404s use +the website's not-found page. Urgent unpublishing needs explicit cache invalidation; +a redeploy alone may reuse the data cache. Public content can remain in cached +pages during an outage. + +`/community-sitemap.xml` lists published student detail pages at request time. +It is added to robots.txt when `DEVHUB_BACKEND_URL` is configured, and returns +a retryable 503 during backend outages. Split it into a sitemap index before +the directory exceeds 50,000 student profiles. Its public cache lasts five +minutes; its backend requests remain uncached and independent of the hourly pages. + +Backend setup, Better Auth administrator provisioning, Neon migrations, source +data provenance, and Resend configuration live in the backend repository's +documentation. A future member cabinet can link authenticated accounts to +profiles there without making the public website responsible for authentication. diff --git a/docs/community-design.md b/docs/community-design.md new file mode 100644 index 00000000..bd4749bf --- /dev/null +++ b/docs/community-design.md @@ -0,0 +1,160 @@ +# MVP and Student Fellows design implementation + +> Student Fellows implementation details below are retained for the deferred +> `feat/student-fellows` branch. The current release publishes only the MVP +> pages and links to the existing external Student Fellows site. + +Implemented 2026-09-07 using the existing DevHub Next.js, Tailwind, and shadcn components. Figma is the visual source. Prime setup was unavailable, and the parent authorized native implementation; no Prime candidate validation, export, or visual parity claim is made. + +## Routes and Figma sources + +All frames belong to `auWfvBwnxY9q6acMsE4xdd`, page `2:5` (Preview). + +| Route | Figma frame | Canvas | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `/mvps` | [12378:7437 — databricks-MVPs-1920](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12378-7437) | 1920 × 3775 | +| `/mvps/directory` | [12378:6109 — databricks-MVP-directory](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12378-6109) | 1920 × 3829 | +| `/student-fellows` | [12378:10492 — databricks-student-fellows-1920](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12378-10492) | 1920 × 3473 | +| `/student-fellows/fellows` | [12378:8397 — databricks-student-fellows](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12378-8397) | 1920 × 3779 | +| `/student-fellows/fellows/[slug]` | [12378:9655 — databricks-individual-fellow-page](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12378-9655) | 1920 × 2093 | + +The original MVP and MVP directory URLs were duplicates. The separate directory was discovered programmatically as a sibling of `12378:7437`, under section `12378:6108`. Student frames share section `12378:8396`. + +## Design evidence + +Design evidence was captured in the local project workspace at +`docs/design/community`. These QA artifacts are maintained outside this repository. + +- Full reference screenshots: `mvp.png`, `mvp-directory.png`, `student-fellows.png`, `students-list.png`, `student-profile.png`. +- Exact Plugin API measurements and visible copy: `mvp-measurements.json`, `student-fellows-measurements.json`, `student-profile-measurements.json`, `directories-measurements.json`. +- Coordinates in measurement manifests are relative to each page canvas, in pixels. They are not inferred from resized screenshots. +- Whole-page `get_design_context` responses exceeded provider size limits. Section calls recovered the MVP hero (`8376`), benefits (`7538`), requirements (`7574`), and badge (`8335`); Student hero (`11383`), campus cards (`11403`), career sections (`10500`, `10524`), and badge (`11328`); directory heading (`6112`) and card collection (`8486`); profile body (`9751`) and sidebar (`9783`). These IDs use prefix `12378:`. +- Measurement values are retained as evidence. No browser measurement manifest or machine-owned `audit.json` was produced, so these files do not certify pixel parity. + +Important desktop measurements: + +| Element | Figma geometry / type | +| ----------------------------- | ------------------------------------------------------------ | +| Standard content | x=352, width=1216 on 1920 canvas | +| Program headline | x=352, y=318, width=966; DM Sans 56/56 | +| Program badge | width=116; MVP height=135, Student height≈138 | +| Directory headline | x=192, y=224, width=1344; Inter 96/96, first-line indent=160 | +| Directory intro | x=1472, y=347, width=256; Inter 16/20 | +| Directory filters | y=675, height=44; two 160-wide selects, 437-wide search | +| Directory grid | x=352, y=779, width=1216; 4 columns, 64px gaps | +| Directory portrait | 256 × 255, displayed as a square media slot | +| Directory name / organization | Inter 20/25 and 16/20 | +| Student campus cards | 3 × 384-wide, 32px gaps, 364-high; 48px vector icons | +| Section headings | Inter 44/55, -4% tracking | +| Profile content | x=383.5, width=736; sidebar x=1215.5, width=352; 96px gap | +| Profile portrait | 352 × 390.193 | + +The implementation uses the existing `Inter`, `DM Sans`, and `Geist Mono` font setup. Directory text uses Inter; program hero text uses DM Sans. Native responsive layouts collapse grids and stack profile columns. No mobile Figma frames were supplied, so responsive checks are acceptance checks, not pixel-parity evidence. + +## Implementation decisions + +| Section | Native mode | Implementation | +| ---------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Shared header / skip link / search | Reuse | `(website)/layout.tsx` and existing Header; no duplicate navigation shell | +| Shared footer | Adapt | Added MVP and Student Fellows program/directory links to existing Community list; increased copyright and legal-link contrast | +| Program heroes | Custom composition | `program-hero.tsx`, existing Button, local Figma badges, exact Figma copy | +| MVP benefits | Custom composition | `program-benefits.tsx`, existing SectionKicker, eight content blocks | +| Student campus cards | Custom composition | `program-benefits.tsx`, three semantic cards with exact Figma SVG icons | +| MVP requirements | Custom composition | `program-requirements.tsx`, four rows and exact exported angled SVG rule | +| Student career pathways | Custom composition | `program-requirements.tsx`, two numbered sections with native lists | +| Directory headline | Custom composition | `directory-hero.tsx`; shared between the two data kinds | +| Filters / search | Adapt | Existing Popover, Checkbox, Label, InputGroup and Button; local multiselect and search with URL state and a keyboard-operable magnifier | +| People cards | Custom composition | `person-card.tsx`, local optimized copies of official photographs and profile data, local social glyphs | +| Pagination | Adapt | Existing shadcn Pagination components, real links, current-page state and ellipses | +| Student profile | Custom composition | `student-profile.tsx`, existing BackLink and SectionKicker; actual biography/highlights | +| Bottom CTA | Adapt | Existing home CTA gains optional `highlightedText`; its default remains `agentic app`. Community CTA supplies Figma titles, actions, and highlights | + +All substantial new sections live in `src/components/community/`. The route files own metadata and visible section order. Static MVP data and the preserved API fetch/schema implementation stay in the parent-owned `src/lib/community/` domain. + +Small shared changes: `src/components/home/cta.tsx`, `src/components/footer.tsx`, and `src/css/custom.css`. The added `db-paper` token preserves the Figma light surface (`#f9f7f4`) inside the site's existing dark root. No dependencies, header logic, or image optimizer configuration were changed by this frontend work. + +## Data and interaction contract + +- The MVP directory loads the checked-in, schema-validated snapshot from `src/lib/community/data/mvps.json`; its official roster and public fields were verified against the Databricks MVP page on 2026-09-16. Portraits resolve to face-centered, 768×768 compressed JPEGs in `public/img/community/mvps`. The preserved backend loader remains available for the deferred Student Fellows directory. +- City and Country are local multiselect filters in both directories. Search and pagination also use the public snapshot already loaded in the browser; changing controls makes no data request. +- The list uses 20 records per page, matching the Figma four-column/five-row layout. Pagination uses `/page/N` (page 1 uses the base directory); search, comma-separated `country` names and repeated `city` values stay in the query string. Native history keeps all controls local. Legacy `?page=N` and repeated `country` parameters remain supported. Filtering resets to the base path; filtered results clamp to the last available page, while nonexistent unfiltered page paths return 404. +- URL values are normalized locally; page is a safe integer in 1–100000, `q` is capped at 200 characters, each City/Country value at 100. Cohort and Expertise remain API capabilities and are not exposed by these Figma-aligned controls. +- Student cards navigate to `/student-fellows/fellows/[slug]`. MVP cards link to an actual published external website/social profile; no unrequested MVP detail route is created. +- The profile rejects missing records and `kind !== "student"` with `notFound()`. Highlights render only when supplied by the backend. Missing biographies, organizations, expertise, locations, and social URLs do not generate invented copy. +- Service errors propagate to a retryable route error boundary; failed ISR regeneration retains the previous successful page. Empty successful searches have their own “No matches found” state and clear-filters link. Unknown student slugs remain 404s, distinct from service outages. +- MVP directory pages are generated statically from the local snapshot. Deferred Student directory and profile routes retain hourly backend revalidation when restored. See `community-backend.md` for the complete boundary. +- Every page has a specific title, description, and canonical. Paginated MVP paths serve their corresponding cards in static HTML, with page-specific titles and canonicals. Directory query URLs share that path's static metadata, canonicalize to the path without query parameters and retain `noindex, follow` through an HTTP header; public profile pages add escaped JSON-LD using `ProfilePage`, `Person`, and educational `affiliation` (not an unsupported graduation claim). Sitemap integration is parent-owned. + +Verified CTA destinations: + +- [MVP nomination](https://surveys.training.databricks.com/jfe/form/SV_6Ed034QOD4pcQFU), linked from the existing [Databricks MVP page](https://www.databricks.com/discover/mvps). +- [Student Fellows application](https://airtable.com/appasC90KmqZ5x1t5/pag6tvR9VUG4Kf1iM/form), supplied by the source/data agent's verified program-link extraction. +- “Browse fellow profiles” navigates to the internal directory; “Apply today” opens the existing Student Fellows application form. “Fellow sign in” opens the existing [Student Fellows sign-in](https://databricksstudentfellows.com/signin). + +## Assets, accessibility, and deviations + +Local assets under `public/img/community/`: MVP and Student badge SVGs; `build.svg`, `share.svg`, `learn.svg`; LinkedIn and X SVG glyphs; `membership-rule.svg`; dropdown glyphs and the supplied `default-avatar.svg`. Badges and program illustrations use vector assets rather than raster approximations. + +Participant photos always use normalized backend `photoUrl` values. Native images specify dimensions and fixed media geometry, lazy-load directory images, and eagerly load the main profile photo. Future arbitrary admin uploads should retain server-side image size limits/optimization; the frontend does not enforce upload limits. Missing photo URLs show the supplied neutral `default-avatar.svg`, not placeholder text or an invented face. + +Known differences and product decisions: + +1. Both directories use the Figma City/Country filters. The parent added City to the backend and SDK contract. Empty facet lists are disabled unless a currently active query value must remain clearable. City values are never inferred from institutions or invented. +2. Directory names and photos in Figma are illustrative and partly duplicated between MVP and Student designs. Actual data determines displayed records, result count, wrapping, and page height. The profile Figma contains inconsistent example location/university/achievements; none are hardcoded into a person's profile. +3. Existing Header/Footer, legal/privacy links, and product navigation take precedence over obsolete Figma shell links. Four new Community links increase footer height. +4. The default CTA was extended through a prop rather than copied. CTA window-label contrast and existing footer small-text contrast were raised after browser accessibility findings. +5. There is no invented mobile reference and no claim of full visual parity. Prime remains unconfigured and no Prime Studio mutations were made. + +Keyboard/focus contracts: labelled popovers, checkboxes and search inputs; full-row checkbox labels; Gray 80 input borders and Gray 60 focus borders without a blue ring; Enter or magnifier submit; real pagination links with local activation and focus/scroll to results; current-page ARIA; named social links; decorative assets with empty alt; real person-name alt text; existing skip-to-main link. Single-line checkbox rows retain a 40px pitch. Content sections remain semantic headings, paragraphs, articles, and lists. + +## Verification and handoff + +- Frontend `pnpm typecheck`: passed after final component changes. +- Scoped Prettier formatting: completed for all owned source files. `git diff --check`: passed. +- `pnpm exec fallow dead-code`: final rerun passed with no issues after City integration. Three SDK-only findings (`personKindSchema`, `directoryQuerySchema`, `PeoplePage`) were handed to and resolved by the parent; the frontend did not mutate parent-owned schema exports. +- `pnpm exec fallow dupes`: completed, 17 existing clone groups / about 1% duplication; no groups involved `src/components/community` or the new routes. Reports were saved in `/tmp/devhub-frontend-{dead-code,dupes}.json`. +- Local SVG files parsed as XML and contain no script elements. Badge availability and the Student badge appearance were checked. +- Parent browser review found and prompted correction of numeric Tailwind line-height syntax, missing glyph exports, profile heading colors, JSON-LD educational relation, and CTA/footer contrast. Current code contains these corrections. +- Parent subsequently reported no overflow or broken images at 390px on Student/MVP program pages, MVP directory, and a student profile. Source worker reported the six baseline integration checks passed; the expanded City/Country suite is owned by that worker. +- Parent/source worker owns the final production build, repository test suite, live desktop/mobile screenshots, automated accessibility check, and `tests/e2e/community.spec.ts`. Do not infer those final results from the typecheck or design screenshots; record their actual outcomes in the overall delivery report. + +No commits, deployment, publication, Prime Studio changes, Figma edits, `.env` changes, or backend-source mutations were performed by this frontend work. + +## Workbook reconciliation — 2026-09-08 + +The supplied MVP export includes links beyond the four primary social icons. +Cards now expose named `additionalLinks` through an accessible **More links** +menu built with the existing shadcn DropdownMenu. The menu is nonmodal, stays +inside its named navigation landmark through an optional portal container, +supports keyboard navigation and restores trigger focus on Escape. It preserves the card's +existing layout and uses real source labels and HTTPS destinations. MVP cards +with only an additional link, such as a YouTube channel, use that destination +for the name and portrait link. Student profile connections and JSON-LD also +support the same optional field. + +The backend now supplies 105 MVP city values from the workbook and 127 local +photos across both programs. City filters become available from those actual +facets. No student source data changed during this reconciliation. This small +data-driven interaction extension does not establish new Figma or Prime parity +evidence. Current import and browser checks are summarized in the backend's +[verification record](https://github.com/pixel-point/devhub-backend/blob/main/docs/verification.md). + +Before publication on 2026-09-08, the changes were transferred to +`feat/community-programs` from the current `origin/main` (`07e7f51`). The final +production build, 355 unit tests across 39 files and 199 browser tests passed, +including all 12 community scenarios. Formatting, typecheck and dead-code +checks passed; existing duplication remains outside the community components. + +## Student Fellows hero update — 2026-09-15 + +Updated the hero actions from [Figma node 12547:14374](https://www.figma.com/design/auWfvBwnxY9q6acMsE4xdd/Databricks-Website-Design?node-id=12547-14374), after synchronizing `feat/community-programs` with the rebased remote branch at `de0db45`. + +The student hero now groups the profile directory and Fellow sign-in controls with “Not a fellow yet? Apply today”. The application uses the same source URL as the directory CTA and the exact exported 16px SVG arrow. Buttons are 44px tall, with desktop widths of 245px and 183px, 20px gaps between buttons and before the application prompt (12px in the stacked mobile layout). The prompt wraps as a single group only when the available width is insufficient; no viewport breakpoint forces it onto a separate row. Existing project fonts and grey tokens are retained. The supporting description moves below the actions before the desktop row becomes crowded. + +Member authentication in DevHub remains future scope. The sign-in control links to the existing Student Fellows site at `https://databricksstudentfellows.com/signin`, as confirmed by the user. + +Browser checks covered 320, 390, 768, 1024, 1280 and 1920px without horizontal overflow. The directory CTA opened the live student directory, and Apply today opened the Databricks Student Fellows Interest Form. The MVP hero retains its existing nomination/directory actions. The page retains one H1, its title/description/canonical, semantic links and a decorative arrow with empty alternative text. + +Prime setup requires an organization login and was unavailable. This update uses direct Figma context and local component patterns; no machine Prime parity or pixel-perfect claim is made. No dependencies or backend configuration were changed by the hero update. + +Validation: production build, 408 unit tests, 204 browser tests, formatting, typecheck and dead-code checks passed. Existing duplication remains outside the community components. diff --git a/next.config.mjs b/next.config.mjs index cf5f5bda..265f450e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -78,6 +78,11 @@ const nextConfig = { const latestAppkitDocs = `/docs/appkit/${appkitDocsChannel()}`; return [ + { + source: "/mvps/directory/page/1", + destination: "/mvps/directory", + permanent: true, + }, { source: "/docs", destination: "/docs/start-here", @@ -132,6 +137,13 @@ const nextConfig = { }, async headers() { return [ + ...["/mvps/directory", "/mvps/directory/page/:page"].flatMap((source) => + ["q", "city", "country", "page"].map((key) => ({ + source, + has: [{ type: "query", key }], + headers: [{ key: "X-Robots-Tag", value: "noindex, follow" }], + })), + ), { source: "/js/home-hero-player.js", headers: [ diff --git a/package.json b/package.json index 368a04f7..9c6e648c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", + "server-only": "^0.0.1", "shiki": "3.19.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57bfbbef..a4699d61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: remark-mdx: specifier: ^3.1.1 version: 3.1.1 + server-only: + specifier: ^0.0.1 + version: 0.0.1 shiki: specifier: 3.19.0 version: 3.19.0 @@ -4764,6 +4767,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -10037,6 +10043,8 @@ snapshots: transitivePeerDependencies: - supports-color + server-only@0.0.1: {} + setprototypeof@1.2.0: {} sharp@0.34.5: diff --git a/public/img/community/apply-arrow.svg b/public/img/community/apply-arrow.svg new file mode 100644 index 00000000..a687384e --- /dev/null +++ b/public/img/community/apply-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/community/arrow-right.svg b/public/img/community/arrow-right.svg new file mode 100644 index 00000000..92417c54 --- /dev/null +++ b/public/img/community/arrow-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/community/default-avatar.svg b/public/img/community/default-avatar.svg new file mode 100644 index 00000000..06bd3e66 --- /dev/null +++ b/public/img/community/default-avatar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/community/filter-check.svg b/public/img/community/filter-check.svg new file mode 100644 index 00000000..8946c55b --- /dev/null +++ b/public/img/community/filter-check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/community/filter-search.svg b/public/img/community/filter-search.svg new file mode 100644 index 00000000..344e94ae --- /dev/null +++ b/public/img/community/filter-search.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/img/community/linkedin.svg b/public/img/community/linkedin.svg new file mode 100644 index 00000000..c6dc4f60 --- /dev/null +++ b/public/img/community/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/community/membership-rule.svg b/public/img/community/membership-rule.svg new file mode 100644 index 00000000..0e951abd --- /dev/null +++ b/public/img/community/membership-rule.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/community/mvp-badge.svg b/public/img/community/mvp-badge.svg new file mode 100644 index 00000000..d3401807 --- /dev/null +++ b/public/img/community/mvp-badge.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/community/mvp-og-image.jpg b/public/img/community/mvp-og-image.jpg new file mode 100644 index 00000000..b3b69c2b Binary files /dev/null and b/public/img/community/mvp-og-image.jpg differ diff --git a/public/img/community/mvps/mvp-aarni-sillanpaa.jpg b/public/img/community/mvps/mvp-aarni-sillanpaa.jpg new file mode 100644 index 00000000..b48ca5bf Binary files /dev/null and b/public/img/community/mvps/mvp-aarni-sillanpaa.jpg differ diff --git a/public/img/community/mvps/mvp-abiola-david.jpg b/public/img/community/mvps/mvp-abiola-david.jpg new file mode 100644 index 00000000..ee12a32e Binary files /dev/null and b/public/img/community/mvps/mvp-abiola-david.jpg differ diff --git a/public/img/community/mvps/mvp-adi-polak.jpg b/public/img/community/mvps/mvp-adi-polak.jpg new file mode 100644 index 00000000..c632aa3a Binary files /dev/null and b/public/img/community/mvps/mvp-adi-polak.jpg differ diff --git a/public/img/community/mvps/mvp-ajay-kumar-pandey.jpg b/public/img/community/mvps/mvp-ajay-kumar-pandey.jpg new file mode 100644 index 00000000..c728778e Binary files /dev/null and b/public/img/community/mvps/mvp-ajay-kumar-pandey.jpg differ diff --git a/public/img/community/mvps/mvp-alex-wiss-wolferding.jpg b/public/img/community/mvps/mvp-alex-wiss-wolferding.jpg new file mode 100644 index 00000000..3455cff7 Binary files /dev/null and b/public/img/community/mvps/mvp-alex-wiss-wolferding.jpg differ diff --git a/public/img/community/mvps/mvp-andrew-sitz.jpg b/public/img/community/mvps/mvp-andrew-sitz.jpg new file mode 100644 index 00000000..4b678160 Binary files /dev/null and b/public/img/community/mvps/mvp-andrew-sitz.jpg differ diff --git a/public/img/community/mvps/mvp-ansh-lambda.jpg b/public/img/community/mvps/mvp-ansh-lambda.jpg new file mode 100644 index 00000000..a8c53254 Binary files /dev/null and b/public/img/community/mvps/mvp-ansh-lambda.jpg differ diff --git a/public/img/community/mvps/mvp-anuj-kumar-sen.jpg b/public/img/community/mvps/mvp-anuj-kumar-sen.jpg new file mode 100644 index 00000000..978e8a88 Binary files /dev/null and b/public/img/community/mvps/mvp-anuj-kumar-sen.jpg differ diff --git a/public/img/community/mvps/mvp-awadelrahman-m-a-ahmed.jpg b/public/img/community/mvps/mvp-awadelrahman-m-a-ahmed.jpg new file mode 100644 index 00000000..9fc4863f Binary files /dev/null and b/public/img/community/mvps/mvp-awadelrahman-m-a-ahmed.jpg differ diff --git a/public/img/community/mvps/mvp-bartosz-konieczny.jpg b/public/img/community/mvps/mvp-bartosz-konieczny.jpg new file mode 100644 index 00000000..8c5a5195 Binary files /dev/null and b/public/img/community/mvps/mvp-bartosz-konieczny.jpg differ diff --git a/public/img/community/mvps/mvp-behzad-nikzad.jpg b/public/img/community/mvps/mvp-behzad-nikzad.jpg new file mode 100644 index 00000000..ea4cd09d Binary files /dev/null and b/public/img/community/mvps/mvp-behzad-nikzad.jpg differ diff --git a/public/img/community/mvps/mvp-bianca-stratulat.jpg b/public/img/community/mvps/mvp-bianca-stratulat.jpg new file mode 100644 index 00000000..030f303b Binary files /dev/null and b/public/img/community/mvps/mvp-bianca-stratulat.jpg differ diff --git a/public/img/community/mvps/mvp-casper-lubbers.jpg b/public/img/community/mvps/mvp-casper-lubbers.jpg new file mode 100644 index 00000000..d83228f7 Binary files /dev/null and b/public/img/community/mvps/mvp-casper-lubbers.jpg differ diff --git a/public/img/community/mvps/mvp-chalapathi-rao-komirisetti.jpg b/public/img/community/mvps/mvp-chalapathi-rao-komirisetti.jpg new file mode 100644 index 00000000..0b0f8a04 Binary files /dev/null and b/public/img/community/mvps/mvp-chalapathi-rao-komirisetti.jpg differ diff --git a/public/img/community/mvps/mvp-dan-williams.jpg b/public/img/community/mvps/mvp-dan-williams.jpg new file mode 100644 index 00000000..12bdedfc Binary files /dev/null and b/public/img/community/mvps/mvp-dan-williams.jpg differ diff --git a/public/img/community/mvps/mvp-daniel-sahal.jpg b/public/img/community/mvps/mvp-daniel-sahal.jpg new file mode 100644 index 00000000..21e0d21f Binary files /dev/null and b/public/img/community/mvps/mvp-daniel-sahal.jpg differ diff --git a/public/img/community/mvps/mvp-derar-alhussein.jpg b/public/img/community/mvps/mvp-derar-alhussein.jpg new file mode 100644 index 00000000..2f0ebcde Binary files /dev/null and b/public/img/community/mvps/mvp-derar-alhussein.jpg differ diff --git a/public/img/community/mvps/mvp-dilorom-abdullah.jpg b/public/img/community/mvps/mvp-dilorom-abdullah.jpg new file mode 100644 index 00000000..61960b4a Binary files /dev/null and b/public/img/community/mvps/mvp-dilorom-abdullah.jpg differ diff --git a/public/img/community/mvps/mvp-dip-kharod.jpg b/public/img/community/mvps/mvp-dip-kharod.jpg new file mode 100644 index 00000000..ecf8c7c1 Binary files /dev/null and b/public/img/community/mvps/mvp-dip-kharod.jpg differ diff --git a/public/img/community/mvps/mvp-domonkos-pal.jpg b/public/img/community/mvps/mvp-domonkos-pal.jpg new file mode 100644 index 00000000..1b192508 Binary files /dev/null and b/public/img/community/mvps/mvp-domonkos-pal.jpg differ diff --git a/public/img/community/mvps/mvp-doug-macwilliams.jpg b/public/img/community/mvps/mvp-doug-macwilliams.jpg new file mode 100644 index 00000000..c52de0d3 Binary files /dev/null and b/public/img/community/mvps/mvp-doug-macwilliams.jpg differ diff --git a/public/img/community/mvps/mvp-dr-alan-l-dennis.jpg b/public/img/community/mvps/mvp-dr-alan-l-dennis.jpg new file mode 100644 index 00000000..3d74e388 Binary files /dev/null and b/public/img/community/mvps/mvp-dr-alan-l-dennis.jpg differ diff --git a/public/img/community/mvps/mvp-dylan-ford.jpg b/public/img/community/mvps/mvp-dylan-ford.jpg new file mode 100644 index 00000000..fb395dff Binary files /dev/null and b/public/img/community/mvps/mvp-dylan-ford.jpg differ diff --git a/public/img/community/mvps/mvp-eddie-edgeworth.jpg b/public/img/community/mvps/mvp-eddie-edgeworth.jpg new file mode 100644 index 00000000..f2e1ce5a Binary files /dev/null and b/public/img/community/mvps/mvp-eddie-edgeworth.jpg differ diff --git a/public/img/community/mvps/mvp-elena-boiarskaia.jpg b/public/img/community/mvps/mvp-elena-boiarskaia.jpg new file mode 100644 index 00000000..59e2f8ca Binary files /dev/null and b/public/img/community/mvps/mvp-elena-boiarskaia.jpg differ diff --git a/public/img/community/mvps/mvp-eloisa-elias-t.jpg b/public/img/community/mvps/mvp-eloisa-elias-t.jpg new file mode 100644 index 00000000..1f0c398d Binary files /dev/null and b/public/img/community/mvps/mvp-eloisa-elias-t.jpg differ diff --git a/public/img/community/mvps/mvp-gary-nakanelua.jpg b/public/img/community/mvps/mvp-gary-nakanelua.jpg new file mode 100644 index 00000000..58b93e77 Binary files /dev/null and b/public/img/community/mvps/mvp-gary-nakanelua.jpg differ diff --git a/public/img/community/mvps/mvp-gavita-regunath.jpg b/public/img/community/mvps/mvp-gavita-regunath.jpg new file mode 100644 index 00000000..380f5cd3 Binary files /dev/null and b/public/img/community/mvps/mvp-gavita-regunath.jpg differ diff --git a/public/img/community/mvps/mvp-geir-alstad.jpg b/public/img/community/mvps/mvp-geir-alstad.jpg new file mode 100644 index 00000000..25bf8087 Binary files /dev/null and b/public/img/community/mvps/mvp-geir-alstad.jpg differ diff --git a/public/img/community/mvps/mvp-geoffrey-freeman.jpg b/public/img/community/mvps/mvp-geoffrey-freeman.jpg new file mode 100644 index 00000000..ff331231 Binary files /dev/null and b/public/img/community/mvps/mvp-geoffrey-freeman.jpg differ diff --git a/public/img/community/mvps/mvp-hubert-dudek.jpg b/public/img/community/mvps/mvp-hubert-dudek.jpg new file mode 100644 index 00000000..5fe7418b Binary files /dev/null and b/public/img/community/mvps/mvp-hubert-dudek.jpg differ diff --git a/public/img/community/mvps/mvp-ike-ellis.jpg b/public/img/community/mvps/mvp-ike-ellis.jpg new file mode 100644 index 00000000..13f778f8 Binary files /dev/null and b/public/img/community/mvps/mvp-ike-ellis.jpg differ diff --git a/public/img/community/mvps/mvp-jacek-laskowski.jpg b/public/img/community/mvps/mvp-jacek-laskowski.jpg new file mode 100644 index 00000000..1eddc2fd Binary files /dev/null and b/public/img/community/mvps/mvp-jacek-laskowski.jpg differ diff --git a/public/img/community/mvps/mvp-jaco-van-gelder.jpg b/public/img/community/mvps/mvp-jaco-van-gelder.jpg new file mode 100644 index 00000000..bdf6997c Binary files /dev/null and b/public/img/community/mvps/mvp-jaco-van-gelder.jpg differ diff --git a/public/img/community/mvps/mvp-jake-duckers.jpg b/public/img/community/mvps/mvp-jake-duckers.jpg new file mode 100644 index 00000000..bbe3c32f Binary files /dev/null and b/public/img/community/mvps/mvp-jake-duckers.jpg differ diff --git a/public/img/community/mvps/mvp-jason-yip.jpg b/public/img/community/mvps/mvp-jason-yip.jpg new file mode 100644 index 00000000..7f92d0ac Binary files /dev/null and b/public/img/community/mvps/mvp-jason-yip.jpg differ diff --git a/public/img/community/mvps/mvp-jonathan-rioux.jpg b/public/img/community/mvps/mvp-jonathan-rioux.jpg new file mode 100644 index 00000000..261c1482 Binary files /dev/null and b/public/img/community/mvps/mvp-jonathan-rioux.jpg differ diff --git a/public/img/community/mvps/mvp-josh-adams.jpg b/public/img/community/mvps/mvp-josh-adams.jpg new file mode 100644 index 00000000..6bff1abe Binary files /dev/null and b/public/img/community/mvps/mvp-josh-adams.jpg differ diff --git a/public/img/community/mvps/mvp-josue-a-bogran.jpg b/public/img/community/mvps/mvp-josue-a-bogran.jpg new file mode 100644 index 00000000..dbd71c4e Binary files /dev/null and b/public/img/community/mvps/mvp-josue-a-bogran.jpg differ diff --git a/public/img/community/mvps/mvp-juan-diaz.jpg b/public/img/community/mvps/mvp-juan-diaz.jpg new file mode 100644 index 00000000..1a034e89 Binary files /dev/null and b/public/img/community/mvps/mvp-juan-diaz.jpg differ diff --git a/public/img/community/mvps/mvp-julia-forde.jpg b/public/img/community/mvps/mvp-julia-forde.jpg new file mode 100644 index 00000000..181d105a Binary files /dev/null and b/public/img/community/mvps/mvp-julia-forde.jpg differ diff --git a/public/img/community/mvps/mvp-kyjah-keys.jpg b/public/img/community/mvps/mvp-kyjah-keys.jpg new file mode 100644 index 00000000..71b78a10 Binary files /dev/null and b/public/img/community/mvps/mvp-kyjah-keys.jpg differ diff --git a/public/img/community/mvps/mvp-lara-rachidi.jpg b/public/img/community/mvps/mvp-lara-rachidi.jpg new file mode 100644 index 00000000..02e20382 Binary files /dev/null and b/public/img/community/mvps/mvp-lara-rachidi.jpg differ diff --git a/public/img/community/mvps/mvp-laurenz-wuttke.jpg b/public/img/community/mvps/mvp-laurenz-wuttke.jpg new file mode 100644 index 00000000..0dcfab90 Binary files /dev/null and b/public/img/community/mvps/mvp-laurenz-wuttke.jpg differ diff --git a/public/img/community/mvps/mvp-liping-huang.jpg b/public/img/community/mvps/mvp-liping-huang.jpg new file mode 100644 index 00000000..72a42cc2 Binary files /dev/null and b/public/img/community/mvps/mvp-liping-huang.jpg differ diff --git a/public/img/community/mvps/mvp-luan-moreno.jpg b/public/img/community/mvps/mvp-luan-moreno.jpg new file mode 100644 index 00000000..7c64c310 Binary files /dev/null and b/public/img/community/mvps/mvp-luan-moreno.jpg differ diff --git a/public/img/community/mvps/mvp-maksim-pachkouski.jpg b/public/img/community/mvps/mvp-maksim-pachkouski.jpg new file mode 100644 index 00000000..8abd6ed0 Binary files /dev/null and b/public/img/community/mvps/mvp-maksim-pachkouski.jpg differ diff --git a/public/img/community/mvps/mvp-mani-kandasamy.jpg b/public/img/community/mvps/mvp-mani-kandasamy.jpg new file mode 100644 index 00000000..cc692573 Binary files /dev/null and b/public/img/community/mvps/mvp-mani-kandasamy.jpg differ diff --git a/public/img/community/mvps/mvp-mantu-samadder.jpg b/public/img/community/mvps/mvp-mantu-samadder.jpg new file mode 100644 index 00000000..307503df Binary files /dev/null and b/public/img/community/mvps/mvp-mantu-samadder.jpg differ diff --git a/public/img/community/mvps/mvp-maria-vechtomova.jpg b/public/img/community/mvps/mvp-maria-vechtomova.jpg new file mode 100644 index 00000000..24856391 Binary files /dev/null and b/public/img/community/mvps/mvp-maria-vechtomova.jpg differ diff --git a/public/img/community/mvps/mvp-mate-gulyas.jpg b/public/img/community/mvps/mvp-mate-gulyas.jpg new file mode 100644 index 00000000..34961323 Binary files /dev/null and b/public/img/community/mvps/mvp-mate-gulyas.jpg differ diff --git a/public/img/community/mvps/mvp-maulik-dixit.jpg b/public/img/community/mvps/mvp-maulik-dixit.jpg new file mode 100644 index 00000000..9da93ffe Binary files /dev/null and b/public/img/community/mvps/mvp-maulik-dixit.jpg differ diff --git a/public/img/community/mvps/mvp-michael-green.jpg b/public/img/community/mvps/mvp-michael-green.jpg new file mode 100644 index 00000000..e72acb7f Binary files /dev/null and b/public/img/community/mvps/mvp-michael-green.jpg differ diff --git a/public/img/community/mvps/mvp-miguel-diaz.jpg b/public/img/community/mvps/mvp-miguel-diaz.jpg new file mode 100644 index 00000000..5d3da9ed Binary files /dev/null and b/public/img/community/mvps/mvp-miguel-diaz.jpg differ diff --git a/public/img/community/mvps/mvp-nidhi-gupta.jpg b/public/img/community/mvps/mvp-nidhi-gupta.jpg new file mode 100644 index 00000000..44e93759 Binary files /dev/null and b/public/img/community/mvps/mvp-nidhi-gupta.jpg differ diff --git a/public/img/community/mvps/mvp-nivethan-venkatachalam.jpg b/public/img/community/mvps/mvp-nivethan-venkatachalam.jpg new file mode 100644 index 00000000..147eadaa Binary files /dev/null and b/public/img/community/mvps/mvp-nivethan-venkatachalam.jpg differ diff --git a/public/img/community/mvps/mvp-pal-de-vibe.jpg b/public/img/community/mvps/mvp-pal-de-vibe.jpg new file mode 100644 index 00000000..875604d7 Binary files /dev/null and b/public/img/community/mvps/mvp-pal-de-vibe.jpg differ diff --git a/public/img/community/mvps/mvp-phani-reddy-janga.jpg b/public/img/community/mvps/mvp-phani-reddy-janga.jpg new file mode 100644 index 00000000..06bdc72b Binary files /dev/null and b/public/img/community/mvps/mvp-phani-reddy-janga.jpg differ diff --git a/public/img/community/mvps/mvp-prakash-trivedi.jpg b/public/img/community/mvps/mvp-prakash-trivedi.jpg new file mode 100644 index 00000000..86d18e01 Binary files /dev/null and b/public/img/community/mvps/mvp-prakash-trivedi.jpg differ diff --git a/public/img/community/mvps/mvp-r-tyler-croy.jpg b/public/img/community/mvps/mvp-r-tyler-croy.jpg new file mode 100644 index 00000000..baa63fab Binary files /dev/null and b/public/img/community/mvps/mvp-r-tyler-croy.jpg differ diff --git a/public/img/community/mvps/mvp-rahul-gupta.jpg b/public/img/community/mvps/mvp-rahul-gupta.jpg new file mode 100644 index 00000000..57850d6f Binary files /dev/null and b/public/img/community/mvps/mvp-rahul-gupta.jpg differ diff --git a/public/img/community/mvps/mvp-rajaniesh-kaushikk.jpg b/public/img/community/mvps/mvp-rajaniesh-kaushikk.jpg new file mode 100644 index 00000000..378185ab Binary files /dev/null and b/public/img/community/mvps/mvp-rajaniesh-kaushikk.jpg differ diff --git a/public/img/community/mvps/mvp-ranjit-nagesh.jpg b/public/img/community/mvps/mvp-ranjit-nagesh.jpg new file mode 100644 index 00000000..bae5d648 Binary files /dev/null and b/public/img/community/mvps/mvp-ranjit-nagesh.jpg differ diff --git a/public/img/community/mvps/mvp-raul-sarachaga.jpg b/public/img/community/mvps/mvp-raul-sarachaga.jpg new file mode 100644 index 00000000..24cf3b59 Binary files /dev/null and b/public/img/community/mvps/mvp-raul-sarachaga.jpg differ diff --git a/public/img/community/mvps/mvp-rishabh-pandey.jpg b/public/img/community/mvps/mvp-rishabh-pandey.jpg new file mode 100644 index 00000000..010122b8 Binary files /dev/null and b/public/img/community/mvps/mvp-rishabh-pandey.jpg differ diff --git a/public/img/community/mvps/mvp-robert-thompson.jpg b/public/img/community/mvps/mvp-robert-thompson.jpg new file mode 100644 index 00000000..24abf48d Binary files /dev/null and b/public/img/community/mvps/mvp-robert-thompson.jpg differ diff --git a/public/img/community/mvps/mvp-ryan-shiva.jpg b/public/img/community/mvps/mvp-ryan-shiva.jpg new file mode 100644 index 00000000..6dc9b0fd Binary files /dev/null and b/public/img/community/mvps/mvp-ryan-shiva.jpg differ diff --git a/public/img/community/mvps/mvp-sai-nageshwaran.jpg b/public/img/community/mvps/mvp-sai-nageshwaran.jpg new file mode 100644 index 00000000..65545b71 Binary files /dev/null and b/public/img/community/mvps/mvp-sai-nageshwaran.jpg differ diff --git a/public/img/community/mvps/mvp-scott-davis.jpg b/public/img/community/mvps/mvp-scott-davis.jpg new file mode 100644 index 00000000..000842bf Binary files /dev/null and b/public/img/community/mvps/mvp-scott-davis.jpg differ diff --git a/public/img/community/mvps/mvp-shashank-shekhar.jpg b/public/img/community/mvps/mvp-shashank-shekhar.jpg new file mode 100644 index 00000000..6c47a181 Binary files /dev/null and b/public/img/community/mvps/mvp-shashank-shekhar.jpg differ diff --git a/public/img/community/mvps/mvp-shekhar-shukla.jpg b/public/img/community/mvps/mvp-shekhar-shukla.jpg new file mode 100644 index 00000000..7998ddde Binary files /dev/null and b/public/img/community/mvps/mvp-shekhar-shukla.jpg differ diff --git a/public/img/community/mvps/mvp-shraddha-shetty.jpg b/public/img/community/mvps/mvp-shraddha-shetty.jpg new file mode 100644 index 00000000..7e8cd743 Binary files /dev/null and b/public/img/community/mvps/mvp-shraddha-shetty.jpg differ diff --git a/public/img/community/mvps/mvp-shubham-kumar.jpg b/public/img/community/mvps/mvp-shubham-kumar.jpg new file mode 100644 index 00000000..c42a43c6 Binary files /dev/null and b/public/img/community/mvps/mvp-shubham-kumar.jpg differ diff --git a/public/img/community/mvps/mvp-simon-whiteley.jpg b/public/img/community/mvps/mvp-simon-whiteley.jpg new file mode 100644 index 00000000..4a650236 Binary files /dev/null and b/public/img/community/mvps/mvp-simon-whiteley.jpg differ diff --git a/public/img/community/mvps/mvp-soufiane-darraz.jpg b/public/img/community/mvps/mvp-soufiane-darraz.jpg new file mode 100644 index 00000000..3cc12923 Binary files /dev/null and b/public/img/community/mvps/mvp-soufiane-darraz.jpg differ diff --git a/public/img/community/mvps/mvp-soumya-ghosh.jpg b/public/img/community/mvps/mvp-soumya-ghosh.jpg new file mode 100644 index 00000000..0131a5e2 Binary files /dev/null and b/public/img/community/mvps/mvp-soumya-ghosh.jpg differ diff --git a/public/img/community/mvps/mvp-srivathsan-rl.jpg b/public/img/community/mvps/mvp-srivathsan-rl.jpg new file mode 100644 index 00000000..6a556a9f Binary files /dev/null and b/public/img/community/mvps/mvp-srivathsan-rl.jpg differ diff --git a/public/img/community/mvps/mvp-steve-notley.jpg b/public/img/community/mvps/mvp-steve-notley.jpg new file mode 100644 index 00000000..5a56c145 Binary files /dev/null and b/public/img/community/mvps/mvp-steve-notley.jpg differ diff --git a/public/img/community/mvps/mvp-subramanian-iyer.jpg b/public/img/community/mvps/mvp-subramanian-iyer.jpg new file mode 100644 index 00000000..ddfc3944 Binary files /dev/null and b/public/img/community/mvps/mvp-subramanian-iyer.jpg differ diff --git a/public/img/community/mvps/mvp-sudarshan-koirala.jpg b/public/img/community/mvps/mvp-sudarshan-koirala.jpg new file mode 100644 index 00000000..bdeb4838 Binary files /dev/null and b/public/img/community/mvps/mvp-sudarshan-koirala.jpg differ diff --git a/public/img/community/mvps/mvp-sudhir-gajre.jpg b/public/img/community/mvps/mvp-sudhir-gajre.jpg new file mode 100644 index 00000000..d90c718a Binary files /dev/null and b/public/img/community/mvps/mvp-sudhir-gajre.jpg differ diff --git a/public/img/community/mvps/mvp-tejas-pandit.jpg b/public/img/community/mvps/mvp-tejas-pandit.jpg new file mode 100644 index 00000000..aee3f599 Binary files /dev/null and b/public/img/community/mvps/mvp-tejas-pandit.jpg differ diff --git a/public/img/community/mvps/mvp-vipul-choudhary.jpg b/public/img/community/mvps/mvp-vipul-choudhary.jpg new file mode 100644 index 00000000..1d482470 Binary files /dev/null and b/public/img/community/mvps/mvp-vipul-choudhary.jpg differ diff --git a/public/img/community/mvps/mvp-vitalija-bartuseviciute.jpg b/public/img/community/mvps/mvp-vitalija-bartuseviciute.jpg new file mode 100644 index 00000000..bb405db0 Binary files /dev/null and b/public/img/community/mvps/mvp-vitalija-bartuseviciute.jpg differ diff --git a/public/img/community/mvps/mvp-yash-mahendra-joshi.jpg b/public/img/community/mvps/mvp-yash-mahendra-joshi.jpg new file mode 100644 index 00000000..44bd3253 Binary files /dev/null and b/public/img/community/mvps/mvp-yash-mahendra-joshi.jpg differ diff --git a/public/img/community/mvps/mvp-yuki-saito.jpg b/public/img/community/mvps/mvp-yuki-saito.jpg new file mode 100644 index 00000000..10a8813f Binary files /dev/null and b/public/img/community/mvps/mvp-yuki-saito.jpg differ diff --git a/public/img/community/mvps/mvp-zoe-van-noppen.jpg b/public/img/community/mvps/mvp-zoe-van-noppen.jpg new file mode 100644 index 00000000..a12e85d8 Binary files /dev/null and b/public/img/community/mvps/mvp-zoe-van-noppen.jpg differ diff --git a/public/img/community/select-arrow.svg b/public/img/community/select-arrow.svg new file mode 100644 index 00000000..b14b9739 --- /dev/null +++ b/public/img/community/select-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/community/student-badge.svg b/public/img/community/student-badge.svg new file mode 100644 index 00000000..a7bc7dd8 --- /dev/null +++ b/public/img/community/student-badge.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/community/x.svg b/public/img/community/x.svg new file mode 100644 index 00000000..44dca332 --- /dev/null +++ b/public/img/community/x.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts/normalize-appkit-doc-links.mjs b/scripts/normalize-appkit-doc-links.mjs new file mode 100644 index 00000000..53a948ce --- /dev/null +++ b/scripts/normalize-appkit-doc-links.mjs @@ -0,0 +1,32 @@ +export function normalizeSyncedDocLinks( + source, + { channel, appKitErrorSource = "" }, +) { + let updated = source + .replaceAll("](/docs/api/", `](/docs/appkit/${channel}/api/`) + .replaceAll( + "(./lakebase.md#on-behalf-of-obo--per-user-connections)", + "(./lakebase.md#on-behalf-of-obo-per-user-connections)", + ); + + // DevHub strips the leading underscore: the protected field gets + // clientmessage and the later public accessor gets clientmessage-1. + // Match the member label too, so a cached sync cannot remap the field + // link to the accessor on its second run. Older channels may lack the field. + if ( + /^### \\_clientMessage\?\r?$/m.test(appKitErrorSource) && + /^### clientMessage\r?$/m.test(appKitErrorSource) + ) { + updated = updated + .replaceAll( + "[`_clientMessage`](Class.AppKitError.md#_clientmessage)", + "[`_clientMessage`](Class.AppKitError.md#clientmessage)", + ) + .replaceAll( + "[`clientMessage`](Class.AppKitError.md#clientmessage)", + "[`clientMessage`](Class.AppKitError.md#clientmessage-1)", + ); + } + + return updated; +} diff --git a/scripts/sync-appkit-docs.mjs b/scripts/sync-appkit-docs.mjs index b40d37c8..6d8b7c87 100644 --- a/scripts/sync-appkit-docs.mjs +++ b/scripts/sync-appkit-docs.mjs @@ -4,6 +4,8 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { normalizeSyncedDocLinks } from "./normalize-appkit-doc-links.mjs"; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); @@ -109,6 +111,7 @@ function walkFiles(root) { } function normalizeSyncedDocs(docsRoot) { + const appKitErrorSources = new Map(); const upstreamLlmsLinkHelper = /import\s+\w+Context\s+from\s+["'][^"']+["'];\n\nexport function LlmsTxtLink\([\s\S]*?^}\n\n/gm; const upstreamSidebarConfigImport = @@ -150,23 +153,28 @@ type TypedocSidebar = { .replace(upstreamLlmsLinkHelper, "") .replaceAll("", "[`llms.txt`](/llms.txt)"); - // Upstream authors links with Docusaurus/typedoc slug conventions that - // differ from DevHub's github-slugger heading ids. Rewrite the known - // mismatches so intra-site anchors and the API index resolve: - // - github-slugger drops the leading underscore, so the `_clientMessage?` - // property renders with id `clientmessage`, not `_clientmessage`. - // - a spaced em-dash (`(OBO) — per-user`) collapses to a single hyphen, - // not the double hyphen typedoc/docusaurus emit. - // - upstream API pages live at /docs/api/*; DevHub serves them under the - // versioned AppKit channel (e.g. /docs/appkit/v0/api/*). - const channel = path.relative(docsRoot, filePath).split(path.sep)[0]; - updated = updated - .replaceAll("#_clientmessage", "#clientmessage") - .replaceAll( - "#on-behalf-of-obo--per-user-connections", - "#on-behalf-of-obo-per-user-connections", - ) - .replaceAll("](/docs/api/", `](/docs/appkit/${channel}/api/`); + if (/\.mdx?$/.test(filePath)) { + const [channel] = path.relative(docsRoot, filePath).split(path.sep); + if (!appKitErrorSources.has(channel)) { + const errorSourcePath = path.join( + docsRoot, + channel, + "api", + "appkit", + "Class.AppKitError.md", + ); + appKitErrorSources.set( + channel, + fs.existsSync(errorSourcePath) + ? fs.readFileSync(errorSourcePath, "utf-8") + : "", + ); + } + updated = normalizeSyncedDocLinks(updated, { + channel, + appKitErrorSource: appKitErrorSources.get(channel), + }); + } if (upstreamSidebarConfigImport.test(updated)) { updated = updated diff --git a/src/app/(website)/mvps/directory/error.tsx b/src/app/(website)/mvps/directory/error.tsx new file mode 100644 index 00000000..109b05cc --- /dev/null +++ b/src/app/(website)/mvps/directory/error.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { CommunityError as default } from "@/components/community/community-error"; diff --git a/src/app/(website)/mvps/directory/not-found.tsx b/src/app/(website)/mvps/directory/not-found.tsx new file mode 100644 index 00000000..8e5eb172 --- /dev/null +++ b/src/app/(website)/mvps/directory/not-found.tsx @@ -0,0 +1,5 @@ +import { NotFoundContent } from "@/components/not-found-content"; + +export default function MVPDirectoryNotFound() { + return ; +} diff --git a/src/app/(website)/mvps/directory/page.tsx b/src/app/(website)/mvps/directory/page.tsx new file mode 100644 index 00000000..3b41f1a7 --- /dev/null +++ b/src/app/(website)/mvps/directory/page.tsx @@ -0,0 +1,43 @@ +import { notFound } from "next/navigation"; + +import { + directoryHref, + directoryPageNumber, + directoryTitle, +} from "@/lib/community/directory-query"; +import { getMetadata } from "@/lib/get-metadata"; +import { CommunityCTA } from "@/components/community/community-cta"; +import { DirectoryHero } from "@/components/community/directory-hero"; +import { PeopleDirectory } from "@/components/community/people-directory"; +import { ProgramBackLink } from "@/components/community/program-back-link"; +import Footer from "@/components/footer"; + +type PageProps = { params: Promise<{ page?: string }> }; + +export async function generateMetadata({ params }: PageProps) { + const page = directoryPageNumber((await params).page); + if (!page) notFound(); + return getMetadata({ + title: directoryTitle("mvp", page), + description: + "Meet experts who share knowledge, build community, and grow the Databricks ecosystem.", + imagePath: "/img/community/mvp-og-image.jpg", + pathname: directoryHref("mvp", {}, page), + }); +} + +export default async function MVPDirectoryPage({ params }: PageProps) { + const page = directoryPageNumber((await params).page); + if (!page) notFound(); + return ( +
+ + +
+ + +
+
+
+ ); +} diff --git a/src/app/(website)/mvps/directory/page/[page]/page.tsx b/src/app/(website)/mvps/directory/page/[page]/page.tsx new file mode 100644 index 00000000..e7da04bd --- /dev/null +++ b/src/app/(website)/mvps/directory/page/[page]/page.tsx @@ -0,0 +1,11 @@ +import { DIRECTORY_PAGE_SIZE } from "@/lib/community/directory-query"; +import { getMvpDirectory } from "@/lib/community/mvps"; + +export { default, generateMetadata } from "../../page"; + +export function generateStaticParams() { + const pageCount = Math.ceil(getMvpDirectory().length / DIRECTORY_PAGE_SIZE); + return Array.from({ length: Math.max(0, pageCount - 1) }, (_, index) => ({ + page: String(index + 2), + })); +} diff --git a/src/app/(website)/mvps/page.tsx b/src/app/(website)/mvps/page.tsx new file mode 100644 index 00000000..648f6702 --- /dev/null +++ b/src/app/(website)/mvps/page.tsx @@ -0,0 +1,30 @@ +import { getMetadata } from "@/lib/get-metadata"; +import { BrandStrip } from "@/components/ui/brand-strip"; +import { CommunityCTA } from "@/components/community/community-cta"; +import { MVPBenefits } from "@/components/community/program-benefits"; +import { ProgramHero } from "@/components/community/program-hero"; +import { MVPRequirements } from "@/components/community/program-requirements"; +import Footer from "@/components/footer"; + +export const metadata = getMetadata({ + title: "Databricks MVPs", + description: + "How Databricks recognizes and supports experts in the Data + AI community. Explore program benefits, meet the MVPs, and nominate a peer.", + imagePath: "/img/community/mvp-og-image.jpg", + pathname: "/mvps", +}); + +export default function MVPPage() { + return ( +
+ + + +
+ + +
+
+
+ ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index dfb8aacf..67912813 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,55 +1,16 @@ -import type { ReactNode } from "react"; import type { Metadata } from "next"; -import Link from "next/link"; import { getDocsSearchItems } from "@/lib/docs-content"; -import { Button } from "@/components/ui/button"; -import Footer from "@/components/footer"; import { Header } from "@/components/header/header"; -import CTA from "@/components/home/cta"; +import { NotFoundContent } from "@/components/not-found-content"; -export const metadata: Metadata = { - title: "Page Not Found", -}; +export const metadata: Metadata = { title: "Page Not Found" }; -export default function WebsiteNotFound(): ReactNode { - const searchItems = getDocsSearchItems(); +export default function WebsiteNotFound() { return ( <> -
-
-
-
-
-

- Error404 - : Page Not Found -

-

- We know this isn't where you intended to land, but we hope - you have some fun while you're here. -

- -
-
-
-
- -
-
-
+
+ ); } diff --git a/src/components/community/community-cta.tsx b/src/components/community/community-cta.tsx new file mode 100644 index 00000000..1ff10d00 --- /dev/null +++ b/src/components/community/community-cta.tsx @@ -0,0 +1,56 @@ +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import CTA from "@/components/home/cta"; + +export const MVP_NOMINATION_URL = + "https://surveys.training.databricks.com/jfe/form/SV_6Ed034QOD4pcQFU"; +export const STUDENT_APPLICATION_URL = + "https://airtable.com/appasC90KmqZ5x1t5/pag6tvR9VUG4Kf1iM/form"; + +export function CommunityCTA({ + variant, + theme = "filled", +}: { + variant: "mvp" | "student" | "student-profile"; + theme?: "filled" | "outline"; +}) { + const mvp = variant === "mvp"; + const application = variant === "student-profile"; + const href = mvp + ? MVP_NOMINATION_URL + : application + ? STUDENT_APPLICATION_URL + : "/student-fellows/fellows"; + const title = mvp + ? "Know someone who is making an impact?" + : application + ? "Could you be the next Student Fellow?" + : "What will\nthe future look like?"; + const action = mvp + ? "Nominate a peer" + : application + ? "Become a student fellow" + : "Browse fellow profiles"; + return ( + + {action} + + } + /> + ); +} diff --git a/src/components/community/community-error.tsx b/src/components/community/community-error.tsx new file mode 100644 index 00000000..2125dc06 --- /dev/null +++ b/src/components/community/community-error.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Button } from "@/components/ui/button"; + +export function CommunityError({ reset }: { reset: () => void }) { + return ( +
+

+ The community directory is temporarily unavailable. +

+

Please try again in a moment.

+ +
+ ); +} diff --git a/src/components/community/directory-filter.tsx b/src/components/community/directory-filter.tsx new file mode 100644 index 00000000..c12c8a0f --- /dev/null +++ b/src/components/community/directory-filter.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useState } from "react"; + +import { normalizeDirectorySearch } from "@/lib/community/directory"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Field, + FieldGroup, + FieldLabel, + FieldLegend, + FieldSet, +} from "@/components/ui/field"; +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export function DirectoryFilter({ + id, + name, + label, + options, + values, + onChange, +}: { + id: string; + name: "city" | "country" | "university"; + label: string; + options: string[]; + values: string[]; + onChange: (values: string[]) => void; +}) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const choices = [...new Set([...options, ...values])]; + const visible = choices.filter((option) => + normalizeDirectorySearch(option).includes(normalizeDirectorySearch(search)), + ); + const placeholder = + name === "country" + ? "Search countries" + : name === "university" + ? "Search universities" + : "Search cities"; + return ( + { + setOpen(next); + setSearch(""); + }} + > + {values.map((value) => ( + + ))} + + + + + + + {placeholder} + + + setSearch(event.target.value)} + placeholder={placeholder} + className="text-base tracking-tight placeholder:text-black/40 focus-visible:border-black focus-visible:ring-0 focus-visible:outline-0 md:text-base" + /> + + + + + +
+ {label} + + {visible.map((option) => ( + + ))} + {!visible.length && ( +

+ No matches found +

+ )} +
+
+ +
+
+ ); +} diff --git a/src/components/community/directory-filters.tsx b/src/components/community/directory-filters.tsx new file mode 100644 index 00000000..28405164 --- /dev/null +++ b/src/components/community/directory-filters.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useRef } from "react"; +import { Search, X } from "lucide-react"; + +import { + directoryHref, + readDirectoryQuery, + type DirectorySearchParams, +} from "@/lib/community/directory-query"; +import type { PersonKind } from "@/lib/community/schema"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group"; +import { DirectoryFilter } from "@/components/community/directory-filter"; + +export function DirectoryFilters({ + kind, + params, + facets, + onChange, +}: { + kind: PersonKind; + params: DirectorySearchParams; + facets: { cities: string[]; countries: string[]; universities: string[] }; + onChange: (params: DirectorySearchParams, replace?: boolean) => void; +}) { + const query = readDirectoryQuery(params, kind); + const search = query.q || ""; + const input = useRef(null); + return ( +
{ + event.preventDefault(); + onChange(params); + }} + role="search" + aria-label={kind === "student" ? "Find student fellows" : "Find MVPs"} + > +
+ + + Filter by: + +
+ {(kind === "student" + ? [ + { + name: "country" as const, + label: "Country", + options: facets.countries, + value: query.country, + }, + { + name: "university" as const, + label: "University", + options: facets.universities, + value: query.university, + }, + ] + : [ + { + name: "city" as const, + label: "City", + options: facets.cities, + value: query.city, + }, + { + name: "country" as const, + label: "Country", + options: facets.countries, + value: query.country, + }, + ] + ).map((filter) => ( + + + {filter.label} + + + onChange({ ...params, [filter.name]: values }) + } + /> + + ))} +
+ + + Search by name or expertise + + + + onChange({ ...params, q: event.target.value }, true) + } + maxLength={200} + placeholder="Search by name or expertise" + className="placeholder:text-grey-60 text-base tracking-tight text-black md:text-base dark:text-black [&::-webkit-search-cancel-button]:appearance-none" + /> + + + + + {search ? ( + + { + onChange({ ...params, q: "" }); + input.current?.focus(); + }} + > + + + ) : null} + + +
+
+
+ ); +} diff --git a/src/components/community/directory-hero.tsx b/src/components/community/directory-hero.tsx new file mode 100644 index 00000000..92041d85 --- /dev/null +++ b/src/components/community/directory-hero.tsx @@ -0,0 +1,24 @@ +import type { PersonKind } from "@/lib/community/schema"; + +export function DirectoryHero({ kind }: { kind: PersonKind }) { + const mvp = kind === "mvp"; + return ( +
+
+

+ + {mvp ? "Meet the MVPs." : "Student Fellows."} + {" "} + {mvp + ? "[The people behind the impact.]" + : "[Meet the next generation of AI.]"} +

+

+ {mvp + ? "Meet experts who share knowledge, build community, and grow the Databricks ecosystem." + : "Meet the students building, learning, and shaping the future of data and AI."} +

+
+
+ ); +} diff --git a/src/components/community/directory-results.tsx b/src/components/community/directory-results.tsx new file mode 100644 index 00000000..cf9318b2 --- /dev/null +++ b/src/components/community/directory-results.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { + useEffect, + useMemo, + useSyncExternalStore, + type MouseEvent, +} from "react"; + +import { + filterDirectory, + type DirectoryPerson, +} from "@/lib/community/directory"; +import { + directoryHref, + directoryTitle, + readDirectoryQuery, + type DirectorySearchParams, +} from "@/lib/community/directory-query"; +import type { PersonKind } from "@/lib/community/schema"; +import { getPageTitle } from "@/lib/get-metadata"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { DirectoryFilters } from "@/components/community/directory-filters"; +import { PersonCard } from "@/components/community/person-card"; + +function subscribe(onChange: () => void) { + window.addEventListener("popstate", onChange); + window.addEventListener("community-directory-change", onChange); + return () => { + window.removeEventListener("popstate", onChange); + window.removeEventListener("community-directory-change", onChange); + }; +} + +function getLocation() { + return window.location.pathname + window.location.search; +} + +function navigate(href: string, replace = false) { + if (window.location.pathname + window.location.search === href) return false; + window.history[replace ? "replaceState" : "pushState"]( + null, + "", + href + window.location.hash, + ); + window.dispatchEvent(new Event("community-directory-change")); + return true; +} + +function navigateLink(event: MouseEvent) { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return; + const link = (event.target as HTMLElement).closest("a"); + if (!link) return; + event.preventDefault(); + if (navigate(link.pathname + link.search)) { + const directory = event.currentTarget.closest("section"); + directory?.scrollIntoView({ behavior: "instant", block: "start" }); + directory?.focus({ preventScroll: true }); + } +} + +function DirectoryPagination({ + kind, + params, + page, + totalPages, +}: { + kind: PersonKind; + params: DirectorySearchParams; + page: number; + totalPages: number; +}) { + if (totalPages < 2) return null; + const pages = Array.from(new Set([1, page - 1, page, page + 1, totalPages])) + .filter((n) => n > 0 && n <= totalPages) + .sort((a, b) => a - b); + const linkClass = + "rounded-none text-black hover:bg-black/5 hover:text-black dark:hover:bg-black/5"; + return ( + + + {page > 1 ? ( + + + + ) : null} + {pages.map((number, index) => ( + + {index > 0 && number - pages[index - 1] > 1 ? ( + + ) : null} + + {number} + + + ))} + {page < totalPages ? ( + + + + ) : null} + + + ); +} + +export function DirectoryResults({ + kind, + members, + initialPage = 1, +}: { + kind: PersonKind; + members: DirectoryPerson[]; + initialPage?: number; +}) { + const location = useSyncExternalStore(subscribe, getLocation, () => + directoryHref(kind, {}, initialPage), + ); + const [pathname, search = ""] = location.split("?"); + const urlParams = new URLSearchParams(search); + const params: DirectorySearchParams = Object.fromEntries( + [...urlParams.keys()].map((key) => [key, urlParams.getAll(key)]), + ); + const query = readDirectoryQuery(params, kind, pathname); + const people = filterDirectory(members, query); + useEffect(() => { + const title = getPageTitle(directoryTitle(kind, query.page)); + document.title = title; + for (const selector of [ + 'meta[property="og:title"]', + 'meta[name="twitter:title"]', + ]) { + document.querySelector(selector)?.setAttribute("content", title); + } + const canonical = document.querySelector( + 'link[rel="canonical"]', + ); + if (canonical) { + canonical.href = new URL( + directoryHref(kind, {}, query.page), + canonical.href, + ).href; + document + .querySelector('meta[property="og:url"]') + ?.setAttribute("content", canonical.href); + } + }, [kind, query.page]); + const facets = useMemo( + () => ({ + countries: [ + ...new Set(members.map((person) => person.country).filter(Boolean)), + ].sort(), + cities: [ + ...new Set(members.map((person) => person.city).filter(Boolean)), + ].sort(), + universities: [ + ...new Set( + members.map((person) => person.organization).filter(Boolean), + ), + ].sort(), + }), + [members], + ); + return ( +
+ + navigate(directoryHref(kind, next), replace) + } + /> +

+ {people.total} {kind === "student" ? "student fellows" : "MVPs"} + {query.q ? ` matching “${query.q}”` : ""} +

+ {people.items.length ? ( +
+ {people.items.map((person) => ( + + ))} +
+ ) : ( +
+

No matches found

+

+ Try another search or clear your filters. +

+ +
+ )} + +
+ ); +} diff --git a/src/components/community/more-person-links.tsx b/src/components/community/more-person-links.tsx new file mode 100644 index 00000000..35af484a --- /dev/null +++ b/src/components/community/more-person-links.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState } from "react"; +import { ArrowUpRight, Ellipsis } from "lucide-react"; + +import type { PublicPerson } from "@/lib/community/schema"; +import { cn } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function MorePersonLinks({ + name, + links, + wideVisibleCount = 0, +}: { + name: string; + links: PublicPerson["additionalLinks"]; + wideVisibleCount?: number; +}) { + const [menuContainer, setMenuContainer] = useState(null); + + return ( + + ); +} diff --git a/src/components/community/people-directory.tsx b/src/components/community/people-directory.tsx new file mode 100644 index 00000000..0ef21bf3 --- /dev/null +++ b/src/components/community/people-directory.tsx @@ -0,0 +1,21 @@ +import { notFound } from "next/navigation"; + +import { DIRECTORY_PAGE_SIZE } from "@/lib/community/directory-query"; +import { getMvpDirectory } from "@/lib/community/mvps"; +import { getDirectory } from "@/lib/community/people.server"; +import type { PersonKind } from "@/lib/community/schema"; + +import { DirectoryResults } from "./directory-results"; + +export async function PeopleDirectory({ + kind, + page = 1, +}: { + kind: PersonKind; + page?: number; +}) { + const members = kind === "mvp" ? getMvpDirectory() : await getDirectory(kind); + if (page > Math.max(1, Math.ceil(members.length / DIRECTORY_PAGE_SIZE))) + notFound(); + return ; +} diff --git a/src/components/community/person-card.tsx b/src/components/community/person-card.tsx new file mode 100644 index 00000000..66b8de10 --- /dev/null +++ b/src/components/community/person-card.tsx @@ -0,0 +1,190 @@ +import Link from "next/link"; +import { Globe } from "lucide-react"; + +import type { DirectoryPerson } from "@/lib/community/directory"; +import { personLinks } from "@/lib/community/person-links"; +import type { PublicPerson } from "@/lib/community/schema"; +import { cn } from "@/lib/utils"; +import { MorePersonLinks } from "@/components/community/more-person-links"; +import { Icons } from "@/components/icons"; + +function PersonLinks({ + person, + className, + compact = false, +}: { + person: Pick; + className?: string; + compact?: boolean; +}) { + const links = personLinks(person); + const iconCount = links.filter((link) => link.kind !== "other").length; + const visibleCount = compact + ? Math.min(iconCount, links.length > 3 || links.length > iconCount ? 2 : 3) + : iconCount; + const compactCount = compact + ? Math.min(iconCount, links.length > 2 || links.length > iconCount ? 1 : 2) + : iconCount; + return ( +
+ {links.slice(0, visibleCount).map(({ kind, url, label }, index) => { + const Icon = + kind === "github" + ? Icons.github + : kind === "youtube" + ? Icons.youtube + : Globe; + return ( + = compactCount && "hidden xl:inline-flex", + )} + > + {kind === "linkedin" ? ( + + ); + })} + {links.length > compactCount && ( + + )} +
+ ); +} + +function PersonPhoto({ + person, + className, + eager = false, +}: { + person: Pick; + className?: string; + eager?: boolean; +}) { + return ( +
+ {person.photoUrl ? ( + {person.name} + ) : ( + + )} +
+ ); +} + +export function PersonCard({ person }: { person: DirectoryPerson }) { + const href = + person.kind === "student" + ? `/student-fellows/fellows/${person.slug}` + : person.links.website || + person.links.linkedin || + person.links.github || + person.links.x || + person.additionalLinks[0]?.url; + const external = person.kind === "mvp"; + const content = ( + <> + + {href ? ( + + + + ) : null} +

+ {person.name} +

+ {person.organization || person.headline ? ( +

+ {person.organization || person.headline} +

+ ) : null} + + ); + return ( +
+ {href ? ( + + {content} + + ) : ( +
{content}
+ )} +
+ {person.country ? ( +

+

+ ) : ( + + )} + +
+
+ ); +} diff --git a/src/components/community/program-back-link.tsx b/src/components/community/program-back-link.tsx new file mode 100644 index 00000000..c8d81aef --- /dev/null +++ b/src/components/community/program-back-link.tsx @@ -0,0 +1,37 @@ +import Image from "next/image"; +import Link from "next/link"; + +import type { PersonKind } from "@/lib/community/schema"; +import { Button } from "@/components/ui/button"; + +export function ProgramBackLink({ kind }: { kind: PersonKind }) { + return ( + + ); +} diff --git a/src/components/community/program-benefits.tsx b/src/components/community/program-benefits.tsx new file mode 100644 index 00000000..0694e2cc --- /dev/null +++ b/src/components/community/program-benefits.tsx @@ -0,0 +1,83 @@ +import { SectionKicker } from "@/components/products/section-kicker"; + +const mvpBenefits = [ + [ + "Join the MVP\nCommunity", + "Connect with other Databricks MVPs through a dedicated private community.", + ], + [ + "Connect With\nExperts", + "Connect directly with Databricks and open source product managers and engineers.", + ], + [ + "Get Early\nAccess", + "Try out new features and products before they are released and early roadmap access.", + ], + [ + "Receive monthly\ncredits", + "Get monthly credits to develop content and create demos.", + ], + [ + "Attend Data + AI\nSummit", + "Attend the annual Databricks Data + AI Summit with complimentary passes.", + ], + [ + "Share Your\nExpertise", + "Get opportunities to share your expertise at events, like Data + AI Summit.", + ], + [ + "Showcase Your\nMVP Status", + "Showcase your MVP status with an official badge across your social profiles and website.", + ], + [ + "Earn Community\nRecognition", + "Get featured across Databricks’ website and social media channels.", + ], +]; + +export function MVPBenefits() { + return ( +
+ Program Benefits +

+ See what MVPs get from the program. +
+ + [Connect, contribute, and grow your impact.] + +

+
+ {mvpBenefits.map(([title, description]) => ( +
+

+ {title} +

+

+ {title === "Share Your\nExpertise" ? ( + <> + Get opportunities to share your expertise at events, like{" "} + + Data + AI Summit + + . + + ) : ( + description + )} +

+
+ ))} +
+
+ ); +} diff --git a/src/components/community/program-hero.tsx b/src/components/community/program-hero.tsx new file mode 100644 index 00000000..16e6b2fe --- /dev/null +++ b/src/components/community/program-hero.tsx @@ -0,0 +1,126 @@ +import Image from "next/image"; +import Link from "next/link"; + +import type { PersonKind } from "@/lib/community/schema"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + MVP_NOMINATION_URL, + STUDENT_APPLICATION_URL, +} from "@/components/community/community-cta"; + +export function ProgramHero({ kind }: { kind: PersonKind }) { + const mvp = kind === "mvp"; + return ( +
+ {mvp +

+ + {mvp ? "Databricks MVPs." : "Student fellows."} + {" "} + {mvp + ? "How Databricks recognizes and supports experts in the Data + AI community." + : "Turn your data expertise into a career in AI."} +

+
+
+
+ + {mvp ? ( + + ) : ( + + )} +
+ {!mvp && ( +

+ Not a fellow yet? + + Apply today + + +

+ )} +
+

+ {mvp + ? "Recognizing those who share knowledge and grow the community." + : "Learn from Databricks experts and build real-world data and AI skills."} +

+
+
+ ); +} diff --git a/src/components/community/program-requirements.tsx b/src/components/community/program-requirements.tsx new file mode 100644 index 00000000..8e5ce97e --- /dev/null +++ b/src/components/community/program-requirements.tsx @@ -0,0 +1,113 @@ +import { SectionKicker } from "@/components/products/section-kicker"; + +const membership = [ + { + label: "Share knowledge", + title: "Create and share technical content.", + items: [ + "Write thought-leadership blogs and technical guides", + "Publish video demonstrations and tutorials", + "Post product breakdowns and tips on social media", + ], + }, + { + label: "Build community", + title: "Help create an active, welcoming, and connected community.", + items: [ + "Speak at industry conferences and data meetups", + "Answer questions and share experiences on Reddit", + "Engage actively in the official Databricks Community forum", + ], + }, + { + label: "Grow the ecosystem", + title: "Help grow the Databricks developer community.", + items: [ + "Organize local or virtual Databricks meetups", + "Develop courses and webinars to educate users", + "Expand the global developer footprint", + ], + }, + { + label: "Shape the future", + title: "Support and help shape the Databricks and the MVP program.", + items: [ + "Champion new products and feature releases", + "Provide candid feedback on what is and isn't working", + "Collaborate with fellow MVPs to refine and publish content", + ], + }, +]; + +export function MVPRequirements() { + return ( +
+ + Membership Requirements + +

+ How Databricks MVPs contribute to the community.{" "} + + MVPs share knowledge and grow the ecosystem. + +

+
+ {membership.map(({ label, title, items }, index) => ( +
+
+ {label} +
+
+ +

+ {title} +

+
    + {items.map((item) => ( +
  • +
  • + ))} +
+
+
+ ))} +
+
+ ); +} diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 0e086d81..46769874 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -43,6 +43,13 @@ const FOOTER_SECTIONS: FooterSection[] = [ { title: "COMMUNITY", items: [ + { label: "MVPs", to: "/mvps" }, + { label: "MVP directory", to: "/mvps/directory" }, + { + label: "Student Fellows", + href: "https://databricksstudentfellows.com/", + externalArrow: true, + }, { label: "Reddit", href: "https://www.reddit.com/r/databricks/", @@ -148,7 +155,7 @@ function LegalLinks({ className }: { className?: string }): ReactNode { > {LEGAL_LINKS.map((link) => ( ))} - + ); } @@ -173,7 +180,7 @@ function CopyrightAndLegal({

diff --git a/src/components/header/mobile-nav.tsx b/src/components/header/mobile-nav.tsx index bca836b8..e8f1efb7 100644 --- a/src/components/header/mobile-nav.tsx +++ b/src/components/header/mobile-nav.tsx @@ -6,10 +6,8 @@ import { usePathname } from "next/navigation"; import { ArrowUpRight } from "lucide-react"; import { - getActiveProductHref, isExternalHref, isHeaderNavItemActive, - PRODUCT_LINKS, type HeaderNavItem, } from "@/lib/header-navigation"; import { cn } from "@/lib/utils"; @@ -85,10 +83,6 @@ function MobileMenuButton({ ); } -function MobileTreeLine({ className }: { className: string }) { - return ; -} - function MobileTreeText({ active = false, activeFill = "content", @@ -149,10 +143,13 @@ export function MobileNav({ }: MobileNavProps) { const menuId = useId(); const pathname = usePathname() ?? "/"; - const activeProductHref = getActiveProductHref(pathname); const isHomeActive = pathname === "/"; - const productItem = items.find(({ label }) => label === "Product"); - const sectionItems = items.filter(({ label }) => label !== "Product"); + let nextTop = 46; + const positionedItems = items.map((item) => { + const top = nextTop; + nextTop += item.links ? 32 + item.links.length * 34 : 34; + return { item, top }; + }); useEffect(() => { onOpenChange(false); @@ -246,7 +243,7 @@ export function MobileNav({ }; }, [onOpenChange, open]); - if (!productItem || items.length === 0) { + if (items.length === 0) { return null; } @@ -272,18 +269,13 @@ export function MobileNav({

@@ -147,10 +151,11 @@ function CTA({ title = "Ready to ship your next agentic app in minutes?", actions, theme = "filled", + highlightedText = TITLE_HIGHLIGHT, }: CTAProps) { const bootstrapPromptApiPath = getBootstrapPromptApiPath(); const [copyState, setCopyState] = useState("idle"); - const { before, highlight, after } = titleSegments(title); + const { before, highlight, after } = titleSegments(title, highlightedText); const handleCopy = useCallback(async () => { if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { diff --git a/src/components/not-found-content.tsx b/src/components/not-found-content.tsx new file mode 100644 index 00000000..3768a90c --- /dev/null +++ b/src/components/not-found-content.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import Footer from "@/components/footer"; +import CTA from "@/components/home/cta"; + +export function NotFoundContent(): ReactNode { + return ( +
+
+
+
+

+ Error404 + : Page Not Found +

+

+ We know this isn't where you intended to land, but we hope + you have some fun while you're here. +

+ +
+
+
+
+ +
+
+
+ ); +} diff --git a/src/components/products/section-kicker.tsx b/src/components/products/section-kicker.tsx index a8185d57..091cc6df 100644 --- a/src/components/products/section-kicker.tsx +++ b/src/components/products/section-kicker.tsx @@ -4,12 +4,14 @@ type SectionKickerProps = { children: string; className?: string; index?: string; + font?: "mono" | "sans"; }; export function SectionKicker({ children, className, index, + font = "mono", }: SectionKickerProps) { if (index) { return ( @@ -28,7 +30,14 @@ export function SectionKicker({ return (