From 4338b4d7014d871dd205a3a736d2c8e13695d3f3 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Fri, 5 Jun 2026 12:46:58 -0400 Subject: [PATCH 1/5] =?UTF-8?q?feat(bio):=20Phase=204=20=E2=80=94=20bio=20?= =?UTF-8?q?self-edit=20UI=20at=20/[lang]/my-bio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final phase of the cdcf-bio-edit-zitadel plan. Adds the TipTap-based editor backed by the Phase 3 REST endpoints. Authenticated team members can now click "Edit my bio" in the header dropdown, land on a language-switchable form, write in any of their available locales, and trigger the re-translation fan-out via Save. New surface ----------- - app/[lang]/my-bio/page.tsx Server component. requireSession() → fetchMyTeamMember() (discovery) → pick initial lang (Zitadel locale → URL locale → first available) → fetchTeamMemberPost() → render the editor. Friendly fallbacks for the no-link / no-langs / load-error paths surface localized copy instead of internal errors. - components/BioEditor.tsx Client. TipTap StarterKit + Link (rel=noopener, target=_blank, no autolink). Editable: post_content, member_title, member_linkedin_url, member_github_url. Read-only: post_title (the team member's name — admin-managed). Language switchLanguage(e.target.value)} + disabled={isLoadingLang || isSaving} + className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm focus:border-cdcf-navy focus:outline-none focus:ring-1 focus:ring-cdcf-navy" + > + {availableLanguages.map((lang) => ( + + ))} + + + + +

{t('intro')}

+ +
+
+ +

{currentTitle}

+
+ +
+ +
+ +
+
+ +
+ + updateField('member_title', e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-cdcf-navy focus:outline-none focus:ring-1 focus:ring-cdcf-navy" + /> +
+ +
+ + updateField('member_linkedin_url', e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-cdcf-navy focus:outline-none focus:ring-1 focus:ring-cdcf-navy" + /> +
+ +
+ + updateField('member_github_url', e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-cdcf-navy focus:outline-none focus:ring-1 focus:ring-cdcf-navy" + /> +
+
+ +
+
+ {status.kind === 'success' && ( + + {status.queued.length > 0 + ? t('savedQueued', { langs: status.queued.join(', ').toUpperCase() }) + : t('savedNoChange')} + + )} + {status.kind === 'error' && ( + {t('saveError', { error: status.message })} + )} +
+ +
+ + ) +} diff --git a/lib/bio-api.test.ts b/lib/bio-api.test.ts new file mode 100644 index 0000000..189c877 --- /dev/null +++ b/lib/bio-api.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { Session } from 'next-auth' + +// Mock getAccessToken so we can simulate a session with/without a +// usable bearer (the helper checks for an undefined return from this +// function to throw BioApiError(401)). The real auth-utils transitively +// imports next/navigation which fails outside Next; the mock skips that. +vi.mock('@/lib/auth-utils', () => ({ + getAccessToken: vi.fn(), +})) + +import { getAccessToken } from '@/lib/auth-utils' +import { + BioApiError, + fetchMyTeamMember, + fetchTeamMemberPost, + saveMyTeamMember, +} from './bio-api' + +const mockedGetAccessToken = vi.mocked(getAccessToken) + +const session: Session = { + accessToken: 'token-abc', + expires: '2099-01-01T00:00:00.000Z', +} + +let fetchMock: ReturnType + +beforeEach(() => { + vi.resetAllMocks() + process.env.WP_REST_URL = 'https://wp.example.org/wp-json' + mockedGetAccessToken.mockReturnValue('token-abc') + fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) +}) + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +// ─── fetchMyTeamMember ─────────────────────────────────────────────── + +describe('fetchMyTeamMember', () => { + it('attaches the bearer token and returns the parsed discovery payload', async () => { + fetchMock.mockResolvedValue( + jsonResponse(200, { + team_member_id: 702, + available_languages: [ + { slug: 'en', post_id: 702, title: 'Me', status: 'publish' }, + ], + }) + ) + + const result = await fetchMyTeamMember(session) + + expect(result.team_member_id).toBe(702) + expect(fetchMock).toHaveBeenCalledOnce() + const [url, init] = fetchMock.mock.calls[0] + expect(url).toBe('https://wp.example.org/wp-json/cdcf/v1/my-team-member') + expect((init as RequestInit).headers).toMatchObject({ + Authorization: 'Bearer token-abc', + }) + }) + + it('throws BioApiError(401) when the session has no access token', async () => { + mockedGetAccessToken.mockReturnValue(undefined) + + await expect(fetchMyTeamMember(session)).rejects.toMatchObject({ + name: 'BioApiError', + status: 401, + }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('propagates a 403 from WP as a BioApiError', async () => { + fetchMock.mockResolvedValue( + jsonResponse(403, { + code: 'rest_no_team_member_link', + message: 'Not linked', + }) + ) + + const err = await fetchMyTeamMember(session).catch((e) => e) + expect(err).toBeInstanceOf(BioApiError) + expect(err.status).toBe(403) + expect(err.code).toBe('rest_no_team_member_link') + }) + + it('throws when WP_REST_URL is unset', async () => { + delete process.env.WP_REST_URL + + const err = await fetchMyTeamMember(session).catch((e) => e) + expect(err).toBeInstanceOf(BioApiError) + expect(err.status).toBe(500) + expect(err.code).toBe('config_missing') + }) +}) + +// ─── fetchTeamMemberPost ───────────────────────────────────────────── + +describe('fetchTeamMemberPost', () => { + it('hits /wp/v2 with context=edit and normalises the response', async () => { + fetchMock.mockResolvedValue( + jsonResponse(200, { + id: 703, + title: { raw: 'Mein Name' }, + content: { raw: '

Hallo.

', rendered: '

Hallo.

' }, + acf: { + member_title: 'Theologe', + member_linkedin_url: 'https://linkedin.com/in/me', + member_github_url: '', + }, + }) + ) + + const post = await fetchTeamMemberPost(session, 703) + + expect(post).toEqual({ + id: 703, + title: 'Mein Name', + content: '

Hallo.

', + member_title: 'Theologe', + member_linkedin_url: 'https://linkedin.com/in/me', + member_github_url: '', + }) + const [url] = fetchMock.mock.calls[0] + expect(url).toBe( + 'https://wp.example.org/wp-json/wp/v2/team_member/703?context=edit' + ) + }) + + it('falls back to rendered title/content when raw is absent', async () => { + fetchMock.mockResolvedValue( + jsonResponse(200, { + id: 999, + title: { rendered: 'Rendered Name' }, + content: { rendered: '

Rendered.

' }, + acf: {}, + }) + ) + + const post = await fetchTeamMemberPost(session, 999) + + expect(post.title).toBe('Rendered Name') + expect(post.content).toBe('

Rendered.

') + expect(post.member_title).toBeUndefined() + }) + + it('propagates non-200 errors as BioApiError', async () => { + fetchMock.mockResolvedValue(jsonResponse(404, { code: 'rest_post_invalid_id' })) + + const err = await fetchTeamMemberPost(session, 1).catch((e) => e) + expect(err).toBeInstanceOf(BioApiError) + expect(err.status).toBe(404) + }) +}) + +// ─── saveMyTeamMember ──────────────────────────────────────────────── + +describe('saveMyTeamMember', () => { + it('PATCHes /cdcf/v1/my-team-member/{lang} with the payload', async () => { + fetchMock.mockResolvedValue( + jsonResponse(200, { + post_id: 703, + queued: ['en', 'it', 'es', 'fr', 'pt'], + errors: [], + }) + ) + + const result = await saveMyTeamMember(session, 'de', { + content: '

Bio.

', + member_title: 'Theologe', + }) + + expect(result.queued).toEqual(['en', 'it', 'es', 'fr', 'pt']) + const [url, init] = fetchMock.mock.calls[0] + expect(url).toBe('https://wp.example.org/wp-json/cdcf/v1/my-team-member/de') + const reqInit = init as RequestInit + expect(reqInit.method).toBe('PATCH') + expect(reqInit.headers).toMatchObject({ + Authorization: 'Bearer token-abc', + 'Content-Type': 'application/json', + }) + expect(JSON.parse(reqInit.body as string)).toEqual({ + content: '

Bio.

', + member_title: 'Theologe', + }) + }) + + it('rejects malformed lang slugs before touching the network', async () => { + const err = await saveMyTeamMember(session, 'deu', {}).catch((e) => e) + expect(err).toBeInstanceOf(BioApiError) + expect(err.status).toBe(400) + expect(err.code).toBe('invalid_lang') + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('surfaces the WP-side rest_invalid_url 400', async () => { + fetchMock.mockResolvedValue( + jsonResponse(400, { + code: 'rest_invalid_url', + message: 'LinkedIn URL must point at linkedin.com (or empty to clear).', + }) + ) + + const err = await saveMyTeamMember(session, 'en', { + member_linkedin_url: 'https://evil.example.org/in/me', + }).catch((e) => e) + expect(err).toBeInstanceOf(BioApiError) + expect(err.status).toBe(400) + expect(err.code).toBe('rest_invalid_url') + expect(err.message).toMatch(/LinkedIn/) + }) + + it('surfaces a 403 when the ownership invariant fails', async () => { + fetchMock.mockResolvedValue( + jsonResponse(403, { + code: 'rest_forbidden', + message: 'You do not own this team_member.', + }) + ) + + const err = await saveMyTeamMember(session, 'en', { content: '

x

' }) + .catch((e) => e) + expect(err.status).toBe(403) + expect(err.code).toBe('rest_forbidden') + }) +}) diff --git a/lib/bio-api.ts b/lib/bio-api.ts new file mode 100644 index 0000000..4930684 --- /dev/null +++ b/lib/bio-api.ts @@ -0,0 +1,187 @@ +// Server-only helpers for the bio self-edit flow. +// +// All three functions attach the caller's Zitadel access token from the +// Auth.js session as a Bearer header. The WP-side bearer validator +// (wordpress/themes/cdcf-headless/includes/auth/zitadel-bearer.php) then +// resolves the email claim to a WP user before the cdcf/v1 handler runs. +// These helpers are how the access token reaches WP without ever passing +// through the browser. + +import 'server-only' +import type { Session } from 'next-auth' +import { getAccessToken } from '@/lib/auth-utils' + +export type BioLanguage = { + slug: string + post_id: number + title: string + status: string +} + +export type BioDiscovery = { + team_member_id: number + available_languages: BioLanguage[] +} + +export type BioPostContent = { + id: number + title: string + content: string + member_title?: string + member_linkedin_url?: string + member_github_url?: string +} + +export type BioSaveResponse = { + post_id: number + queued: string[] + errors: string[] +} + +export class BioApiError extends Error { + constructor( + message: string, + readonly status: number, + readonly code?: string + ) { + super(message) + this.name = 'BioApiError' + } +} + +function getWpRestUrl(): string { + const url = process.env.WP_REST_URL + if (!url) { + throw new BioApiError('WP_REST_URL not configured', 500, 'config_missing') + } + return url.replace(/\/$/, '') +} + +function bearerHeader(session: Session | null | undefined): Record { + const token = getAccessToken(session) + if (!token) { + throw new BioApiError('No access token on session', 401, 'no_token') + } + return { Authorization: `Bearer ${token}` } +} + +async function readJson(response: Response): Promise { + try { + return await response.json() + } catch { + return null + } +} + +function toBioApiError(body: unknown, fallback: string, status: number): BioApiError { + if (body && typeof body === 'object') { + const obj = body as { message?: unknown; code?: unknown } + const message = typeof obj.message === 'string' ? obj.message : fallback + const code = typeof obj.code === 'string' ? obj.code : undefined + return new BioApiError(message, status, code) + } + return new BioApiError(fallback, status) +} + +export async function fetchMyTeamMember(session: Session | null): Promise { + const response = await fetch(`${getWpRestUrl()}/cdcf/v1/my-team-member`, { + headers: bearerHeader(session), + cache: 'no-store', + }) + const body = await readJson(response) + if (!response.ok) { + throw toBioApiError(body, 'Discovery request failed', response.status) + } + return body as BioDiscovery +} + +export async function fetchTeamMemberPost( + session: Session | null, + postId: number +): Promise { + // Hits the core /wp/v2 endpoint (NOT /cdcf/v1) — Polylang exposes the + // language siblings as independent posts so we can fetch the {lang} + // version directly. The ?_embed=false keeps the payload tight. + // Use context=edit so unfiltered content is returned for editing + // (default 'view' returns the rendered/sanitized HTML which we'd + // then re-edit, causing drift). + const response = await fetch( + `${getWpRestUrl()}/wp/v2/team_member/${postId}?context=edit`, + { + headers: bearerHeader(session), + cache: 'no-store', + } + ) + const body = await readJson(response) + if (!response.ok) { + throw toBioApiError(body, 'Failed to load post', response.status) + } + return normaliseTeamMemberPost(body) +} + +function normaliseTeamMemberPost(body: unknown): BioPostContent { + const obj = (body ?? {}) as { + id?: unknown + title?: { rendered?: unknown; raw?: unknown } + content?: { rendered?: unknown; raw?: unknown } + acf?: { + member_title?: unknown + member_linkedin_url?: unknown + member_github_url?: unknown + } + } + const titleSrc = + (typeof obj.title?.raw === 'string' && obj.title.raw) || + (typeof obj.title?.rendered === 'string' && obj.title.rendered) || + '' + const contentSrc = + (typeof obj.content?.raw === 'string' && obj.content.raw) || + (typeof obj.content?.rendered === 'string' && obj.content.rendered) || + '' + return { + id: typeof obj.id === 'number' ? obj.id : 0, + title: titleSrc, + content: contentSrc, + member_title: stringOrUndefined(obj.acf?.member_title), + member_linkedin_url: stringOrUndefined(obj.acf?.member_linkedin_url), + member_github_url: stringOrUndefined(obj.acf?.member_github_url), + } +} + +function stringOrUndefined(v: unknown): string | undefined { + return typeof v === 'string' ? v : undefined +} + +export type BioSavePayload = { + content?: string + member_title?: string + member_linkedin_url?: string + member_github_url?: string +} + +export async function saveMyTeamMember( + session: Session | null, + lang: string, + payload: BioSavePayload +): Promise { + if (!/^[a-z]{2}$/.test(lang)) { + throw new BioApiError('Invalid language slug', 400, 'invalid_lang') + } + const response = await fetch( + `${getWpRestUrl()}/cdcf/v1/my-team-member/${lang}`, + { + method: 'PATCH', + headers: { + ...bearerHeader(session), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + cache: 'no-store', + } + ) + const body = await readJson(response) + if (!response.ok) { + throw toBioApiError(body, 'Save failed', response.status) + } + return body as BioSaveResponse +} diff --git a/messages/de.json b/messages/de.json index 1e39b59..2da539b 100644 --- a/messages/de.json +++ b/messages/de.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Anmelden", "signOut": "Abmelden", - "loading": "Wird geladen\u2026" + "loading": "Wird geladen\u2026", + "editMyBio": "Meine Bio bearbeiten" + }, + "MyBio": { + "title": "Meine Bio bearbeiten", + "intro": "Aktualisiere deine Bio in der Sprache deiner Wahl \u2014 \u00dcbersetzungen in die anderen 5 Sprachen werden automatisch eingereiht.", + "languageLabel": "Sprache", + "postTitleLabel": "Name", + "contentLabel": "Bio", + "memberTitleLabel": "Position / Zugeh\u00f6rigkeit", + "linkedinLabel": "LinkedIn-URL", + "githubLabel": "GitHub-URL", + "save": "\u00c4nderungen speichern", + "saving": "Wird gespeichert\u2026", + "savedQueued": "Gespeichert. \u00dcbersetzungen eingereiht f\u00fcr {langs}.", + "savedNoChange": "Gespeichert.", + "saveError": "Speichern fehlgeschlagen: {error}", + "unsavedSwitch": "Du hast nicht gespeicherte \u00c4nderungen. Verwerfen und Sprache wechseln?", + "notLinked": "Dein Konto ist mit keinem team_member-Profil verkn\u00fcpft. Wende dich an einen Administrator.", + "noLanguages": "Es sind noch keine Sprachversionen zur Bearbeitung verf\u00fcgbar.", + "loadError": "Diese Version deiner Bio konnte nicht geladen werden." } } diff --git a/messages/en.json b/messages/en.json index 252f1b9..e05d30e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Sign in", "signOut": "Sign out", - "loading": "Loading\u2026" + "loading": "Loading\u2026", + "editMyBio": "Edit my bio" + }, + "MyBio": { + "title": "Edit my bio", + "intro": "Update your bio in the language you're most comfortable with \u2014 translations into the other 5 languages will be queued automatically.", + "languageLabel": "Language", + "postTitleLabel": "Name", + "contentLabel": "Bio", + "memberTitleLabel": "Position / affiliation", + "linkedinLabel": "LinkedIn URL", + "githubLabel": "GitHub URL", + "save": "Save changes", + "saving": "Saving\u2026", + "savedQueued": "Saved. Translations queued for {langs}.", + "savedNoChange": "Saved.", + "saveError": "Could not save: {error}", + "unsavedSwitch": "You have unsaved changes. Discard them and switch language?", + "notLinked": "Your account isn't linked to a team_member profile. Contact an administrator.", + "noLanguages": "No language versions are available to edit yet.", + "loadError": "Could not load this version of your bio." } } diff --git a/messages/es.json b/messages/es.json index dc69152..acaff2b 100644 --- a/messages/es.json +++ b/messages/es.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Iniciar sesi\u00f3n", "signOut": "Cerrar sesi\u00f3n", - "loading": "Cargando\u2026" + "loading": "Cargando\u2026", + "editMyBio": "Editar mi biograf\u00eda" + }, + "MyBio": { + "title": "Editar mi biograf\u00eda", + "intro": "Actualiza tu biograf\u00eda en el idioma que prefieras \u2014 las traducciones a los otros 5 idiomas se pondr\u00e1n en cola autom\u00e1ticamente.", + "languageLabel": "Idioma", + "postTitleLabel": "Nombre", + "contentLabel": "Biograf\u00eda", + "memberTitleLabel": "Cargo / afiliaci\u00f3n", + "linkedinLabel": "URL de LinkedIn", + "githubLabel": "URL de GitHub", + "save": "Guardar cambios", + "saving": "Guardando\u2026", + "savedQueued": "Guardado. Traducciones en cola para {langs}.", + "savedNoChange": "Guardado.", + "saveError": "No se pudo guardar: {error}", + "unsavedSwitch": "Tienes cambios sin guardar. \u00bfDescartarlos y cambiar de idioma?", + "notLinked": "Tu cuenta no est\u00e1 vinculada a un perfil de team_member. Contacta a un administrador.", + "noLanguages": "A\u00fan no hay versiones en otros idiomas para editar.", + "loadError": "No se pudo cargar esta versi\u00f3n de tu biograf\u00eda." } } diff --git a/messages/fr.json b/messages/fr.json index 4fcdf47..bd02243 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Se connecter", "signOut": "Se d\u00e9connecter", - "loading": "Chargement\u2026" + "loading": "Chargement\u2026", + "editMyBio": "Modifier ma bio" + }, + "MyBio": { + "title": "Modifier ma bio", + "intro": "Mettez \u00e0 jour votre bio dans la langue de votre choix \u2014 les traductions dans les 5 autres langues seront mises en file automatiquement.", + "languageLabel": "Langue", + "postTitleLabel": "Nom", + "contentLabel": "Bio", + "memberTitleLabel": "Fonction / affiliation", + "linkedinLabel": "URL LinkedIn", + "githubLabel": "URL GitHub", + "save": "Enregistrer", + "saving": "Enregistrement\u2026", + "savedQueued": "Enregistr\u00e9. Traductions mises en file pour {langs}.", + "savedNoChange": "Enregistr\u00e9.", + "saveError": "\u00c9chec de l'enregistrement : {error}", + "unsavedSwitch": "Vous avez des modifications non enregistr\u00e9es. Les abandonner et changer de langue ?", + "notLinked": "Votre compte n'est pas li\u00e9 \u00e0 un profil team_member. Contactez un administrateur.", + "noLanguages": "Aucune version linguistique n'est disponible \u00e0 la modification.", + "loadError": "Impossible de charger cette version de votre bio." } } diff --git a/messages/it.json b/messages/it.json index 24ef43c..7ba801e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Accedi", "signOut": "Esci", - "loading": "Caricamento\u2026" + "loading": "Caricamento\u2026", + "editMyBio": "Modifica la mia bio" + }, + "MyBio": { + "title": "Modifica la mia bio", + "intro": "Aggiorna la tua bio nella lingua che preferisci \u2014 le traduzioni nelle altre 5 lingue verranno accodate automaticamente.", + "languageLabel": "Lingua", + "postTitleLabel": "Nome", + "contentLabel": "Bio", + "memberTitleLabel": "Ruolo / affiliazione", + "linkedinLabel": "URL LinkedIn", + "githubLabel": "URL GitHub", + "save": "Salva", + "saving": "Salvataggio\u2026", + "savedQueued": "Salvato. Traduzioni accodate per {langs}.", + "savedNoChange": "Salvato.", + "saveError": "Impossibile salvare: {error}", + "unsavedSwitch": "Hai modifiche non salvate. Scartarle e cambiare lingua?", + "notLinked": "Il tuo account non \u00e8 collegato a un profilo team_member. Contatta un amministratore.", + "noLanguages": "Nessuna versione linguistica disponibile per la modifica.", + "loadError": "Impossibile caricare questa versione della tua bio." } } diff --git a/messages/pt.json b/messages/pt.json index bc3a26a..fcc5299 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -287,6 +287,26 @@ "Auth": { "signIn": "Entrar", "signOut": "Sair", - "loading": "A carregar\u2026" + "loading": "A carregar\u2026", + "editMyBio": "Editar a minha bio" + }, + "MyBio": { + "title": "Editar a minha bio", + "intro": "Atualize a sua bio no idioma que preferir \u2014 as tradu\u00e7\u00f5es para os outros 5 idiomas ser\u00e3o enfileiradas automaticamente.", + "languageLabel": "Idioma", + "postTitleLabel": "Nome", + "contentLabel": "Bio", + "memberTitleLabel": "Cargo / afilia\u00e7\u00e3o", + "linkedinLabel": "URL do LinkedIn", + "githubLabel": "URL do GitHub", + "save": "Guardar altera\u00e7\u00f5es", + "saving": "A guardar\u2026", + "savedQueued": "Guardado. Tradu\u00e7\u00f5es enfileiradas para {langs}.", + "savedNoChange": "Guardado.", + "saveError": "N\u00e3o foi poss\u00edvel guardar: {error}", + "unsavedSwitch": "Tem altera\u00e7\u00f5es por guardar. Descart\u00e1-las e mudar de idioma?", + "notLinked": "A sua conta n\u00e3o est\u00e1 ligada a um perfil team_member. Contacte um administrador.", + "noLanguages": "Ainda n\u00e3o h\u00e1 vers\u00f5es lingu\u00edsticas dispon\u00edveis para edi\u00e7\u00e3o.", + "loadError": "N\u00e3o foi poss\u00edvel carregar esta vers\u00e3o da sua bio." } } diff --git a/package-lock.json b/package-lock.json index 7ea347e..0a3877e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "0.1.0", "dependencies": { "@heroicons/react": "^2.2.0", + "@tiptap/extension-link": "^3.26.0", + "@tiptap/pm": "^3.26.0", + "@tiptap/react": "^3.26.0", + "@tiptap/starter-kit": "^3.26.0", "clsx": "^2.1.1", "next": "^16.2.6", "next-auth": "5.0.0-beta.31", @@ -555,6 +559,34 @@ "npm": ">=6.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT", + "optional": true + }, "node_modules/@formatjs/fast-memoize": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.2.tgz", @@ -3017,6 +3049,435 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tiptap/core": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.26.0.tgz", + "integrity": "sha512-7jTed/RirIVsp+lLdLvGzGqF3EBGpnGHGYKOwz6t28V2BIJLAFdUhfEVdWie7xPxQNWK0TP+fPlsqZS0vxfHBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.26.0.tgz", + "integrity": "sha512-57accpka9affjiJRjP2LMNCDJDTMjTvO23RJCxtP43sp9cTIZ7YZnyDfRxCINTRBNK0X4o4w2+emOLyRwsk3CA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.26.0.tgz", + "integrity": "sha512-j6CzTMofcGJ5iMoUgDRQpM0FkG00jBID3aKqs+UBbgtzLgtG/CI/91tMFv0XPC30LeFA895qYgvGZtHdejZhiQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.26.0.tgz", + "integrity": "sha512-H2E3Hp0lV79jQV8YGtdDJkXkUalXZeYzKCx+vCZlDpb2ChS7/rNT9YY7poRA1NlJLUO0DH1wbAnFhx9KZMUx5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.26.0.tgz", + "integrity": "sha512-Jv7BX+kBB2wUIvO/NhuUjv+T3kAed2Tjr664fgQ2zKT6X69jKIkYuCCedrIHuOyaOQ+SBDuH9h51wYv/E97QgQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.26.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.26.0.tgz", + "integrity": "sha512-VJYcV6rvjnENRTroOi9tDcHWW6G0pmCoRETwatlbgfDzuCmkTOwVwQjeJCXOVMMLNPzNiXZzibsRCUt+Azq/jw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.26.0.tgz", + "integrity": "sha512-WPN9iZ3UjeDD2ckDzSs9tleibXv0cLj7j575NxuvjhwZTehYGNeYDSUTi+6DQUG6bKbhGg9Wcei5H0131vvJHg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.26.0.tgz", + "integrity": "sha512-Xhd6DCjaxCN4otQNvV6qra+XuoIjk6Vyjm87E5xn5Y/BMw7UGAG7LTkk3C2IEvxKrVZwJjalfxEqdHOgXQzVfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.26.0.tgz", + "integrity": "sha512-rhAtp5J/YVDUCUIc5T7b0XY9dLeuI72JgOr53w0QQc0VA0uwbfTn7sx0LI9PDCE9uwmDH8H3snVRZRnAvlM8oA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.26.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.26.0.tgz", + "integrity": "sha512-reQ77NRYAOP7iPudsNbzLBuBTdL2aGxZzjccUFmE2lNdmwP23n9A/JhkuUhshVBs/6IozvahI+smG3Bnea0TCQ==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.26.0.tgz", + "integrity": "sha512-SIe68SDwx2fozt/XKG0FhCwzz/yRN6Bvo4D5TqvfDg6NK3PQb1DS4GN9PilmJqbY+kXryuiWEEJOWi7HpO8SuQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.26.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.26.0.tgz", + "integrity": "sha512-baXvv/rtOTVd2Axjb7Zbb41Y9Qmy3U2fP7EHqLuhViqGxVX8LwQtP0PHUXEZkPokbBpRez10+dmOlvvsYFKAZQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.26.0.tgz", + "integrity": "sha512-qenEQEgzE5FjQay/H6iKOnwIt6DPO27cS+v0mGhXmrL1MjrNER4X0ZkATJbVd0WA6ffsAGaP44NKYDworGeidw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.26.0.tgz", + "integrity": "sha512-a+N/C4wkQV+/8x4ShdoiC2JdTW3Tw84C5cAloYLFMeaWmRa2me9ACSI+zo0SO9bbH9RJwsoRp7eaxBbk27eF1Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.26.0.tgz", + "integrity": "sha512-s8oFpH+0xmhvY19f452/2dExO3p1tjxh761g6cg4irwEUNUEAJKF2VLcjiaeOhNJ+pmnQYxb+VSkwkXvO+7vHQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.26.0.tgz", + "integrity": "sha512-FA/d157aBxyvZFvsdc5eSu46tmHWXebAsqOQSvivOMyw+deBb00VlMsf+iD2J8+sekjbMYwx/hvbsu+xUoX43Q==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.26.0.tgz", + "integrity": "sha512-EM8woyHDNKLEQ+lWUEoDtA4KrwP6fei/mYX1NxseMzKHHo7LFecx7wk6sovAXZrUvdML/yFBihgiMiO5VIsfkg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.26.0.tgz", + "integrity": "sha512-MccGyj9HY4fkl04eIiFoTCkr8067Jku/VVdJNtRWW104Spx43C/7V2zpbxPvpcDhq3dW384fDxYXfpnb186xLg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.26.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.26.0.tgz", + "integrity": "sha512-oBcj6qaNrRHQ+N0+pDuOVAQa4Nx9r8Cm5ANvyM2lTpoy60sOLOizuVvcvw1andVxbSrsZ1N/Sk+RZWyv1uoWyQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.26.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.26.0.tgz", + "integrity": "sha512-ItLdFlcMsJz2vhbs1PcUfcN7nzVqGBOwPeCrrWxjrgscp+K3JoOGD+HhVVpBACOMwivUrlh8Ry5Ohvues2nOeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.26.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.26.0.tgz", + "integrity": "sha512-h8fYLikg4qN39IghQ1y9g+zzUsgxBpDi5YS3IZbWoxWYYx1YqLL8nAvOiPr7Us14aQ0TjA2/xY7zqmyf29rX1A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.26.0.tgz", + "integrity": "sha512-jUll3Pqhq7u1JKvO0B6USW/bmVmUsO6sRcxo/d5tXqLhS0tWAobOGoGU2IgwXnQDSjf+vF73RYD5tRGDLkRC9Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.26.0.tgz", + "integrity": "sha512-yZXdevp3/8omGbb40Z52VfvID+tsRNhPQ1GNUToD56XSr2BjdJyAzAb9rWGgDKgVMUPLgJ26yT0O278RFqOKhA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.26.0.tgz", + "integrity": "sha512-LlVkivH5cBwov/EMD8BL7ZRcU6YcadiSVIffLW1hyalw9YfhaFzoLxjtWhL7jiU/n2Kg+9dXSZxmV2hTeTwyrQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.26.0.tgz", + "integrity": "sha512-4wajuqnO2X0+LVvsBjW/xk3/tmdb16bNL939QhicAay4YYqXITeV2v3XJsryzmG4L5GkK1yLxvRGk4aLoxWrnA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.26.0.tgz", + "integrity": "sha512-q4RDeWwVrhOL0jJCGRgGxLSdjOYwzQ4h2InURZVhC66433ipcHd6f3bqSOhcXZ4r0sFmMNsuF7aZmUntjWLc7w==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.7", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.0", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.26.0.tgz", + "integrity": "sha512-NLPAG6tk4/AsfOsUNsbGqdgIHuGsD4A/hlYriozuo+LCAAduuluhzsL/MEHZXtFT4GXUOlCdaEqNCOrMuz/zaw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.26.0", + "@tiptap/extension-floating-menu": "^3.26.0" + }, + "peerDependencies": { + "@tiptap/core": "3.26.0", + "@tiptap/pm": "3.26.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.26.0.tgz", + "integrity": "sha512-o34EtMfqtBaljdmeElZsRG/067oGx9Zcq+j2GWo71KlZe22ga/ALexeTf1c+ETsjCxSTKR6eyQ4RZvz/2JpYfg==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.26.0", + "@tiptap/extension-blockquote": "^3.26.0", + "@tiptap/extension-bold": "^3.26.0", + "@tiptap/extension-bullet-list": "^3.26.0", + "@tiptap/extension-code": "^3.26.0", + "@tiptap/extension-code-block": "^3.26.0", + "@tiptap/extension-document": "^3.26.0", + "@tiptap/extension-dropcursor": "^3.26.0", + "@tiptap/extension-gapcursor": "^3.26.0", + "@tiptap/extension-hard-break": "^3.26.0", + "@tiptap/extension-heading": "^3.26.0", + "@tiptap/extension-horizontal-rule": "^3.26.0", + "@tiptap/extension-italic": "^3.26.0", + "@tiptap/extension-link": "^3.26.0", + "@tiptap/extension-list": "^3.26.0", + "@tiptap/extension-list-item": "^3.26.0", + "@tiptap/extension-list-keymap": "^3.26.0", + "@tiptap/extension-ordered-list": "^3.26.0", + "@tiptap/extension-paragraph": "^3.26.0", + "@tiptap/extension-strike": "^3.26.0", + "@tiptap/extension-text": "^3.26.0", + "@tiptap/extension-underline": "^3.26.0", + "@tiptap/extensions": "^3.26.0", + "@tiptap/pm": "^3.26.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3112,7 +3573,6 @@ "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3122,7 +3582,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3143,6 +3602,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", @@ -4605,7 +5070,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5585,6 +6049,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -7387,6 +7860,12 @@ "uc.micro": "^2.0.0" } }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8870,6 +9349,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/outdent": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", @@ -9201,6 +9686,145 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.7", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz", + "integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.8", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", + "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/protobufjs": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", @@ -9597,6 +10221,12 @@ "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10866,7 +11496,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -11074,6 +11703,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 340510c..3fd532e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ }, "dependencies": { "@heroicons/react": "^2.2.0", + "@tiptap/extension-link": "^3.26.0", + "@tiptap/pm": "^3.26.0", + "@tiptap/react": "^3.26.0", + "@tiptap/starter-kit": "^3.26.0", "clsx": "^2.1.1", "next": "^16.2.6", "next-auth": "5.0.0-beta.31", diff --git a/tests/stubs/server-only.ts b/tests/stubs/server-only.ts new file mode 100644 index 0000000..416997d --- /dev/null +++ b/tests/stubs/server-only.ts @@ -0,0 +1,3 @@ +// no-op stub for Vitest; the real package throws to prevent +// server-only modules from being bundled into the client. +export {} diff --git a/vitest.config.ts b/vitest.config.ts index 350a8c4..af42ddc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,18 @@ import { defineConfig } from 'vitest/config' +import path from 'node:path' export default defineConfig({ + resolve: { + alias: { + // Next.js's `server-only` package throws on import to guard + // against bundling server modules into the client. In Vitest + // (Node, no client bundle) that guard is a false positive, so + // alias the module to an empty stub. + 'server-only': path.resolve(__dirname, 'tests/stubs/server-only.ts'), + // Resolve the `@/` alias used by source for absolute imports. + '@': path.resolve(__dirname), + }, + }, test: { environment: 'node', globals: false, From 902bf71912b71e8334019852e619bea2de11f981 Mon Sep 17 00:00:00 2001 From: "John R. D'Orazio" Date: Fri, 5 Jun 2026 13:19:42 -0400 Subject: [PATCH 2/5] fix(bio): address Codacy + CodeRabbit findings on PR #174 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings verified valid, one skipped with reason. [HIGH, no-implicit-any] app/[lang]/my-bio/page.tsx `let discovery` was inferred as `any`. Added the BioDiscovery type import and an explicit annotation `let discovery: BioDiscovery`. [HIGH, jsx-a11y/label-has-associated-control] components/BioEditor.tsx The "Name" label for the read-only display had no associated input (the team member's display name is admin-managed, so there's no control to label). Swapped