react-typescript-frontend
SKILL.md
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"
}
}
Weekly Installs
7
Repository
findinfinitelabs/chuukFirst Seen
Mar 1, 2026
Security Audits
Installed on
codex7
opencode6
gemini-cli6
github-copilot6
kimi-cli6
amp6