We stand in solidarity with our brave soldiers defending Cambodia's sovereignty and territorial integrity. Our hearts are with those protecting our homeland during these challenging times. We call upon the international community to support peaceful resolution and respect for Cambodia's borders.
🙏 កម្ពុជាត្រូវការសន្តិភាព • Together we stand for peace and sovereignty
A modern, feature-rich PDF viewer application for Cambodian national exam papers (ឯកសារប្រឡងបាក់ឌុប). Built with Nuxt 3 and Vue 3, this application provides an intuitive interface for students to access, view, and study exam papers from 2014 to 2022. Available as both a web application and Android app.
🔗 GitHub Repository: https://github.com/HELMAB/bacii 🌐 Live Demo: https://dobpi.netlify.app 📱 Android App: Download APK
- Interactive PDF viewer with smooth rendering
- Zoom in/out controls (50% - 200%)
- Fullscreen mode
- Print and download functionality
- Page progress indicator
- Skeleton loading animations
- Internal link navigation support
- Scroll-to-top button
- Side-by-side PDF comparison
- Independent zoom controls for each PDF
- Swap PDFs functionality
- Modal selector for choosing comparison documents
- Synchronized viewing experience
- Organized by category, year, and subject
- Quick navigation between PDFs with arrow keys
- Recently viewed history (last 5 documents)
- Favorites system with localStorage persistence
- Search functionality across all subjects
- Breadcrumb navigation
- Current document position indicator
- Full offline support with Service Worker
- Install prompt for desktop and mobile
- Multiple caching strategies:
- Cache-first for static assets
- Network-first for runtime assets
- Stale-while-revalidate for document data
- Dedicated PDF cache for offline access
- Offline indicator showing connection status
- App shortcuts and share target API support
- PWA manifest with app icons (72x72 to 512x512)
- Data Caching: 24-hour localStorage cache for docs.json with background refresh
- Service Worker Caching: 4 cache types with version-based invalidation
- CDN Optimization: Netlify caching headers for optimal delivery
- PDFs cached for 1 year (immutable)
- Fonts cached for 1 year (immutable)
- Document data with stale-while-revalidate strategy
- Cache fallback to stale data when network fails
- Responsive design (mobile, tablet, desktop)
- Dark mode support with persistent preference
- Collapsible sidebar with compact mode
- Keyboard shortcuts for power users (14 shortcuts)
- Toast notifications for user feedback (3-second duration)
- Modal confirmation dialogs for destructive actions
- Smooth transitions and animations
- Custom scrollbar styling
- Empty state when no PDF selected
- Khmer language support (km-KH)
- Custom PengHuoth font for authentic Khmer typography
- Keyboard navigation support
- ARIA-friendly modals
- Focus-visible styles
- Responsive touch controls for mobile devices
- Framework: Nuxt 4.2.1
- Frontend: Vue 3.5.24 with Composition API
- Router: Vue Router 4.6.3
- Styling: TailwindCSS 3.4.18
- PDF Rendering: vue-pdf-embed 2.1.3
- Language: TypeScript
- PWA: Custom Service Worker implementation
- Deployment: Netlify with advanced caching headers
- Build Tools: PostCSS 8.5.6, Autoprefixer 10.4.22
The application uses Vue 3 Composition API with 13 custom composables for state management:
useComparison- PDF comparison mode stateuseDocsCache- Document data caching with 24-hour expiryuseFavorites- Favorites management with localStorageuseKeyboardShortcuts- Global keyboard navigationuseNavigation- PDF navigation and historyusePdfViewer- PDF viewer state (zoom, page, fullscreen)useRecentlyViewed- Recently viewed tracking (max 5 items)useResponsive- Responsive breakpoints detectionuseSearch- Search functionality across documentsuseSidebar- Sidebar state and compact modeuseTheme- Dark mode toggle with persistenceuseToast- Toast notification system
bacii/
├── app/
│ ├── app.vue # Main application component (443 lines)
│ ├── components/
│ │ ├── ConfirmModal.vue # Confirmation modal component
│ │ ├── InstallPrompt.vue # PWA install prompt
│ │ ├── KeyboardShortcutsModal.vue # Keyboard shortcuts help
│ │ ├── OfflineIndicator.vue # Connection status indicator
│ │ ├── ScrollToTopButton.vue # Scroll to top functionality
│ │ ├── ToastNotification.vue # Toast notification system
│ │ ├── PdfViewer/ # PDF viewer components
│ │ │ ├── ComparisonModal.vue # PDF comparison selector
│ │ │ ├── ComparisonView.vue # Side-by-side comparison
│ │ │ └── PdfControls.vue # Zoom, print, download controls
│ │ └── Sidebar/ # Sidebar components
│ │ ├── FavoritesSection.vue # Favorites list
│ │ ├── RecentSection.vue # Recently viewed
│ │ ├── SearchBar.vue # Search functionality
│ │ └── TreeView.vue # Document tree navigation
│ ├── composables/ # Vue composables (13 files)
│ │ ├── useComparison.js
│ │ ├── useDocsCache.js
│ │ ├── useFavorites.js
│ │ ├── useKeyboardShortcuts.js
│ │ ├── useNavigation.js
│ │ ├── usePdfViewer.js
│ │ ├── useRecentlyViewed.js
│ │ ├── useResponsive.js
│ │ ├── useSearch.js
│ │ ├── useSidebar.js
│ │ ├── useTheme.js
│ │ └── useToast.js
│ ├── constants/
│ │ └── icons.js # Subject icons and category gradients
│ ├── utils/
│ │ └── helpers.js # Utility functions
│ └── assets/
│ └── css/
│ └── main.css # Global styles and animations
├── public/
│ ├── pdfs/ # PDF files organized by category/year
│ │ ├── society/ # Social sciences exam papers (2014-2022)
│ │ └── science/ # Natural sciences exam papers (2014-2022)
│ ├── docs.json # Exam papers metadata (788 lines)
│ ├── manifest.json # PWA manifest with app icons
│ ├── sw.js # Service Worker (228 lines, 4 cache types)
│ ├── fonts/PengHuoth/ # Khmer font files (Regular & Bold)
│ ├── sitemap.xml # SEO sitemap
│ └── robots.txt # Search engine directives
├── scripts/
│ └── generate-pwa-icons.js # PWA icon generator script
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.js # TailwindCSS configuration
├── tsconfig.json # TypeScript configuration
├── netlify.toml # Netlify deployment config with caching
└── package.json # Dependencies and scripts
Make sure to install dependencies:
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun installStart the development server on http://localhost:3000:
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run devBuild the application for production:
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run buildLocally preview production build:
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run previewThe application supports 14 keyboard shortcuts for power users:
| Key | Action |
|---|---|
/ |
Focus search input |
← |
Previous PDF |
→ |
Next PDF |
+ |
Zoom in |
- |
Zoom out |
F |
Toggle fullscreen |
D |
Toggle dark mode |
C |
Toggle comparison mode |
S |
Toggle sidebar |
? |
Show keyboard shortcuts modal |
ESC |
Close sidebar/modal/search |
Home |
Go to first page |
End |
Go to last page |
P |
Print current PDF |
The application implements a comprehensive multi-layer caching system:
- Version:
dobpi-v2 - 4 Cache Types:
STATIC_CACHE: Cache-first for static assets (JS, CSS, images)PDF_CACHE: Dedicated cache for PDF filesDATA_CACHE: Stale-while-revalidate for docs.jsonRUNTIME_CACHE: Network-first for runtime assets
- Version-based invalidation: Old caches automatically cleared on updates
- Offline fallback: Serves cached content when network unavailable
docs.jsoncached for 24 hours with timestamp versioning- Background refresh for seamless updates
- Fallback to stale cache when network fails
- Cache keys:
bacii_docs_data- Document structurebacii_docs_version- Cache timestampfavorites- User favorites listrecentlyViewed- Recently viewed documentstheme- Dark mode preferencecompactMode- Sidebar compact mode state
- PDFs: 1 year cache (immutable)
- Fonts: 1 year cache (immutable, font-display: swap)
- JS/CSS: 1 year cache (immutable)
- docs.json: 1 hour max-age, 2 hours s-maxage, 1 day stale-while-revalidate
- Security headers: X-Frame-Options, X-Content-Type-Options
- Lazy loading for PDF components
- Skeleton loading animations
- Optimized font loading with font-display: swap
- Responsive image handling
- Efficient Vue 3 reactivity system
- Minimal JavaScript bundle
Years: 2014(1), 2014(2), 2015-2022 (9 time periods)
Subjects:
- អក្សរសាស្ត្រខ្មែរ (Khmer Literature) 📚
- គណិតវិទ្យា (Mathematics) 🧮
- រូបវិទ្យា (Physics) ⚛️
- ជីវវិទ្យា (Biology) 🧬
- ប្រវត្តិសាស្ត្រ (History) 📜
- ភូមិវិទ្យា (Geography) 🌍
- ផែនដីវិទ្យា (Geology) ⛰️
- សីលធម៌-ពលរដ្ឋ (Morality-Citizenship) ❤️
- ភាសាអង់គ្លេស (English) 🇬🇧
- ភាសាបារាំង (French) 🇫🇷
Years: 2014(1), 2014(2), 2015-2022 (9 time periods)
Subjects:
- អក្សរសាស្ត្រខ្មែរ (Khmer Literature) 📚
- គណិតវិទ្យា (Mathematics) 🧮
- រូបវិទ្យា (Physics) ⚛️
- គីមីវិទ្យា (Chemistry) 🧪
- ជីវវិទ្យា (Biology) 🧬
- ប្រវត្តិសាស្ត្រ (History) 📜
- ភាសាអង់គ្លេស (English) 🇬🇧
- ភាសាបារាំង (French) 🇫🇷
Total: 150+ exam papers across both categories
The public/docs.json file (788 lines) contains the complete document tree structure:
[
{
"label": "Category Name (e.g., វិទ្យាសាស្ត្រសង្គម)",
"children": [
{
"label": "Year (e.g., ២០២២)",
"children": [
{
"label": "Subject Name (e.g., អក្សរសាស្ត្រខ្មែរ)",
"pdf": "/pdfs/society/2022/khmer-literature.pdf",
"icon": "Subject icon emoji"
}
]
}
]
}
]Step 1: Place PDF files in the appropriate directory
public/pdfs/{category}/{year}/{subject}.pdf
Step 2: Update public/docs.json with the new entry
{
"label": "Subject Name in Khmer",
"pdf": "/pdfs/category/year/subject.pdf",
"icon": "Choose from subjectIcons"
}Step 3: If adding a new subject, update app/constants/icons.js
export const subjectIcons = {
'អក្សរសាស្ត្រខ្មែរ': '📚',
'គណិតវិទ្យា': '🧮',
'Your New Subject': '🎯', // Choose appropriate emoji
// ...
}Step 4: (Optional) Update category gradients if adding new category
export const categoryGradients = {
'វិទ្យាសាស្ត្រសង្គម': 'from-blue-500 to-indigo-600',
'Your New Category': 'from-purple-500 to-pink-600',
// ...
}Theme colors and gradients are defined in app/constants/icons.js:
export const categoryGradients = {
'វិទ្យាសាស្ត្រសង្គម': 'from-blue-500 to-indigo-600',
'វិទ្យាសាស្ត្រធម្មជាតិ': 'from-green-500 to-teal-600',
'គណិតវិទ្យា': 'from-purple-500 to-pink-600',
'ប្រវត្តិសាស្ត្រ': 'from-amber-500 to-orange-600'
}Subject icons (emojis) are also defined in the same file:
export const subjectIcons = {
'អក្សរសាស្ត្រខ្មែរ': '📚',
'គណិតវិទ្យា': '🧮',
// ...
}The application includes comprehensive SEO optimization configured in nuxt.config.ts:
- Title: Bacii - ឯកសារប្រឡងបាក់ឌុប
- Description: Cambodian national exam papers viewer
- Language: Khmer (km-KH)
- Theme Color: #3B82F6 (blue)
- Viewport: Responsive, mobile-optimized
- Open Graph tags for Facebook/LinkedIn sharing
- Twitter Card support (summary_large_image)
- Custom OG image (
/og-image.svg) - Canonical URL: https://dobpi.netlify.app
- Sitemap:
public/sitemap.xml- Complete site structure - Robots:
public/robots.txt- Search engine directives - Manifest:
public/manifest.json- PWA configuration
- Mobile web app capable
- Apple touch icons
- Standalone display mode
- Status bar styling
- Modern Browsers:
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- Mobile Browsers:
- iOS Safari 12+
- Chrome Mobile
- Samsung Internet
- Requirements:
- JavaScript enabled
- Service Worker support (for offline features)
- PDF rendering support
- Version: 1.0.1
- Download: APK file
- Features: Same as web version with native Android integration
- Installable on desktop and mobile
- Works offline with cached content
- App-like experience
- Push notification ready (infrastructure in place)
- Visit https://dobpi.netlify.app
- Click the install icon in the address bar (⊕ or computer icon)
- Click "Install" in the prompt
- App will open in a standalone window
- Visit https://dobpi.netlify.app in Chrome/Firefox
- Tap the menu (⋮) → "Install app" or "Add to Home Screen"
- Confirm installation
- App icon will appear on your home screen
Alternatively, download the native Android APK for full native integration.
- Visit https://dobpi.netlify.app in Safari
- Tap the Share button (square with arrow)
- Scroll and tap "Add to Home Screen"
- Tap "Add" to confirm
The application uses a custom Service Worker (public/sw.js) with the following features:
- Version:
dobpi-v2(automatically invalidates old caches) - Strategies:
- Static assets: Cache-first (instant loading)
- PDFs: Dedicated cache for offline access
- Document data: Stale-while-revalidate (fresh data with instant response)
- Runtime: Network-first with cache fallback
The Service Worker automatically clears old caches when a new version is activated. To manually clear caches:
- Open DevTools → Application → Storage
- Click "Clear site data"
- Refresh the page
During development, the Service Worker is registered but can be disabled via DevTools:
- DevTools → Application → Service Workers
- Check "Update on reload" and "Bypass for network"
The application is deployed at: https://dobpi.netlify.app
The project includes a netlify.toml file with optimized settings:
- Build command:
npm run build - Publish directory:
.output/public - Node version: Automatic
- Static assets (JS/CSS/images): 1 year immutable
- PDFs: 1 year immutable with public caching
- Fonts: 1 year immutable
- docs.json: Advanced stale-while-revalidate strategy
- max-age: 1 hour
- s-maxage: 2 hours
- stale-while-revalidate: 1 day
X-Frame-Options: DENYX-Content-Type-Options: nosniff
For deployment to other platforms:
- Build the application:
npm run build - Deploy the
.output/publicdirectory - Configure similar caching headers for optimal performance
- Ensure Service Worker is served with proper MIME type
Check out the Nuxt deployment documentation for platform-specific guides.
- Check browser console for errors
- Clear Service Worker cache: DevTools → Application → Storage → Clear site data
- Verify PDF path in docs.json matches actual file location
- Check file permissions if running locally
- Open DevTools → Application → Service Workers
- Click "Unregister" on the current service worker
- Hard refresh the page (Ctrl/Cmd + Shift + R)
- The new service worker will be registered
- Check localStorage is enabled in your browser
- Verify
themekey exists in localStorage - Clear localStorage and toggle dark mode again
- Check localStorage quota (usually 5-10MB)
- Verify localStorage is not disabled
- Check browser's privacy settings
The development server supports HMR. Changes to Vue components, composables, and CSS will reflect immediately without full page reload.
Use Vue DevTools to inspect composable state:
- Install Vue DevTools browser extension
- Open DevTools → Vue tab
- Inspect component state and composables
To test Service Worker in development:
- Build the production version:
npm run build - Preview locally:
npm run preview - Open DevTools → Application → Service Workers
To regenerate PWA icons:
node scripts/generate-pwa-icons.jsView all app data in localStorage:
// In browser console
Object.keys(localStorage).filter(key =>
key.startsWith('bacii') ||
['favorites', 'recentlyViewed', 'theme', 'compactMode'].includes(key)
).forEach(key => console.log(key, localStorage.getItem(key)))- DevTools → Performance tab
- Start recording
- Navigate through the app
- Stop recording and analyze
- Look for long tasks and optimize accordingly
No environment variables are currently used. All configuration is in:
nuxt.config.ts- Nuxt configurationtailwind.config.js- Styling configurationnetlify.toml- Deployment configuration
MIT License - see the LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request or open an issue on GitHub.