react-typescript-frontend
React TypeScript Frontend
Overview
Build and maintain the Chuuk Dictionary React frontend using TypeScript, Mantine UI v8, and Vite. Focuses on type-safe development, component patterns, and proper Chuukese text handling.
Tech Stack
- React 19: Latest React with hooks and concurrent features
- TypeScript 5.9: Strict type checking
- Mantine v8: UI component library with dark mode
- Vite 7: Fast build tool and dev server
- React Router v7: Client-side routing
- Axios: HTTP client for API calls
Project Structure
frontend/
├── src/
│ ├── App.tsx # Main app component
│ ├── main.tsx # Entry point with providers
│ ├── theme.ts # Mantine theme configuration
│ ├── components/ # Reusable components
│ │ ├── Footer.tsx
│ │ └── GrammarLearning.tsx
│ ├── contexts/ # React contexts
│ ├── hooks/ # Custom hooks
│ ├── pages/ # Page components
│ ├── data/ # Static data
│ └── assets/ # Images, fonts
├── public/ # Static assets
├── index.html # HTML template
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript config
└── package.json
Configuration
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5002',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
});
Component Patterns
1. Page Component
// src/pages/DictionaryPage.tsx
import { useState, useEffect } from 'react';
import { Container, Title, TextInput, Stack, Loader, Alert } from '@mantine/core';
import { IconSearch, IconAlertCircle } from '@tabler/icons-react';
import { useDictionary } from '@/hooks/useDictionary';
import { DictionaryEntry } from '@/components/DictionaryEntry';
interface DictionaryPageProps {
initialQuery?: string;
}
export function DictionaryPage({ initialQuery = '' }: DictionaryPageProps) {
const [query, setQuery] = useState(initialQuery);
const { entries, loading, error, search } = useDictionary();
useEffect(() => {
if (query.length >= 2) {
search(query);
}
}, [query, search]);
return (
<Container size="lg" py="xl">
<Title order={1} mb="lg">Chuukese Dictionary</Title>
<TextInput
placeholder="Search Chuukese or English..."
value={query}
onChange={(e) => setQuery(e.target.value)}
leftSection={<IconSearch size={16} />}
size="lg"
mb="xl"
styles={{
input: {
fontFamily: "'Noto Sans', sans-serif",
}
}}
/>
{error && (
<Alert icon={<IconAlertCircle />} color="red" mb="md">
{error}
</Alert>
)}
{loading ? (
<Loader />
) : (
<Stack gap="md">
{entries.map((entry) => (
<DictionaryEntry key={entry._id} entry={entry} />
))}
</Stack>
)}
</Container>
);
}
2. Reusable Component
// src/components/DictionaryEntry.tsx
import { Card, Text, Badge, Group, Stack } from '@mantine/core';
interface DictionaryEntryData {
_id: string;
chuukese_word: string;
english_definition: string;
part_of_speech?: string;
grammar_type?: string;
pronunciation?: string;
}
interface DictionaryEntryProps {
entry: DictionaryEntryData;
onClick?: (entry: DictionaryEntryData) => void;
}
export function DictionaryEntry({ entry, onClick }: DictionaryEntryProps) {
return (
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
onClick={() => onClick?.(entry)}
style={{ cursor: onClick ? 'pointer' : 'default' }}
>
<Group justify="space-between" mb="xs">
<Text
fw={600}
size="xl"
style={{ fontFeatureSettings: "'kern' 1" }}
>
{entry.chuukese_word}
</Text>
{entry.part_of_speech && (
<Badge color="blue" variant="light">
{entry.part_of_speech}
</Badge>
)}
</Group>
<Text c="dimmed" size="md">
{entry.english_definition}
</Text>
{entry.pronunciation && (
<Text size="sm" c="dimmed" mt="xs" fs="italic">
/{entry.pronunciation}/
</Text>
)}
</Card>
);
}
3. Custom Hook
// src/hooks/useDictionary.ts
import { useState, useCallback } from 'react';
import axios from 'axios';
interface DictionaryEntry {
_id: string;
chuukese_word: string;
english_definition: string;
part_of_speech?: string;
grammar_type?: string;
}
interface UseDictionaryResult {
entries: DictionaryEntry[];
loading: boolean;
error: string | null;
search: (query: string) => Promise<void>;
getEntry: (word: string) => Promise<DictionaryEntry | null>;
}
export function useDictionary(): UseDictionaryResult {
const [entries, setEntries] = useState<DictionaryEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const search = useCallback(async (query: string) => {
if (!query.trim()) {
setEntries([]);
return;
}
setLoading(true);
setError(null);
try {
const response = await axios.get<{ entries: DictionaryEntry[] }>(
`/api/dictionary/search`,
{ params: { query } }
);
setEntries(response.data.entries);
} catch (err) {
setError(err instanceof Error ? err.message : 'Search failed');
setEntries([]);
} finally {
setLoading(false);
}
}, []);
const getEntry = useCallback(async (word: string): Promise<DictionaryEntry | null> => {
try {
const response = await axios.get<DictionaryEntry>(
`/api/dictionary/entry/${encodeURIComponent(word)}`
);
return response.data;
} catch {
return null;
}
}, []);
return { entries, loading, error, search, getEntry };
}
4. Context Provider
// src/contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import axios from 'axios';
interface User {
email: string;
name: string;
role: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string) => Promise<void>;
logout: () => Promise<void>;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await axios.get<User>('/api/auth/me');
setUser(response.data);
} catch {
setUser(null);
} finally {
setLoading(false);
}
};
const login = async (email: string) => {
await axios.post('/api/auth/magic-link', { email });
};
const logout = async () => {
await axios.post('/api/auth/logout');
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
loading,
login,
logout,
isAuthenticated: !!user,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
API Integration
Axios Setup
// src/api/client.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
// Request interceptor
apiClient.interceptors.request.use((config) => {
// Add any auth headers if needed
return config;
});
// Response interceptor
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
Type-Safe API Calls
// src/api/translation.ts
import apiClient from './client';
interface TranslationRequest {
text: string;
direction: 'chk_to_en' | 'en_to_chk';
}
interface TranslationResponse {
original: string;
translated: string;
confidence: number;
model_used: string;
}
export async function translate(request: TranslationRequest): Promise<TranslationResponse> {
const response = await apiClient.post<TranslationResponse>('/translate', request);
return response.data;
}
export async function translateBatch(
texts: string[],
direction: 'chk_to_en' | 'en_to_chk'
): Promise<TranslationResponse[]> {
const response = await apiClient.post<TranslationResponse[]>('/translate/batch', {
texts,
direction,
});
return response.data;
}
Chuukese Text Handling
Font Configuration
/* src/index.css */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap');
:root {
font-family: 'Noto Sans', 'Arial Unicode MS', sans-serif;
}
/* Chuukese text styling */
.chuukese-text {
font-family: 'Noto Sans', 'Arial Unicode MS', sans-serif;
font-feature-settings: 'kern' 1, 'liga' 1;
line-height: 1.6;
}
Text Component with Accent Support
// src/components/ChuukeseText.tsx
import { Text, TextProps } from '@mantine/core';
interface ChuukeseTextProps extends TextProps {
children: React.ReactNode;
}
export function ChuukeseText({ children, ...props }: ChuukeseTextProps) {
return (
<Text
{...props}
style={{
fontFamily: "'Noto Sans', 'Arial Unicode MS', sans-serif",
fontFeatureSettings: "'kern' 1, 'liga' 1",
...props.style,
}}
>
{children}
</Text>
);
}
Routing
Router Setup
// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Layout } from './components/Layout';
import { DictionaryPage } from './pages/DictionaryPage';
import { TranslationPage } from './pages/TranslationPage';
import { GrammarPage } from './pages/GrammarPage';
import { LoginPage } from './pages/LoginPage';
import { useAuth } from './contexts/AuthContext';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, loading } = useAuth();
if (loading) return <LoadingScreen />;
if (!isAuthenticated) return <Navigate to="/login" />;
return <>{children}</>;
}
export function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<Layout />}>
<Route index element={<DictionaryPage />} />
<Route path="translate" element={<TranslationPage />} />
<Route path="grammar" element={<GrammarPage />} />
<Route
path="admin/*"
element={
<ProtectedRoute>
<AdminRoutes />
</ProtectedRoute>
}
/>
</Route>
</Routes>
</BrowserRouter>
);
}
Development Commands
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Lint code
npm run lint
# Type check
npx tsc --noEmit
Best Practices
TypeScript
- Enable strict mode: Catch more errors at compile time
- Define interfaces: Type all API responses and component props
- Avoid
any: Use proper types orunknown - Use type guards: Narrow types safely
React
- Use functional components: Hooks for all state management
- Memoize expensive operations:
useMemo,useCallback - Split components: Keep components focused and reusable
- Handle loading and error states: Always show feedback
Mantine
- Use theme tokens: Don't hardcode colors or spacing
- Leverage built-in components: Avoid custom implementations
- Use compound components: Group related components logically
- Dark mode first: Test in both color schemes
Dependencies
{
"dependencies": {
"@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10",
"@mantine/notifications": "^8.3.10",
"@mantine/modals": "^8.3.10",
"@tabler/icons-react": "^3.35.0",
"axios": "^1.13.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.10.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.1.1",
"typescript": "~5.9.3",
"vite": "^7.2.4"
}
}
More from findinfinitelabs/chuuk
bible-epub-processing
Parse and extract structured content from Bible EPUBs (NWT) for parallel text alignment between Chuukese and English. Use when working with Bible data, verse extraction, parallel corpus building, or generating training data from Scripture.
14security-environment-standards
Security and environment configuration standards for web applications, including environment variable management, secure coding practices, and production deployment security. Use when setting up environments, configuring security, or deploying applications.
13document-ocr-processing
Process scanned documents and images containing Chuukese text using OCR with specialized post-processing for accent characters and traditional formatting. Use when working with scanned books, documents, or images that contain Chuukese text that needs to be digitized.
12multi-language-document-processing
Process documents in multiple languages with focus on low-resource languages. Covered by the chuukese-language-processing skill — see that skill for combined guidance.
8azure-container-deployment
Deploy containerized applications to Azure Container Apps with Cosmos DB, Key Vault, ACR integration, and multi-container orchestration. Use when deploying the Chuuk Dictionary app, managing Azure infrastructure, or setting up production environments.
8chuukese-language-processing
Specialized processing for Chuukese language text including tokenization, accent handling, cultural context preservation, and language-specific patterns. Use when working with Chuukese text, translation tasks, or when building language models for this Micronesian language.
7