diff --git a/src/app/settings/page.test.tsx b/src/app/settings/page.test.tsx index a30224c..dc4a0d4 100644 --- a/src/app/settings/page.test.tsx +++ b/src/app/settings/page.test.tsx @@ -3,7 +3,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen, waitFor, within } from '@testing-library/react' import { axe } from 'vitest-axe' import SettingsPage from './page' @@ -23,6 +23,7 @@ vi.mock('@/hooks/use-auth', () => { handle: 'alice.bsky.social', displayName: 'Alice', avatarUrl: null, + role: 'user' as const, }, isAuthenticated: true, isLoading: false, @@ -81,6 +82,13 @@ describe('SettingsPage', () => { expect(screen.getByLabelText(/maturity level/i)).toBeInTheDocument() }) + it('renders age bracket dropdown', async () => { + render() + await waitFor(() => { + expect(screen.getByLabelText(/age bracket/i)).toBeInTheDocument() + }) + }) + it('renders muted words input', async () => { render() await waitFor(() => { @@ -88,6 +96,16 @@ describe('SettingsPage', () => { }) }) + it('renders blocked users section with handle-based input', async () => { + render() + await waitFor(() => { + expect(screen.getByText('Blocked users')).toBeInTheDocument() + }) + const safetyFieldset = screen.getByText('Content Safety').closest('fieldset')! + expect(within(safetyFieldset).getByLabelText('Handle to block')).toBeInTheDocument() + expect(within(safetyFieldset).getByText('No users blocked.')).toBeInTheDocument() + }) + it('renders cross-posting section', async () => { render() await waitFor(() => { diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index b0162b2..e567455 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -35,6 +35,7 @@ export default function SettingsPage() { values, setValues, loading, + declaredAge, savingCommunity, communityError, communitySuccess, @@ -45,6 +46,9 @@ export default function SettingsPage() { showCrossPostAuthDialog, setShowCrossPostAuthDialog, crossPostScopesGranted, + handleBlockUser, + handleUnblockUser, + handleAgeChange, handleSaveCommunitySettings, handleSaveGlobalSettings, handleAgeConfirm, @@ -187,11 +191,14 @@ export default function SettingsPage() {
setValues({ ...values, maturityLevel: level })} + onAgeChange={handleAgeChange} onMutedWordsChange={(words) => setValues({ ...values, mutedWords: words })} - onBlockedDidsChange={(dids) => setValues({ ...values, blockedDids: dids })} + onBlockUser={handleBlockUser} + onUnblockUser={handleUnblockUser} />
diff --git a/src/components/age-gate-dialog.tsx b/src/components/age-gate-dialog.tsx index 544db1a..745ea0f 100644 --- a/src/components/age-gate-dialog.tsx +++ b/src/components/age-gate-dialog.tsx @@ -9,19 +9,10 @@ import { useState } from 'react' import { cn } from '@/lib/utils' +import { AGE_OPTIONS } from '@/lib/constants' import { declareAge } from '@/lib/api/client' import { useAuth } from '@/hooks/use-auth' -/** Valid age bracket options. 0 = "Rather not say". */ -const AGE_OPTIONS = [ - { value: 0, label: 'Rather not say' }, - { value: 13, label: '13+' }, - { value: 14, label: '14+' }, - { value: 15, label: '15+' }, - { value: 16, label: '16+' }, - { value: 18, label: '18+' }, -] as const - interface AgeGateDialogProps { open: boolean onConfirm: (declaredAge: number) => void diff --git a/src/components/onboarding-field-input.tsx b/src/components/onboarding-field-input.tsx index 2fe438a..7c1862b 100644 --- a/src/components/onboarding-field-input.tsx +++ b/src/components/onboarding-field-input.tsx @@ -4,15 +4,7 @@ */ import type { OnboardingField } from '@/lib/api/types' - -const AGE_OPTIONS = [ - { value: 0, label: 'Rather not say' }, - { value: 13, label: '13+' }, - { value: 14, label: '14+' }, - { value: 15, label: '15+' }, - { value: 16, label: '16+' }, - { value: 18, label: '18+' }, -] as const +import { AGE_OPTIONS } from '@/lib/constants' const INPUT_CLASS = 'mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground' diff --git a/src/components/settings/content-safety-section.tsx b/src/components/settings/content-safety-section.tsx index 024ca9a..216f930 100644 --- a/src/components/settings/content-safety-section.tsx +++ b/src/components/settings/content-safety-section.tsx @@ -1,35 +1,72 @@ /** - * ContentSafetySection - Maturity level selector, muted words, blocked users. + * ContentSafetySection - Age bracket, maturity level, muted words, blocked users. * @see specs/prd-web.md Section M8 */ 'use client' import { cn } from '@/lib/utils' +import { AGE_OPTIONS } from '@/lib/constants' +import { UserBlockList } from './user-block-list' +import type { AuthorProfile } from '@/lib/api/types' type MaturityLevel = 'sfw' | 'sfw-mature' interface ContentSafetySectionProps { maturityLevel: MaturityLevel + declaredAge: number | null mutedWords: string - blockedDids: string + blockedUsers: AuthorProfile[] onMaturityChange: (level: MaturityLevel) => void + onAgeChange: (age: number) => void onMutedWordsChange: (words: string) => void - onBlockedDidsChange: (dids: string) => void + onBlockUser: (handle: string) => Promise + onUnblockUser: (did: string) => void } export function ContentSafetySection({ maturityLevel, + declaredAge, mutedWords, - blockedDids, + blockedUsers, onMaturityChange, + onAgeChange, onMutedWordsChange, - onBlockedDidsChange, + onBlockUser, + onUnblockUser, }: ContentSafetySectionProps) { return (
Content Safety +
+ + +

+ Your age bracket determines which content is available. Selecting "Rather not + say" restricts you to safe content only. +

+
+
-
- -