diff --git a/index.html b/index.html
index a0bb625..3c0ade9 100644
--- a/index.html
+++ b/index.html
@@ -69,14 +69,27 @@
to { transform: rotate(360deg); }
}
-
+
diff --git a/public/fonts/OpenDyslexic-Bold.otf b/public/fonts/OpenDyslexic-Bold.otf
new file mode 100644
index 0000000..fc14a5a
Binary files /dev/null and b/public/fonts/OpenDyslexic-Bold.otf differ
diff --git a/public/fonts/OpenDyslexic-Regular.otf b/public/fonts/OpenDyslexic-Regular.otf
new file mode 100644
index 0000000..039ceda
Binary files /dev/null and b/public/fonts/OpenDyslexic-Regular.otf differ
diff --git a/src/App.tsx b/src/App.tsx
index d75113e..b5d58c7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "@/hooks/useAuth";
import { PendoProvider } from "@/hooks/usePendo";
import { AppNavigationProvider } from "@/hooks/useAppNavigation";
+import { AccessibilityProvider } from "@/hooks/useAccessibility";
import { useWindowControlsOverlay } from "@/hooks/useWindowControlsOverlay";
import { ThemeProvider } from "next-themes";
import { PWAInstallBanner } from "@/components/PWAInstallBanner";
@@ -64,17 +65,19 @@ const AppContent = () => {
const App = () => (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/src/components/AccessibilitySettings.tsx b/src/components/AccessibilitySettings.tsx
new file mode 100644
index 0000000..17b510f
--- /dev/null
+++ b/src/components/AccessibilitySettings.tsx
@@ -0,0 +1,91 @@
+import { Type, Bold, ZoomIn } from "lucide-react";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+import { useAccessibility, FontFamily } from "@/hooks/useAccessibility";
+
+export function AccessibilitySettings() {
+ const {
+ fontFamily,
+ setFontFamily,
+ boldText,
+ setBoldText,
+ largeFont,
+ setLargeFont,
+ } = useAccessibility();
+
+ return (
+
+ {/* Font Selection */}
+
+
+
+ Font
+
+
value && setFontFamily(value as FontFamily)}
+ className="justify-start flex-wrap"
+ >
+
+ Default
+
+
+ OpenDyslexic
+
+
+ Arimo
+
+
+
+ OpenDyslexic is designed to help readers with dyslexia
+
+
+
+ {/* Bold All Text */}
+
+
+
+
+
+
+
+
+ Make all text bold for better readability
+
+
+
+
+
+
+ {/* Extra Large Text */}
+
+
+
+
+
+
+
+
+ Increase text size throughout the app
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ProfileModal.test.tsx b/src/components/ProfileModal.test.tsx
index 4ad07d7..7e8a53d 100644
--- a/src/components/ProfileModal.test.tsx
+++ b/src/components/ProfileModal.test.tsx
@@ -3,6 +3,7 @@ import { render, screen, waitFor, within, fireEvent } from '@testing-library/rea
import userEvent from '@testing-library/user-event';
import { ProfileModal } from './ProfileModal';
import { BrowserRouter } from 'react-router-dom';
+import { AccessibilityProvider } from '@/hooks/useAccessibility';
import React from 'react';
// Mock react-router-dom's useNavigate
@@ -63,7 +64,9 @@ const defaultProps = {
const renderProfileModal = (props = {}) => {
return render(
-
+
+
+
);
};
@@ -80,6 +83,20 @@ const clickMenuItem = async (user: ReturnType, label: st
}
};
+// Helper to navigate to Delete Account confirmation (now inline in Account view)
+const navigateToDeleteConfirm = async (user: ReturnType) => {
+ // Navigate to Account view first
+ const accountMenuItem = screen.getByText('Name, email, password').closest('button');
+ await user.click(accountMenuItem!);
+ // Click Delete Account button to show the inline confirmation
+ const deleteButton = screen.getByRole('button', { name: /^delete account$/i });
+ await user.click(deleteButton);
+ // Wait for the confirmation to appear
+ await waitFor(() => {
+ expect(screen.getByText(/delete your account permanently/i)).toBeInTheDocument();
+ });
+};
+
describe('ProfileModal', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -93,8 +110,7 @@ describe('ProfileModal', () => {
expect(screen.getByText('Settings')).toBeInTheDocument();
expect(screen.getByText('Account')).toBeInTheDocument();
expect(screen.getByText('Appearance')).toBeInTheDocument();
- // The delete account menu item
- expect(screen.getByText('Delete Account')).toBeInTheDocument();
+ expect(screen.getByText('Accessibility')).toBeInTheDocument();
});
it('displays user info', () => {
@@ -139,14 +155,30 @@ describe('ProfileModal', () => {
expect(screen.getByText('Theme')).toBeInTheDocument();
});
- it('navigates to Delete Account view when Delete Account is clicked', async () => {
+ it('navigates to Accessibility view when Accessibility is clicked', async () => {
const user = userEvent.setup();
renderProfileModal();
- // Find the Delete Account menu item in main view (not the destructive button in danger view)
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
+ // Find and click the Accessibility menu item
+ const accessibilityMenuItem = screen.getByText('Fonts, text size, contrast').closest('button');
+ await user.click(accessibilityMenuItem!);
+
+ expect(screen.getByText('Customize the app for your needs')).toBeInTheDocument();
+ });
+
+ it('opens Delete Account dialog from Account view', async () => {
+ const user = userEvent.setup();
+ renderProfileModal();
+ // Navigate to Account view first
+ const accountMenuItem = screen.getByText('Name, email, password').closest('button');
+ await user.click(accountMenuItem!);
+
+ // Click Delete Account button to open the AlertDialog
+ const deleteButton = screen.getByRole('button', { name: /delete account/i });
+ await user.click(deleteButton);
+
+ // AlertDialog should open
expect(screen.getByText(/delete your account permanently/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText('DELETE')).toBeInTheDocument();
});
@@ -263,13 +295,11 @@ describe('ProfileModal', () => {
});
describe('Account Deletion', () => {
- it('shows delete confirmation when navigating to Delete Account', async () => {
+ it('shows delete confirmation dialog from Account view', async () => {
const user = userEvent.setup();
renderProfileModal();
- // Find and click the Delete Account menu item
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
+ await navigateToDeleteConfirm(user);
expect(screen.getByText(/delete your account permanently/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText('DELETE')).toBeInTheDocument();
@@ -279,12 +309,11 @@ describe('ProfileModal', () => {
const user = userEvent.setup();
renderProfileModal();
- // Navigate to Delete Account view
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
+ await navigateToDeleteConfirm(user);
// The delete button should be disabled initially
- expect(screen.getByRole('button', { name: /delete account forever/i })).toBeDisabled();
+ const deleteBtn = await screen.findByText(/Delete Forever/i);
+ expect(deleteBtn.closest('button')).toBeDisabled();
// The input should have DELETE placeholder
expect(screen.getByPlaceholderText('DELETE')).toBeInTheDocument();
@@ -296,14 +325,13 @@ describe('ProfileModal', () => {
renderProfileModal();
- // Navigate to Delete Account view
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
+ await navigateToDeleteConfirm(user);
// Type DELETE and confirm
- const confirmInput = screen.getByRole('textbox');
+ const confirmInput = screen.getByPlaceholderText('DELETE');
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
- await user.click(screen.getByRole('button', { name: /delete account forever/i }));
+ const deleteForeverBtn = await screen.findByText(/Delete Forever/i);
+ await user.click(deleteForeverBtn.closest('button')!);
await waitFor(() => {
expect(mockRpc).toHaveBeenCalledWith('delete_own_account');
@@ -316,11 +344,11 @@ describe('ProfileModal', () => {
renderProfileModal();
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
- const confirmInput = screen.getByRole('textbox');
+ await navigateToDeleteConfirm(user);
+ const confirmInput = screen.getByPlaceholderText('DELETE');
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
- await user.click(screen.getByRole('button', { name: /delete account forever/i }));
+ const deleteForeverBtn = await screen.findByText(/Delete Forever/i);
+ await user.click(deleteForeverBtn.closest('button')!);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalled();
@@ -333,11 +361,11 @@ describe('ProfileModal', () => {
renderProfileModal();
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
- const confirmInput = screen.getByRole('textbox');
+ await navigateToDeleteConfirm(user);
+ const confirmInput = screen.getByPlaceholderText('DELETE');
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
- await user.click(screen.getByRole('button', { name: /delete account forever/i }));
+ const deleteForeverBtn = await screen.findByText(/Delete Forever/i);
+ await user.click(deleteForeverBtn.closest('button')!);
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/');
@@ -354,11 +382,11 @@ describe('ProfileModal', () => {
renderProfileModal();
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
- const confirmInput = screen.getByRole('textbox');
+ await navigateToDeleteConfirm(user);
+ const confirmInput = screen.getByPlaceholderText('DELETE');
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
- await user.click(screen.getByRole('button', { name: /delete account forever/i }));
+ const deleteForeverBtn = await screen.findByText(/Delete Forever/i);
+ await user.click(deleteForeverBtn.closest('button')!);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Database error');
@@ -375,11 +403,11 @@ describe('ProfileModal', () => {
renderProfileModal();
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
- const confirmInput = screen.getByRole('textbox');
+ await navigateToDeleteConfirm(user);
+ const confirmInput = screen.getByPlaceholderText('DELETE');
fireEvent.change(confirmInput, { target: { value: 'DELETE' } });
- await user.click(screen.getByRole('button', { name: /delete account forever/i }));
+ const deleteForeverBtn = await screen.findByText(/Delete Forever/i);
+ await user.click(deleteForeverBtn.closest('button')!);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Not authenticated');
@@ -389,22 +417,18 @@ describe('ProfileModal', () => {
expect(mockSignOut).not.toHaveBeenCalled();
});
- it('can go back from delete view', async () => {
+ it('can cancel delete dialog', async () => {
const user = userEvent.setup();
renderProfileModal();
- // Navigate to Delete Account view
- const deleteMenuItem = screen.getByText('Delete Account').closest('button');
- await user.click(deleteMenuItem!);
+ await navigateToDeleteConfirm(user);
expect(screen.getByText(/delete your account permanently/i)).toBeInTheDocument();
- // Find and click back button
- const backButtons = screen.getAllByRole('button');
- const backButton = backButtons.find(btn => btn.querySelector('.rotate-180'));
- await user.click(backButton!);
+ // Click Cancel button in the AlertDialog
+ await user.click(screen.getByRole('button', { name: /cancel/i }));
- // Should be back on main view
- expect(screen.getByText('Settings')).toBeInTheDocument();
+ // Dialog should close, we should still be in Account view
+ expect(screen.getByText('Display Name')).toBeInTheDocument();
});
});
});
diff --git a/src/components/ProfileModal.tsx b/src/components/ProfileModal.tsx
index 331cbae..95f7c38 100644
--- a/src/components/ProfileModal.tsx
+++ b/src/components/ProfileModal.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Dialog,
@@ -25,7 +25,9 @@ import {
Check,
Loader2,
LogOut,
+ Accessibility,
} from "lucide-react";
+import { AccessibilitySettings } from "@/components/AccessibilitySettings";
import { validateForumUsername } from "@/lib/validation";
import { cn } from "@/lib/utils";
@@ -42,7 +44,7 @@ interface ProfileModalProps {
onSignOut?: () => void;
}
-type SettingsView = "main" | "account" | "appearance" | "danger";
+type SettingsView = "main" | "account" | "appearance" | "accessibility";
export function ProfileModal({
open,
@@ -62,6 +64,7 @@ export function ProfileModal({
);
const [newEmail, setNewEmail] = useState("");
const [deleteConfirmText, setDeleteConfirmText] = useState("");
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Loading states
const [isUpdatingName, setIsUpdatingName] = useState(false);
@@ -74,6 +77,14 @@ export function ProfileModal({
const [isEditingName, setIsEditingName] = useState(false);
const [isEditingForumUsername, setIsEditingForumUsername] = useState(false);
+ // Reset delete confirmation when navigating away from account view
+ useEffect(() => {
+ if (currentView !== "account") {
+ setShowDeleteConfirm(false);
+ setDeleteConfirmText("");
+ }
+ }, [currentView]);
+
const handleUpdateDisplayName = async () => {
if (!displayName.trim()) {
toast.error("Display name cannot be empty");
@@ -428,10 +439,10 @@ export function ProfileModal({
onClick={() => setCurrentView("appearance")}
/>