A modern, interactive web application for learning European Portuguese vocabulary. Built with Next.js 15, React 19, TypeScript, and SQLite, this app provides an engaging learning experience with features like XP tracking, daily statistics, speech synthesis, and AI-powered explanations.
- Interactive Quiz System: Practice vocabulary with bidirectional translation (Portuguese ↔ English)
- Database-Powered: SQLite database with phrase relationships and similarity scoring
- XP System: Earn experience points based on response speed (1-10 XP per correct answer)
- Smart Practice Algorithm: Words you struggle with are added to a practice list for reinforcement
- Speech Synthesis: Hear Portuguese words pronounced with European Portuguese accent
- Server-Side Validation: Answer checking with phrase similarity matching
- Database Statistics: Real-time statistics from the vocabulary database
- Daily Statistics: Track your daily learning progress
- Visual Progress Chart: 14-day histogram showing your consistency
- Achievement System: Monitor improvement with XP tracking
- Phrase Relationships: Learn synonyms and alternative translations
- Detailed Phrase Analysis: Get comprehensive explanations powered by OpenAI GPT-5-nano
- Contextual Understanding: Includes synonyms and alternative translations from the database
- European Portuguese Focus: Specifically tailored for European Portuguese learners
- Rich Context: Includes examples, grammar, pronunciation (IPA), etymology, and cultural context
- Long-Term Caching: Explanations are cached server-side for 90 days to improve performance
- SQLite Database: Fast, reliable local database with phrase relationships
- Secured Import API: Protected vocabulary import with authentication
- Phrase Similarity Scoring: Intelligent phrase relationship mapping
- Server-Side Processing: Answer validation and explanation generation
- Automatic Cache Management: Smart caching for explanations and API responses
- Dark Theme: Easy on the eyes with a modern dark interface
- Keyboard Shortcuts: Navigate efficiently with keyboard controls
- Responsive Design: Works seamlessly on desktop and mobile devices
- JSON-Style Interface: Unique developer-friendly aesthetic
- Error Boundaries: Graceful error handling prevents crashes
- Node.js 18+
- npm or yarn package manager
- OpenAI API key (for explanation features)
-
Clone the repository
git clone <your-repo-url> cd pt
-
Install dependencies
npm install # or yarn install
-
Set up environment variables
Create a
.env.local
file in the root directory:OPENAI_API_KEY=your_openai_api_key_here PRESHARED_KEY=your_secure_preshared_key_here
-
Run the development server
npm run dev # or yarn dev
The application uses SQLite for vocabulary management. To populate the database:
-
Set the import authentication key
# Add to your .env.local file # Note: PRESHARED_KEY is used for both app auth and import operations PRESHARED_KEY=your_secure_import_key_here
-
Run the migration script
# Dry run to see what will be imported node scripts/migrate-vocabulary.js --dry-run # Import vocabulary data (server must be running) node scripts/migrate-vocabulary.js # Or overwrite existing data node scripts/migrate-vocabulary.js --overwrite
Note: The Next.js development server must be running (
npm run dev
) before running the migration script. -
Verify the import
Check the database statistics at: http://localhost:3000/api/vocabulary/stats
-
Open your browser
Navigate to http://localhost:3000
To use the AI explanation features:
- Set your
PRESHARED_KEY
in the environment variables - Access the app with the authentication key in the URL:
http://localhost:3000/#auth_YOUR_PRESHARED_KEY
- The key will be automatically saved and the URL cleaned up
- Frontend: Next.js 15 with App Router
- UI Framework: React 19 with TypeScript
- Styling: Tailwind CSS 4 with custom design system (no Radix/Shadcn)
- State Management: React hooks with custom state management
- API: Next.js API routes
- AI Integration: OpenAI GPT-5-nano with structured output
- Data Validation: Zod for runtime type checking
- Code Quality: ESLint + TypeScript for type safety
- Testing: Jest with React Testing Library for comprehensive unit testing
- Separation of Concerns: Business logic is separated from UI components
- Reusability: Common patterns are abstracted into reusable hooks
- Type Safety: Full TypeScript support with proper typing
- Single Responsibility: Each component has a clear, focused purpose
- Composition: Complex UI is built from simple, composable components
- Props Interface: Consistent prop interfaces with TypeScript validation
- Custom + Tailwind: All UI elements are built with Tailwind and small custom components under
src/components/ui
. - No Radix/Shadcn: Radix UI and shadcn/ui have been removed to reduce bundle size and complexity.
- Examples:
Button
,Card
,Input
are minimal Tailwind components.- Notifications use
sonner
via a lightweight wrapper.
- Removed:
@shadcn/ui
,@radix-ui/*
,class-variance-authority
,cmdk
, and unused icon packs. - Replaced Radix Select with a native
<select>
styled with Tailwind insrc/app/add/page.tsx
.
- Local Storage Integration: Automatic persistence with error handling
- Optimistic Updates: UI updates immediately with background persistence
- Error Recovery: Graceful handling of storage and parsing errors
- Type to Answer: Simply start typing in the active input field
- Keyboard Shortcuts:
N
- Next wordS
- Show answerSpace
- Speak current wordE
- Explain word (requires authentication)
- Start Learning: A Portuguese word is shown, you translate to English (or vice versa)
- Input Answer: Type your translation in the active input field
- Instant Feedback: Correct answers are highlighted immediately
- XP Rewards: Faster responses earn more XP (1-10 points)
- Progress Tracking: Incorrect answers add words to your practice list
- Explanations: Click "Explain" for detailed AI-powered analysis
- Words you struggle with are automatically added to a practice list
- Practice words have a higher chance of appearing in future sessions
- After 3 correct answers, words are removed from the practice list
- The practice probability increases as your practice list grows
# Required: OpenAI API key for explanations
OPENAI_API_KEY=sk-proj-...
# Required: Authentication key for API access
PRESHARED_KEY=your-secure-random-key
# Optional: Override Next.js configuration
NODE_ENV=development
Modify constants in src/types/index.ts
:
export const CONFIG = {
MIN_XP: 1, // Minimum XP per correct answer
MAX_XP: 10, // Maximum XP per correct answer
FAST_RESPONSE_TIME: 2000, // Fast response threshold (ms)
SLOW_RESPONSE_TIME: 30000, // Slow response threshold (ms)
CORRECT_DELAY: 500, // Delay after correct answer (ms)
REVEAL_DELAY: 2000, // Delay after showing answer (ms)
MAX_CORRECT_COUNT: 3, // Attempts needed to master a word
// ... more configuration options
} as const;
The application uses a custom Tailwind configuration with a dark theme. Modify src/app/globals.css
to customize colors and styling.
npm run build
npm start
The application includes server-side caching for both vocabulary data (30 days) and AI explanations (90 days). For production deployments, you should mount the cache directory as a volume to persist cache across container restarts.
# Build the Docker image
docker build -t pt-learn .
# Run the container with cache volume mounting
docker run -d \
--name pt-learn \
-p 3000:3000 \
--env-file .env \
-v $(pwd)/cache:/app/cache \
--restart unless-stopped \
pt-learn
Create a docker-compose.yml
file:
version: '3.8'
services:
pt-learn:
image: ghcr.io/mtib/pt:latest
# Or build from source:
# build: .
ports:
- "3000:3000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- PRESHARED_KEY=${PRESHARED_KEY}
volumes:
- ./cache:/app/cache
- ./logs:/app/logs # Optional: for application logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
Then run:
docker-compose up -d
# Run with persistent cache
docker run -d \
--name pt-learn \
-p 3000:3000 \
-e OPENAI_API_KEY=your-key \
-e PRESHARED_KEY=your-auth-key \
-v /host/path/to/cache:/app/cache \
--restart unless-stopped \
ghcr.io/mtib/pt:latest
When properly mounted, your cache directory will contain:
cache/
├── vocabulary.json # Cached vocabulary data (30-day TTL)
└── explanations/ # AI explanation cache (90-day TTL)
├── [hash1].json
├── [hash2].json
└── ...
- Cache Persistence: Always mount
/app/cache
as a volume in production - Cache Size: The explanation cache can grow over time. Monitor disk usage
- Backup: Consider backing up the cache directory for faster cold starts
- Permissions: Ensure the cache directory is writable by the container user
The application is optimized for deployment on Vercel:
- Connect your GitHub repository to Vercel
- Set environment variables in the Vercel dashboard
- Deploy with zero configuration
- TypeScript: Full type safety throughout the application
- ESLint: Enforces consistent code style and catches common errors
- Error Boundaries: Prevents crashes with graceful error handling
- Comprehensive Error Handling: Network errors, API failures, and edge cases are handled
# Run all tests
npm test
# Run tests in watch mode during development
npm run test:watch
# Generate coverage reports
npm run test:coverage
The application uses Jest with React Testing Library for comprehensive testing:
- Test Coverage: Utility functions, text normalization, XP calculations
- Advanced Text Processing: Tests for 16+ different apostrophe and quote types
- CI Integration: Automated testing in GitHub Actions pipeline
- Coverage Reports: Integrated with Codecov for coverage tracking
- Input validation with Zod schemas
- API error handling with custom error types
- Client-side error boundaries
- Graceful fallbacks for missing data
- Code Splitting: Automatic code splitting with Next.js
- Caching: Smart caching for API responses and explanations
- Optimistic Updates: Immediate UI feedback with background persistence
- Request Deduplication: Prevents duplicate API calls
- Bundle Optimization: Optimized production builds
Serves filtered and cached Portuguese vocabulary data.
GET /api/vocabulary
{
"words": [
{
"rank": 1,
"targetWord": "casa",
"englishWord": "house"
},
// ... more words
]
}
- Server-side filtering: Removes identical Portuguese-English pairs
- 30-day cache: Reduces external API calls
- Automatic validation: Ensures data quality
- CDN optimization: Proper cache headers for edge caching
Generates detailed explanations for Portuguese words.
POST /api/explain
Content-Type: application/json
Authorization: Bearer <your-auth-key>
{
"word": "casa",
"englishReference": "house"
}
{
"word": "casa",
"englishReference": "house",
"example": "A minha casa é muito bonita. (My house is very beautiful.)",
"explanation": "This is a fundamental word in Portuguese...",
"definition": "A building for human habitation...",
"grammar": "Feminine noun (a casa, as casas)...",
"facts": "The word 'casa' comes from Latin 'casa'...",
"pronunciationIPA": "/ˈka.zɐ/",
"pronunciationEnglish": "KAH-za"
}
400
- Bad Request (missing or invalid parameters)403
- Forbidden (invalid or missing authentication)429
- Too Many Requests (rate limit exceeded)500
- Internal Server Error
We welcome contributions! Please follow these guidelines:
- Fork the repository and create a feature branch
- Follow TypeScript best practices and maintain type safety
- Add documentation for new features or API changes
- Test your changes thoroughly before submitting
- Follow the existing code style and conventions
- Clone your fork and install dependencies
- Create a
.env.local
file with required environment variables - Run
npm run dev
to start the development server - Make your changes and test thoroughly
- Submit a pull request with a clear description
This project is licensed under the MIT License. See the LICENSE file for details.
- Vocabulary Data: Thousand Most Common Words by SMenigat
- AI Technology: Powered by OpenAI's GPT-5-nano model
- UI Components: Built with Tailwind CSS and custom components
- Icons & Fonts: Geist Sans and Geist Mono fonts by Vercel
- Check your internet connection
- Ensure the external vocabulary API is accessible
- Try refreshing the page
- Verify your
PRESHARED_KEY
environment variable - Make sure you've added the auth key to the URL:
#auth_YOUR_KEY
- Check that the key matches between client and server
- Verify your
OPENAI_API_KEY
is valid and has sufficient quota - Check OpenAI service status
- Review API usage limits
- Clear your browser's local storage for the site
- Disable browser extensions that might interfere
- Try using an incognito/private browsing window
- Check the console for detailed error messages
- Review the application logs for server-side issues
- Open an issue on GitHub with detailed reproduction steps
Happy Learning! 🇵🇹 Enjoy your Portuguese learning journey with this modern, feature-rich application!