From d013ddf84febef7684917412e8ef2afcbda40b4d Mon Sep 17 00:00:00 2001 From: Ilya Topilskii Date: Tue, 8 Sep 2026 15:42:46 +0200 Subject: [PATCH 01/11] feat: add MVP and Student Fellows pages --- .gitignore | 1 + docs/community-backend.md | 65 +++ docs/community-design.md | 142 ++++++ next-sitemap.config.cjs | 5 + package.json | 1 + pnpm-lock.yaml | 8 + public/img/community/build.svg | 20 + public/img/community/learn.svg | 13 + public/img/community/linkedin.svg | 3 + public/img/community/membership-rule.svg | 3 + public/img/community/mvp-badge.svg | 86 ++++ public/img/community/share.svg | 14 + public/img/community/student-badge.png | Bin 0 -> 9958 bytes public/img/community/x.svg | 10 + scripts/normalize-appkit-doc-links.mjs | 32 ++ scripts/sync-appkit-docs.mjs | 42 +- src/app/(website)/mvps/directory/page.tsx | 42 ++ src/app/(website)/mvps/page.tsx | 29 ++ .../student-fellows/fellows/[slug]/page.tsx | 95 ++++ .../student-fellows/fellows/page.tsx | 44 ++ src/app/(website)/student-fellows/page.tsx | 29 ++ src/app/community-sitemap.xml/route.ts | 47 ++ src/components/community/community-cta.tsx | 53 +++ src/components/community/directory-hero.tsx | 24 + src/components/community/directory-query.ts | 55 +++ .../community/more-person-links.tsx | 59 +++ src/components/community/people-directory.tsx | 268 +++++++++++ src/components/community/person-card.tsx | 148 ++++++ src/components/community/program-benefits.tsx | 145 ++++++ src/components/community/program-hero.tsx | 65 +++ .../community/program-requirements.tsx | 173 +++++++ src/components/community/student-profile.tsx | 154 +++++++ src/components/footer.tsx | 10 +- src/components/home/cta.tsx | 17 +- src/components/ui/dropdown-menu.tsx | 9 +- src/css/custom.css | 2 + src/lib/community/people.server.ts | 106 +++++ src/lib/community/schema.ts | 106 +++++ tests/appkit-link-normalization.test.ts | 102 ++++ tests/broken-links.test.ts | 18 +- tests/community-api.test.ts | 87 ++++ tests/community-contract.test.ts | 96 ++++ tests/community-sitemap.test.ts | 48 ++ tests/e2e/community.spec.ts | 435 ++++++++++++++++++ tests/e2e/navigation.spec.ts | 8 + tests/e2e/pages.spec.ts | 22 +- tests/smoke.test.ts | 6 + 47 files changed, 2907 insertions(+), 40 deletions(-) create mode 100644 docs/community-backend.md create mode 100644 docs/community-design.md create mode 100644 public/img/community/build.svg create mode 100644 public/img/community/learn.svg create mode 100644 public/img/community/linkedin.svg create mode 100644 public/img/community/membership-rule.svg create mode 100644 public/img/community/mvp-badge.svg create mode 100644 public/img/community/share.svg create mode 100644 public/img/community/student-badge.png create mode 100644 public/img/community/x.svg create mode 100644 scripts/normalize-appkit-doc-links.mjs create mode 100644 src/app/(website)/mvps/directory/page.tsx create mode 100644 src/app/(website)/mvps/page.tsx create mode 100644 src/app/(website)/student-fellows/fellows/[slug]/page.tsx create mode 100644 src/app/(website)/student-fellows/fellows/page.tsx create mode 100644 src/app/(website)/student-fellows/page.tsx create mode 100644 src/app/community-sitemap.xml/route.ts create mode 100644 src/components/community/community-cta.tsx create mode 100644 src/components/community/directory-hero.tsx create mode 100644 src/components/community/directory-query.ts create mode 100644 src/components/community/more-person-links.tsx create mode 100644 src/components/community/people-directory.tsx create mode 100644 src/components/community/person-card.tsx create mode 100644 src/components/community/program-benefits.tsx create mode 100644 src/components/community/program-hero.tsx create mode 100644 src/components/community/program-requirements.tsx create mode 100644 src/components/community/student-profile.tsx create mode 100644 src/lib/community/people.server.ts create mode 100644 src/lib/community/schema.ts create mode 100644 tests/appkit-link-normalization.test.ts create mode 100644 tests/community-api.test.ts create mode 100644 tests/community-contract.test.ts create mode 100644 tests/community-sitemap.test.ts create mode 100644 tests/e2e/community.spec.ts 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..627382cb --- /dev/null +++ b/docs/community-backend.md @@ -0,0 +1,65 @@ +# Community backend integration + +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 and Student Fellows pages read published profiles from the separate +`devhub-backend` service. The website contains no editable profile database or +duplicated data snapshot. + +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. + +Requests are rendered on the server, time out after eight seconds, and use +`cache: "no-store"`. Admin edits therefore appear on the next page request. +Search and pagination stay in the page URL for navigation and sharing. +Service errors must display an unavailable state, while an actual detail 404 +must render the website's not-found page. An unavailable service must never be +presented as a genuinely empty directory. + +`/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; website profile requests remain uncached. + +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..01569c41 --- /dev/null +++ b/docs/community-design.md @@ -0,0 +1,142 @@ +# MVP and Student Fellows design implementation + +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 NativeSelect, Input, Button; native GET form, including a keyboard-operable magnifier submit button | +| People cards | Custom composition | `person-card.tsx`, backend photographs, actual names and organization, 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 five route files own metadata and visible section order. API fetch/schema implementation stays 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 + +- Directories request published records through `getPeople`, `getFacets`, and the parent SDK. They never contain a frontend seed or synthetic participant records. +- Filters are City and Country in both directories; the native form submits `q`, `city`, and `country`. +- The list uses 20 records per page, matching the Figma four-column/five-row layout. `page`, search, and filters round-trip in the URL; submitting filters starts on page 1. Pagination preserves the active search/filter values. An out-of-range page redirects to the last available page. +- Query values are normalized from Next.js `searchParams`; page is a safe integer in 1–100000, `q` is capped at 200 characters, City/Country 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. +- `CommunityApiError` produces a visible nonfatal error panel with a real reload link. Empty successful searches have their own “No matches found” state and clear-filters link. Unknown student slugs remain 404s, distinct from service outages. +- The parent SDK uses `cache: "no-store"`; admin updates are visible on subsequent navigation. Server requests use the configured backend origin and do not expose its configuration in client controls. +- Every page has a specific title, description, and canonical. Directory query URLs use `noindex, follow`; 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. +- “Explore the Program” scrolls to `#program`; “Browse fellow profiles” navigates to the internal directory. + +## Assets, accessibility, and deviations + +Local assets under `public/img/community/`: MVP badge SVG; Student badge PNG; `build.svg`, `share.svg`, `learn.svg`; LinkedIn and X SVG glyphs; `membership-rule.svg`. These are actual Figma exports, not approximate redrawn icons. The Student badge is 116 × 138 PNG, matching its displayed size; a higher-resolution/vector replacement can improve high-DPI rendering later. + +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. The source agent reported 110 delivered photos totaling approximately 10 MB, so no frontend image optimizer or permissive remote-host rule is needed for this delivery. Future arbitrary admin uploads should retain server-side image size limits/optimization; the frontend does not enforce upload limits. Missing photos show a neutral “Photo unavailable” slot and never invent a 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 native selects and search input; Enter or magnifier submit; normal link navigation and pagination; current-page ARIA; named social links; decorative assets with empty alt; real person-name alt text; existing skip-to-main link. 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. diff --git a/next-sitemap.config.cjs b/next-sitemap.config.cjs index dcad7b35..bcb1acff 100644 --- a/next-sitemap.config.cjs +++ b/next-sitemap.config.cjs @@ -9,6 +9,11 @@ module.exports = { generateRobotsTxt: true, trailingSlash: false, exclude: ["/hackathon", "/hackathon/*"], + robotsTxtOptions: { + additionalSitemaps: process.env.DEVHUB_BACKEND_URL + ? [`${siteUrl}/community-sitemap.xml`] + : [], + }, transform: async (config, path) => ({ loc: path, changefreq: config.changefreq, 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/build.svg b/public/img/community/build.svg new file mode 100644 index 00000000..9274a9a9 --- /dev/null +++ b/public/img/community/build.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/community/learn.svg b/public/img/community/learn.svg new file mode 100644 index 00000000..06c9e66b --- /dev/null +++ b/public/img/community/learn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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/share.svg b/public/img/community/share.svg new file mode 100644 index 00000000..012f152d --- /dev/null +++ b/public/img/community/share.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/img/community/student-badge.png b/public/img/community/student-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba2a2441fc52cdaf67e4b9ef4f37623ba4904dc GIT binary patch literal 9958 zcmVz1^@s6djf7w00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP zy?RnoQX(y0{659R#8N{;gObo0GhU#{lP6JdaIo8UR{q5X15rv!3N2l_jONUFi5dzT z1ffu`AAY!)B`)TY+JjL1XXVXkG77OE739`;)N4XP;0GVP?^LzA0|>QP%w#rO`L%@j z9`I(Q)vMP~eEgJpZC~r94OMnBDd|w9$y6|zO#Huv$u&@3UY=}s+%c@)+gFp(X!;w~ zBTSk!iB_!mqRK=9u%8wSt6U5IfW%s;q{K|w*}3xm+H0?+JMSF9v1=wJB{+SfC@+`jhn3p+D zP~Jra1w|~ag}jxp@ERzhbvXI?`O%UkOWARHxz5|yI;(PFC#Oz*hRIw^ojZ4~)Uk9h zfoSnOqW^wFtB;(fQS0~8!hOtm!W@KUM1aW4IRU}iBR*SqZ zVWQ8SKTkzPMbxifU;67`pOVw3rXH)asl+-dp<~93W%W9@!g#VA&ZiN5{4UYXEwnx@ zgXZi>kykx>1P9Q{>$_1*Q$Ml{7(&Ij-$&-)P?pjXCXQ8ZNwG+0iM(R7@MlG`wQALh zqN1YebeL182z8aNs}Rag{zv&wB9ZPerfoRQ>KQA<7T$I*<=;A*C9_zFHJi;; zSXf9FR>MUsrA0+0BBccd`Q+>CL($RE6cG_2#10)gjFOX+sIH(+5DGgvW5#qEH*OqN z8ap?$RQ?}joIXJd51bGpGntf2x-OGBwp}>Q>=8@mkd~Bp^F5T)tG}I4I7*OK&8})h zW5atNA8$z``uh6Pzy7t9UU}seI)3~Z)i!FCP}s@Ilb^7q>w-wU5{ZmCmZ&I1hMi=G zhYs!*wK{(5K}yXnpsE1G&gkBj#;$1gw}Cy-ZISS0O=iVEdD)Gx?} z0)-!zy>Es_wYfTb4cLPaVFhbs2= z_NF#%+OR5)p<~BVh1AbK|D0+AHBPABPKrw7o5R5aKj8+>tD49tgz|d>6N*iw)rM8e zDrG0D3T0ngw{A`8U9Y1v{f3CcgzpP7HByT;qDwRt41)dqe5qZ#4nl51!Y+FF;R$r& z#Br)g)L7U8_4>*ybLj22-{L1ZlJZ&7u#tho27^^36J&xw=?!5C6+V&9N!&y9_h-d> zU9jhfgdrQwdM<3(osmPUkDsRy&hrk)Jwwg+Zzr>F6UybJghk@k5_tgeJ>dykt!9Eo zK|Fi<^cl9fd8~#z(PNK2Mv;-NY2Utml$DiDHGvwDP}Qx~tXVTE$j_tv{9JyLOl0yh zv20rLR1t27iF-Z_%p!U~_(3BB$AHFv&9~dh_#H`$By*tv#s1}sowlgJN zcME0t`b+9ibCfh^2_!=Wt2h>~VPRp^zI{6(dFj%n+#~ca@-#e3sJ>e(FE^WVcr!ry zj7G_)dV6_m_ENgByyU4Da`W=Z^!2A?`gv`&+sUdzALg9t(1Vh?_ow_OLAG>ZiAbsP zfHgwRqL!hCJ9Oy4>bniw*JHF`!2(*jawT~R9wAhpt}84opsdV`RKm^!ScK$hwvucP zkV&M{BvNT4pZ|VKUf+H}nOT{Y4nt}JGrGsJHykcWhNQ0j=~Vl!VqcXlg=FGyNGsH{ z)=g*T@Wzk2bm>B&p`o;D)mKdHLbk6bsk%|Egu+h7$3I2SJ@=gaxQiFg3ZW*Wa%?om z$6zDYWMV=M+TY$s6UlGI5;C1kr6tK}tX>b-DI)dIhdKF8o6(-`gXmdeulZk{7oP_~d=^PCno=U3a+{9N*(0|iyVfe(o#;OS0{p4jn9h=q71%?b53W| zh#)^2*P;n6JeNajGOCt@a0b17-Or>RjP64D1}%=($hF7=jL-(E*(x2eR<^HAsAIDsNPP(YrS~k3>CAT#23d8fb@y;LrN-*9Dy*wES-KkwoveY zKBuhgY>(JU5IH%lDfRUCq4*OSlv-qV_>_&n^eBuSg^2`UlY7j@M99 ztF|_kYqg~jExOhgAscELX~h2h2e2CU7l-N1H{YaFr%q8-P-#N-={j@*pF4MkaAm`oG49i+MYk9oxDiShBGq0Rg#mCv1!@A~|~Oym@n)Nw5W zXmwUTWtywUH^gsRbK<-tCPst>QvbBW)T}6vGMlxaA}=4gH;`B@1Ey�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 literal 0 HcmV?d00001 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/page.tsx b/src/app/(website)/mvps/directory/page.tsx new file mode 100644 index 00000000..644012fa --- /dev/null +++ b/src/app/(website)/mvps/directory/page.tsx @@ -0,0 +1,42 @@ +import { getMetadata } from "@/lib/get-metadata"; +import { BrandStrip } from "@/components/ui/brand-strip"; +import { CommunityCTA } from "@/components/community/community-cta"; +import { DirectoryHero } from "@/components/community/directory-hero"; +import { + hasDirectoryFilters, + type DirectorySearchParams, +} from "@/components/community/directory-query"; +import { PeopleDirectory } from "@/components/community/people-directory"; +import Footer from "@/components/footer"; + +type PageProps = { searchParams: Promise }; + +export async function generateMetadata({ searchParams }: PageProps) { + const params = await searchParams; + const metadata = getMetadata({ + title: "Meet the Databricks MVPs", + description: + "Meet experts who share knowledge, build community, and grow the Databricks ecosystem.", + pathname: "/mvps/directory", + }); + return { + ...metadata, + ...(hasDirectoryFilters(params) + ? { robots: { index: false, follow: true } } + : {}), + }; +} + +export default async function MVPDirectoryPage({ searchParams }: PageProps) { + return ( +
+ + +
+ + +
+
+
+ ); +} diff --git a/src/app/(website)/mvps/page.tsx b/src/app/(website)/mvps/page.tsx new file mode 100644 index 00000000..1bbdfaec --- /dev/null +++ b/src/app/(website)/mvps/page.tsx @@ -0,0 +1,29 @@ +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.", + pathname: "/mvps", +}); + +export default function MVPPage() { + return ( +
+ + + +
+ + +
+
+
+ ); +} diff --git a/src/app/(website)/student-fellows/fellows/[slug]/page.tsx b/src/app/(website)/student-fellows/fellows/[slug]/page.tsx new file mode 100644 index 00000000..83cfff9b --- /dev/null +++ b/src/app/(website)/student-fellows/fellows/[slug]/page.tsx @@ -0,0 +1,95 @@ +import { notFound } from "next/navigation"; + +import { CommunityApiError, getPerson } from "@/lib/community/people.server"; +import { absoluteSiteUrl, getMetadata } from "@/lib/get-metadata"; +import { BackLink } from "@/components/ui/back-link"; +import { BrandStrip } from "@/components/ui/brand-strip"; +import { CommunityCTA } from "@/components/community/community-cta"; +import { CommunityUnavailable } from "@/components/community/people-directory"; +import { StudentProfile } from "@/components/community/student-profile"; +import Footer from "@/components/footer"; + +type PageProps = { params: Promise<{ slug: string }> }; + +export async function generateMetadata({ params }: PageProps) { + const { slug } = await params; + let person; + try { + person = await getPerson(slug); + } catch (error) { + if (!(error instanceof CommunityApiError)) throw error; + return getMetadata({ + title: "Student Fellow", + description: "Databricks Student Fellows directory", + pathname: `/student-fellows/fellows/${slug}`, + noIndex: true, + }); + } + if (!person || person.kind !== "student") notFound(); + return getMetadata({ + title: `${person.name} — Student Fellow`, + description: ( + person.headline || + person.bio || + `${person.name}, Databricks Student Fellow.` + ).slice(0, 160), + pathname: `/student-fellows/fellows/${person.slug}`, + }); +} + +export default async function StudentProfilePage({ params }: PageProps) { + const { slug } = await params; + let person; + try { + person = await getPerson(slug); + } catch (error) { + if (!(error instanceof CommunityApiError)) throw error; + return ( +
+
+ Back to fellows +

Student Fellow

+ +
+
+
+ ); + } + if (!person || person.kind !== "student") notFound(); + const structuredData = { + "@context": "https://schema.org", + "@type": "ProfilePage", + url: absoluteSiteUrl(`/student-fellows/fellows/${person.slug}`), + mainEntity: { + "@type": "Person", + name: person.name, + description: person.headline || undefined, + image: person.photoUrl || undefined, + sameAs: [ + ...Object.values(person.links).filter(Boolean), + ...person.additionalLinks.map((link) => link.url), + ], + affiliation: person.organization + ? { "@type": "EducationalOrganization", name: person.organization } + : undefined, + }, + }; + return ( +
+