Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,27 @@
to { transform: rotate(360deg); }
}
</style>
<!-- Prevent theme flash by setting theme class before React loads -->
<!-- Prevent theme and accessibility flash by setting classes before React loads -->
<script>
(function() {
try {
// Theme
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// Accessibility: Font family
var font = localStorage.getItem('accessibility-font-family');
if (font === 'dyslexic') document.documentElement.classList.add('font-dyslexic');
if (font === 'arimo') document.documentElement.classList.add('font-arimo');
// Accessibility: Bold text
if (localStorage.getItem('accessibility-bold-text') === 'true') {
document.documentElement.classList.add('font-bold');
}
// Accessibility: Large font
if (localStorage.getItem('accessibility-large-font') === 'true') {
document.documentElement.classList.add('font-xl');
}
} catch (e) {}
})();
</script>
Expand Down
Binary file added public/fonts/OpenDyslexic-Bold.otf
Binary file not shown.
Binary file added public/fonts/OpenDyslexic-Regular.otf
Binary file not shown.
25 changes: 14 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -64,17 +65,19 @@ const AppContent = () => {

const App = () => (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem={true}>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<PendoProvider>
<AppNavigationProvider>
<TooltipProvider>
<AppContent />
</TooltipProvider>
</AppNavigationProvider>
</PendoProvider>
</AuthProvider>
</QueryClientProvider>
<AccessibilityProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<PendoProvider>
<AppNavigationProvider>
<TooltipProvider>
<AppContent />
</TooltipProvider>
</AppNavigationProvider>
</PendoProvider>
</AuthProvider>
</QueryClientProvider>
</AccessibilityProvider>
</ThemeProvider>
);

Expand Down
91 changes: 91 additions & 0 deletions src/components/AccessibilitySettings.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-6">
{/* Font Selection */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Type className="w-4 h-4" />
Font
</div>
<ToggleGroup
type="single"
value={fontFamily}
onValueChange={(value) => value && setFontFamily(value as FontFamily)}
className="justify-start flex-wrap"
>
<ToggleGroupItem value="default" aria-label="Default font" className="text-xs">
Default
</ToggleGroupItem>
<ToggleGroupItem value="dyslexic" aria-label="OpenDyslexic font" className="text-xs">
OpenDyslexic
</ToggleGroupItem>
<ToggleGroupItem value="arimo" aria-label="Arimo font" className="text-xs">
Arimo
</ToggleGroupItem>
</ToggleGroup>
<p className="text-xs text-muted-foreground">
OpenDyslexic is designed to help readers with dyslexia
</p>
</div>

{/* Bold All Text */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<Bold className="w-4 h-4 text-primary" />
</div>
<div className="space-y-0.5">
<Label htmlFor="bold-text" className="text-sm font-medium cursor-pointer">
Bold All Text
</Label>
<p className="text-xs text-muted-foreground">
Make all text bold for better readability
</p>
</div>
</div>
<Switch
id="bold-text"
checked={boldText}
onCheckedChange={setBoldText}
/>
</div>

{/* Extra Large Text */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<ZoomIn className="w-4 h-4 text-primary" />
</div>
<div className="space-y-0.5">
<Label htmlFor="large-font" className="text-sm font-medium cursor-pointer">
Extra Large Text
</Label>
<p className="text-xs text-muted-foreground">
Increase text size throughout the app
</p>
</div>
</div>
<Switch
id="large-font"
checked={largeFont}
onCheckedChange={setLargeFont}
/>
</div>
</div>
);
}
116 changes: 70 additions & 46 deletions src/components/ProfileModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,7 +64,9 @@ const defaultProps = {
const renderProfileModal = (props = {}) => {
return render(
<BrowserRouter>
<ProfileModal {...defaultProps} {...props} />
<AccessibilityProvider>
<ProfileModal {...defaultProps} {...props} />
</AccessibilityProvider>
</BrowserRouter>
);
};
Expand All @@ -80,6 +83,20 @@ const clickMenuItem = async (user: ReturnType<typeof userEvent.setup>, label: st
}
};

// Helper to navigate to Delete Account confirmation (now inline in Account view)
const navigateToDeleteConfirm = async (user: ReturnType<typeof userEvent.setup>) => {
// 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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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');
Expand All @@ -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();
Expand All @@ -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('/');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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();
});
});
});
Loading
Loading