diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..d70c7a9 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +{ + "languages": { + "CSS": { + "language_servers": ["tailwindcss-language-server"], + "tab_size": 2, + "formatter": "auto", + }, + }, +} diff --git a/PROJECT_GUIDELINES.md b/PROJECT_GUIDELINES.md index f25d7e7..8ea0094 100644 --- a/PROJECT_GUIDELINES.md +++ b/PROJECT_GUIDELINES.md @@ -17,7 +17,10 @@ - ❌ **NEVER modify backend/API for frontend features without user confirmation** - ❌ **NEVER use custom CSS** - TailwindCSS classes only - ❌ **NEVER use JavaScript** - convert all to TypeScript -- ❌ **NEVER use object-oriented programming patterns in TypeScript** - avoid classes, inheritance, and OOP bloat; use functional/other paradigms +- ❌ **NEVER use Object-Oriented Programming** (no classes, inheritance, or this-capture) +- ✅ **DO use procedural/imperative style** as your default +- ✅ **DO borrow functional techniques** when they simplify code +- ✅ **DO avoid ideological purity** - the best paradigm is the one that fits the problem - ❌ **NEVER add new Dockerfiles without user confirmation - ❌ **NEVER fetch initial data via AJAX on page load** - use server-side rendering instead - ❌ **NEVER break progressive enhancement** - pages must work without JavaScript @@ -113,7 +116,10 @@ VERIFY → Compile successfully ### Frontend & Styling - ✅ Always use **TailwindCSS classes** for all styling - ✅ Convert all JavaScript to **TypeScript** -- ✅ Avoid OOP patterns - prefer functional/other paradigms +- ✅ **Never use Object-Oriented Programming** (no classes, inheritance, or this-capture) +- ✅ **Use procedural/imperative style** as your default +- ✅ **Borrow functional techniques** when they simplify code +- ✅ **Avoid ideological purity** - the best paradigm is the one that fits the problem - ✅ **Render initial data server-side** in Go templates for fast page loads - ✅ **Use JavaScript/HTMX for CRUD operations** (create, update, delete) - ✅ **Ensure progressive enhancement** - pages work without JavaScript @@ -207,7 +213,7 @@ VERIFY → Compile successfully - **Styling**: TailwindCSS (no custom CSS) - **Language**: TypeScript (no JavaScript) - **Templates**: HTMX with server-side rendering -- **Patterns**: Functional/other (no OOP) +- **Patterns**: Procedural/imperative with functional techniques where helpful (no OOP) ### Containerization - **Runtime**: Podman (not Docker) diff --git a/SCREENSHOT_AUTOMATION.md b/SCREENSHOT_AUTOMATION.md new file mode 100644 index 0000000..b223003 --- /dev/null +++ b/SCREENSHOT_AUTOMATION.md @@ -0,0 +1,773 @@ +# Screenshot Automation Plan for Bookhoard Documentation + +> **Status**: Ready to implement when frontend is complete +> **Last Updated**: 2026-02-03 + +This document outlines the complete plan for automatically generating screenshots for Bookhoard documentation using Playwright. + +## Overview + +Playwright will be used to: +1. Navigate the running Bookhoard server +2. Perform key user/admin workflows +3. Capture screenshots at each step +4. Save to `docs/images/` organized by documentation section +5. Generate/update markdown files with proper image references + +## Prerequisites (To Verify When Ready) + +### Frontend Pages Complete +Verify these pages are fully functional before starting: + +- [ ] `/` - Login page +- [ ] `/register` - Registration page +- [ ] `/dashboard` - Main dashboard with library browsing +- [ ] `/collections` - Collections management +- [ ] `/devices` - Device management and registration +- [ ] `/conflicts` - Sync conflict resolution +- [ ] `/progress` - Reading progress tracking +- [ ] `/analytics` - Usage analytics +- [ ] `/queue` - Sync queue monitoring +- [ ] `/admin` - Admin dashboard +- [ ] `/admin/profile` - Profile settings +- [ ] `/admin/library` - Library management +- [ ] `/api-explorer` - API testing interface + +### Test Environment Ready +- [ ] Bookhoard server running on `http://localhost:8765` +- [ ] Test database seeded with sample data (books, collections, devices) +- [ ] Test admin account ready (username, password, role=admin) +- [ ] Test regular user account ready (username, password, role=user) + +## Installation & Setup + +```bash +# Install Playwright (run in project root) +npm init -y +npm install -D @playwright/test +npx playwright install chromium +``` + +Project structure after setup: +``` +bookhoard/ +├── docs/ +│ ├── images/ # Screenshots will be stored here +│ │ ├── user/ +│ │ ├── admin/ +│ │ ├── devices/ +│ │ └── sync/ +│ └── *.md # Existing markdown files +├── screenshots/ # Playwright test files +│ ├── auth.spec.ts +│ ├── user-workflows.spec.ts +│ ├── admin-workflows.spec.ts +│ ├── device-workflows.spec.ts +│ ├── sync-workflows.spec.ts +│ └── config.ts +├── .env.screenshots # Environment configuration +└── ... +``` + +## Environment Setup + +Create `.env.screenshots` in the project root: + +```env +# Bookhoard Server +BASE_URL=http://localhost:8765 + +# Admin Account (for admin guide screenshots) +ADMIN_EMAIL=admin@example.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=SecureAdminPass123! + +# Regular User Account (for user guide screenshots) +USER_EMAIL=user@example.com +USER_USERNAME=user +USER_PASSWORD=SecureUserPass456! +``` + +## Questions to Ask When Ready + +Before running the screenshot automation, ask the user: + +### 1. Server Access +**Q**: Where is the Bookhoard server running? +- [ ] `localhost:8765` (default) +- [ ] Custom port: `________` +- [ ] Remote URL: `________` + +### 2. Test Credentials +**Q**: What credentials should Playwright use? + +**Admin Account** (for admin guide screenshots): +- Username: `________` +- Password: `________` + +**Regular User Account** (for user guide screenshots): +- Username: `________` +- Password: `________` + +### 3. Screenshot Format +**Q**: What format for screenshots? +- [ ] **WebP** (recommended - modern, good compression) +- [ ] PNG (highest quality, larger files) +- [ ] JPEG (smaller files, compression artifacts) + +### 4. Screenshot Dimensions +**Q**: What viewport sizes for screenshots? +- [ ] **Desktop**: 1920x1080 (full-width screenshots) +- [ ] **Tablet**: 768x1024 (responsive documentation) +- [ ] **Mobile**: 375x667 (mobile documentation) +- [ ] All three sizes (comprehensive coverage) + +### 5. Theme +**Q**: What theme should screenshots use? +- [ ] **Default** (Tokyo Night theme as seen in templates) +- [ ] Light theme (if implemented) +- [ ] Multiple themes (document theme switching) + +### 6. Language +**Q**: What language/region for the UI? +- [ ] **English** (default) +- [ ] Other: `________` + +## Screenshot Plan by Documentation Section + +### 1. User Guide Screenshots +**File**: `docs/user/user-guide.md` + +**Screenshots Needed**: + +| Screenshot Name | Description | Page/Action | +|-----------------|-------------|-------------| +| `login-page.webp` | Login form with filled credentials | `/` - Login page | +| `dashboard-overview.webp` | Main dashboard showing libraries | `/dashboard` | +| `library-grid.webp` | Media items grid view | `/dashboard` → Click library | +| `book-detail.webp` | Book detail view with metadata | `/dashboard` → Click book | +| `search-results.webp` | Search in action | `/dashboard` → Type in search | +| `filter-panel.webp` | Filter options expanded | `/dashboard` → Open filters | +| `collections-list.webp` | Collections grid view | `/collections` | +| `create-collection.webp` | New collection modal | `/collections` → Click "New Collection" | +| `reading-progress.webp` | Progress tracking view | `/progress` | +| `analytics-view.webp` | User analytics dashboard | `/analytics` | + +**Estimated Screenshots**: ~10 + +### 2. Admin Guide Screenshots +**File**: `docs/user/admin-guide.md` + +**Screenshots Needed**: + +| Screenshot Name | Description | Page/Action | +|-----------------|-------------|-------------| +| `admin-dashboard.webp` | Admin overview panel | `/admin` | +| `user-management.webp` | User list with actions | `/admin` → Users section | +| `add-user.webp` | Add new user form | `/admin` → Click "Add User" | +| `library-settings.webp` | Library configuration | `/admin/library` | +| `add-library.webp` | Create new library form | `/admin/library` → Click "Add Library" | +| `analytics-admin.webp` | Admin analytics view | `/analytics` (admin view) | +| `sync-queue.webp` | Sync queue monitoring | `/queue` | +| `theme-settings.webp` | Theme selection interface | `/admin/profile` → Theme section | +| `user-profile-edit.webp` | Edit user profile | `/admin/profile` | + +**Estimated Screenshots**: ~9 + +### 3. Device Setup Screenshots +**File**: `docs/user/devices/kobo-setup.md` and `koreader-setup.md` + +**Screenshots Needed**: + +| Screenshot Name | Description | Page/Action | +|-----------------|-------------|-------------| +| `device-list.webp` | Device management page | `/devices` | +| `add-device-modal.webp` | Add new device modal | `/devices` → Click "Add New Device" | +| `device-form-kobo.webp` | Kobo device registration form | `/devices` → Select Kobo type | +| `device-form-koreader.webp` | KOReader device registration form | `/devices` → Select KOReader type | +| `device-qr-code.webp` | QR code for device approval | After device registration | +| `device-approved.webp` | Device approved confirmation | After approving device | +| `device-sync-settings.webp` | Sync configuration for device | `/devices` → Click device settings | +| `shelf-mapping.webp` | Collection to shelf mapping | `/devices` → Click shelf mapping | +| `sync-queue-item.webp` | Device sync in queue | `/queue` (device specific) | + +**Estimated Screenshots**: ~9 + +### 4. Sync Guide Screenshots +**File**: `docs/user/sync-guide.md` + +**Screenshots Needed**: + +| Screenshot Name | Description | Page/Action | +|-----------------|-------------|-------------| +| `sync-conflicts.webp` | Conflicts list view | `/conflicts` | +| `conflict-resolution.webp` | Resolve conflict dialog | `/conflicts` → Click resolve | +| `unlinked-books.webp` | Unlinked books list | `/unlinked-books` | +| `book-linking.webp` | Link book to metadata | `/unlinked-books` → Click link | +| `sync-success.webp` | Successful sync indicator | Any page after sync | + +**Estimated Screenshots**: ~5 + +### 5. Settings Guide Screenshots +**File**: `docs/user/settings-guide.md` + +**Screenshots Needed**: + +| Screenshot Name | Description | Page/Action | +|-----------------|-------------|-------------| +| `profile-overview.webp` | Profile settings page | `/admin/profile` | +| `update-username.webp` | Username change form | `/admin/profile` | +| `update-email.webp` | Email change form | `/admin/profile` | +| `change-password.webp` | Password change form | `/admin/profile` | +| `theme-selector.webp` | Theme selection dropdown | `/admin/profile` (if implemented) | + +**Estimated Screenshots**: ~5 + +## Playwright Test Files + +### `screenshots/config.ts` - Playwright Configuration + +```typescript +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './', + fullyParallel: false, + retries: 1, + reporter: 'list', + use: { + baseURL: process.env.BASE_URL || 'http://localhost:8765', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium-desktop', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1920, height: 1080 } + }, + }, + ], +}); +``` + +### `screenshots/auth.spec.ts` - Authentication Screenshots + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Authentication Screenshots', () => { + test('Login page', async ({ page }) => { + await page.goto('/'); + + await page.screenshot({ + path: 'docs/images/user/login-page.webp', + fullPage: true + }); + }); + + test('User login flow', async ({ page }) => { + await page.goto('/'); + + await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user'); + await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password'); + + await page.screenshot({ + path: 'docs/images/user/login-form-filled.webp', + fullPage: true + }); + + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + + await page.screenshot({ + path: 'docs/images/user/dashboard-after-login.webp', + fullPage: true + }); + }); + + test('Admin login flow', async ({ page }) => { + await page.goto('/'); + + await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin'); + await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password'); + + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + + await page.goto('/admin'); + + await page.screenshot({ + path: 'docs/images/admin/admin-dashboard.webp', + fullPage: true + }); + }); + + test('Registration page', async ({ page }) => { + await page.goto('/register'); + + await page.screenshot({ + path: 'docs/images/user/registration-page.webp', + fullPage: true + }); + + await page.fill('input[name="email"]', 'newuser@example.com'); + await page.fill('input[name="username"]', 'newuser'); + await page.fill('input[name="password"]', 'SecurePass123!'); + + await page.screenshot({ + path: 'docs/images/user/registration-form-filled.webp', + fullPage: true + }); + }); +}); +``` + +### `screenshots/user-workflows.spec.ts` - User Guide Screenshots + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('User Guide Screenshots', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user'); + await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password'); + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + }); + + test('Dashboard overview', async ({ page }) => { + await page.goto('/dashboard'); + + await page.screenshot({ + path: 'docs/images/user/dashboard-overview.webp', + fullPage: true + }); + }); + + test('Library grid view', async ({ page }) => { + await page.goto('/dashboard'); + + await page.waitForSelector('#libraries-container', { timeout: 5000 }); + + const firstLibrary = page.locator('[data-library]').first(); + if (await firstLibrary.isVisible()) { + await firstLibrary.click(); + await page.waitForURL(/\/dashboard/, { timeout: 5000 }); + + await page.screenshot({ + path: 'docs/images/user/library-grid.webp', + fullPage: true + }); + } + }); + + test('Search functionality', async ({ page }) => { + await page.goto('/dashboard'); + + await page.waitForSelector('#search-input', { timeout: 5000 }); + + await page.fill('#search-input', 'science'); + await page.waitForTimeout(1000); + + await page.screenshot({ + path: 'docs/images/user/search-results.webp', + fullPage: true + }); + }); + + test('Filter panel', async ({ page }) => { + await page.goto('/dashboard'); + + const filterPanel = page.locator('.filter-panel details'); + if (await filterPanel.isVisible()) { + await filterPanel.click(); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'docs/images/user/filter-panel-open.webp', + fullPage: true + }); + } + }); + + test('Collections list', async ({ page }) => { + await page.goto('/collections'); + + await page.screenshot({ + path: 'docs/images/user/collections-list.webp', + fullPage: true + }); + }); + + test('Create collection modal', async ({ page }) => { + await page.goto('/collections'); + + await page.click('button:has-text("New Collection"), button:has-text("Create Your First Collection")'); + + await page.waitForSelector('#create-modal', { state: 'visible', timeout: 5000 }); + + await page.fill('#collection-name', 'My Reading List'); + await page.fill('#collection-description', 'Books I want to read'); + + await page.screenshot({ + path: 'docs/images/user/create-collection-modal.webp', + fullPage: true + }); + }); + + test('Reading progress view', async ({ page }) => { + await page.goto('/progress'); + + await page.screenshot({ + path: 'docs/images/user/reading-progress.webp', + fullPage: true + }); + }); + + test('Analytics view', async ({ page }) => { + await page.goto('/analytics'); + + await page.screenshot({ + path: 'docs/images/user/analytics-view.webp', + fullPage: true + }); + }); +}); +``` + +### `screenshots/admin-workflows.spec.ts` - Admin Guide Screenshots + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Admin Guide Screenshots', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.fill('input[name="login"]', process.env.ADMIN_USERNAME || 'admin'); + await page.fill('input[name="password"]', process.env.ADMIN_PASSWORD || 'password'); + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + }); + + test('Admin dashboard', async ({ page }) => { + await page.goto('/admin'); + + await page.screenshot({ + path: 'docs/images/admin/admin-dashboard.webp', + fullPage: true + }); + }); + + test('User management', async ({ page }) => { + await page.goto('/admin'); + + const userSection = page.locator('[data-section="users"], text="Users"'); + if (await userSection.isVisible()) { + await page.screenshot({ + path: 'docs/images/admin/user-management.webp', + fullPage: true + }); + } + }); + + test('Library management', async ({ page }) => { + await page.goto('/admin/library'); + + await page.screenshot({ + path: 'docs/images/admin/library-management.webp', + fullPage: true + }); + }); + + test('Add library form', async ({ page }) => { + await page.goto('/admin/library'); + + const addLibraryBtn = page.locator('button:has-text("Add Library"), button:has-text("Create Library")'); + if (await addLibraryBtn.isVisible()) { + await addLibraryBtn.click(); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'docs/images/admin/add-library-form.webp', + fullPage: true + }); + } + }); + + test('Profile settings', async ({ page }) => { + await page.goto('/admin/profile'); + + await page.screenshot({ + path: 'docs/images/admin/profile-settings.webp', + fullPage: true + }); + }); + + test('Theme selector', async ({ page }) => { + await page.goto('/admin/profile'); + + const themeSelect = page.locator('select[name="theme"], [data-theme-selector]'); + if (await themeSelect.isVisible()) { + await themeSelect.click(); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'docs/images/admin/theme-selector.webp', + fullPage: true + }); + } + }); +}); +``` + +### `screenshots/device-workflows.spec.ts` - Device Setup Screenshots + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Device Setup Screenshots', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user'); + await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password'); + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + }); + + test('Device list page', async ({ page }) => { + await page.goto('/devices'); + + await page.screenshot({ + path: 'docs/images/devices/device-list.webp', + fullPage: true + }); + }); + + test('Add device modal', async ({ page }) => { + await page.goto('/devices'); + + await page.click('button:has-text("Add New Device"), button:has-text("Add Your First Device")'); + + await page.waitForSelector('[data-modal="add-device"], #add-device-modal', { state: 'visible', timeout: 5000 }); + + await page.screenshot({ + path: 'docs/images/devices/add-device-modal.webp', + fullPage: true + }); + }); + + test('Kobo device form', async ({ page }) => { + await page.goto('/devices'); + + await page.click('button:has-text("Add New Device")'); + + await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 }); + + const deviceTypeSelect = page.locator('select[name="device_type"]'); + if (await deviceTypeSelect.isVisible()) { + await deviceTypeSelect.selectOption('kobo'); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'docs/images/devices/device-form-kobo.webp', + fullPage: true + }); + } + }); + + test('KOReader device form', async ({ page }) => { + await page.goto('/devices'); + + await page.click('button:has-text("Add New Device")'); + + await page.waitForSelector('[data-modal="add-device"]', { state: 'visible', timeout: 5000 }); + + const deviceTypeSelect = page.locator('select[name="device_type"]'); + if (await deviceTypeSelect.isVisible()) { + await deviceTypeSelect.selectOption('koreader'); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'docs/images/devices/device-form-koreader.webp', + fullPage: true + }); + } + }); + + test('Sync queue', async ({ page }) => { + await page.goto('/queue'); + + await page.screenshot({ + path: 'docs/images/devices/sync-queue.webp', + fullPage: true + }); + }); +}); +``` + +### `screenshots/sync-workflows.spec.ts` - Sync Guide Screenshots + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Sync Guide Screenshots', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.fill('input[name="login"]', process.env.USER_USERNAME || 'user'); + await page.fill('input[name="password"]', process.env.USER_PASSWORD || 'password'); + await page.click('button[type="submit"]'); + await page.waitForURL('/dashboard', { timeout: 5000 }); + }); + + test('Sync conflicts page', async ({ page }) => { + await page.goto('/conflicts'); + + await page.screenshot({ + path: 'docs/images/sync/sync-conflicts.webp', + fullPage: true + }); + }); + + test('Unlinked books page', async ({ page }) => { + await page.goto('/unlinked-books'); + + await page.screenshot({ + path: 'docs/images/sync/unlinked-books.webp', + fullPage: true + }); + }); +}); +``` + +## Running the Tests + +### Run all tests +```bash +npx playwright test +``` + +### Run specific test file +```bash +npx playwright test auth.spec.ts +``` + +### Run in headed mode (see browser) +```bash +npx playwright test --headed +``` + +### Run with debug mode +```bash +npx playwright test --debug +``` + +## Markdown Update Strategy + +### Option 1: Create New Markdown +Generate fresh markdown files with embedded screenshots. + +### Option 2: Update Existing Markdown +Update existing markdown files by: +1. Finding section headers +2. Inserting screenshot references after relevant steps +3. Using alt text to describe what's shown + +**Example Markdown Update**: + +```markdown +### Step 1: Log In to Bookhoard + +1. Open your browser and navigate to `http://localhost:8765` +2. Enter your username and password +3. Click the **Login** button + + +``` + +## Workflow When Ready + +### Step 1: Verify Frontend Complete +- Check all pages listed in "Prerequisites" are working +- Confirm no TODO placeholders in templates + +### Step 2: Seed Test Data +```bash +# Add sample books, collections, devices +# Create test admin and user accounts +``` + +### Step 3: Set Up Playwright +```bash +npm install -D @playwright/test +npx playwright install chromium +``` + +### Step 4: Create Environment File +Create `.env.screenshots` with the test credentials + +### Step 5: Run Screenshot Scripts +```bash +# Run all screenshot workflows +npx playwright test + +# Or run specific suites +npx playwright test auth.spec.ts +npx playwright test user-workflows.spec.ts +``` + +### Step 6: Review and Adjust +- Manually review screenshots +- Retake any that need adjustment +- Update markdown files if needed + +## Customizing for Your Implementation + +When you're ready to run these, you may need to update: + +1. **Selectors**: Update CSS selectors to match your actual HTML structure +2. **Routes**: Ensure all routes match your router configuration +3. **Wait conditions**: Adjust timeouts based on your app's performance +4. **Modal IDs**: Update modal selectors to match your implementation + +## Tips for Better Screenshots + +1. **Wait for animations**: Add `page.waitForTimeout(500)` after clicking to let animations complete +2. **Full page vs viewport**: Use `fullPage: true` for complete page screenshots +3. **Hide scrollbars**: Use `page.evaluate(() => document.body.style.overflow = 'hidden')` for cleaner screenshots +4. **Dark theme**: Most screenshots will use the default Tokyo Night theme +5. **Clean test data**: Use consistent test data for reproducible screenshots + +## Estimated Time Investment + +| Task | Time | +|------|------| +| Install & configure Playwright | 15 min | +| Seed test database | 30 min | +| Write Playwright scripts | 2-3 hours | +| Run screenshot automation | 10 min | +| Review & retake screenshots | 30-60 min | +| Update markdown files | 30-60 min | +| **Total** | **4-6 hours** | + +## Future Enhancements + +- **Visual Regression Testing**: Use Playwright to detect UI changes +- **Multi-theme Screenshots**: Automatically capture all theme variations +- **Multi-language Screenshots**: Capture screenshots in different locales +- **Auto-update on Deploy**: Run screenshots as part of CI/CD on documentation updates + +## References + +- [Playwright Documentation](https://playwright.dev/) +- [Playwright Screenshots API](https://playwright.dev/docs/screenshots) +- [Bookhoard Frontend Templates](templates/) +- [Bookhoard API Documentation](docs/developer/api/) + +--- + +**Created by**: GLM (OpenCode) +**Version**: 1.0 +**Ready for Implementation**: When frontend development is complete diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 7229638..b2db201 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -4,7 +4,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide ## 🚀 Getting Started -- **[Development Guide](DEVELOPMENT.md)** - Development workflow and architecture +- **[Development Guide](Development.md)** - Development workflow and architecture - Architecture overview - Directory structure - Local development setup @@ -22,7 +22,7 @@ Welcome to the Bookhoard contributing documentation. This section contains guide ## 🤝 How to Contribute -We welcome contributions! Please see our [Development Guide](DEVELOPMENT.md) for information on: +We welcome contributions! Please see our [Development Guide](Development.md) for information on: - Setting up your development environment - Understanding the codebase - Making pull requests diff --git a/docs/contributing/DEVELOPMENT.md b/docs/contributing/development.md similarity index 100% rename from docs/contributing/DEVELOPMENT.md rename to docs/contributing/development.md diff --git a/docs/index.md b/docs/index.md index 7d10383..5842c12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,7 +53,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly. **[Contributing Portal](contributing/contributing.md)** - Development workflow -- [Development Guide](contributing/DEVELOPMENT.md) - Architecture, setup, testing + - [Development Guide](contributing/Development.md) - Architecture, setup, testing - [PROJECT_GUIDELINES.md](PROJECT_GUIDELINES.md) - Development rules and standards --- @@ -66,7 +66,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly. | **Set up a device** | [User Portal → Device Setup](user/user-guide.md) | | **Use the API** | [Developer Portal → API Docs](developer/development.md) | | **Deploy Bookhoard** | [Operations Portal → Troubleshooting](operations/troubleshooting.md) | -| **Contribute code** | [Contributing Portal → Development Guide](contributing/DEVELOPMENT.md) | + | **Contribute code** | [Contributing Portal → Development Guide](contributing/Development.md) | | **Understand sync** | [User Portal → Sync Guide](user/sync-guide.md) | --- @@ -84,7 +84,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly. | ...resolve conflicts? | [Sync Guide](user/sync-guide.md) - Managing Conflicts | | ...troubleshoot deployment? | [Troubleshooting Guide](operations/troubleshooting.md) | | ...use the API? | [API Reference](developer/api-reference.md) | -| ...contribute code? | [Development Guide](contributing/DEVELOPMENT.md) | + | ...contribute code? | [Development Guide](contributing/Development.md) | ### "Where is..." @@ -116,7 +116,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly. 1. Follow [README.md](../README.md) quick start 2. Configure environment: [.env.example](../.env.example) 3. Review [Troubleshooting Guide](operations/troubleshooting.md) -4. Check [Development Guide](contributing/DEVELOPMENT.md) for performance tuning +4. Check [Development Guide](contributing/Development.md) for performance tuning --- @@ -126,7 +126,7 @@ When adding new features: 1. **User-facing features** → Update relevant User docs 2. **API endpoints** → Update [API Reference](developer/api-reference.md) & split docs -3. **Backend changes** → Update [Development Guide](contributing/DEVELOPMENT.md) +3. **Backend changes** → Update [Development Guide](contributing/Development.md) 4. **Deployment changes** → Update [Operations Portal](operations/operations.md) Keep [PROJECT_GUIDELINES.md](PROJECT_GUIDELINES.md) in mind for documentation standards. diff --git a/internal/docs/http_handler.go b/internal/docs/http_handler.go index 272f048..d670dfd 100644 --- a/internal/docs/http_handler.go +++ b/internal/docs/http_handler.go @@ -6,7 +6,9 @@ import ( "net/http" "strings" + "bookhoard/internal/database" "bookhoard/templates" + "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -58,18 +60,38 @@ func (h *HTTPHandler) ShowDocumentation(c echo.Context) error { // Get navigation nav := h.docs.BuildNavigation() - // Create empty user (docs are public, no auth required) - user := templates.User{ - ID: "", - Username: "", - Email: "", - Role: "", - Theme: "tokyo-night", + // Check if user is authenticated via middleware + var user templates.User + if userID := c.Get("user"); userID != nil { + dbUser := userID.(database.Users) + var theme string + if dbUser.Theme.Valid { + theme = dbUser.Theme.String + } else { + theme = "tokyo-night" + } + user = templates.User{ + ID: uuid.UUID(dbUser.ID.Bytes).String(), + Username: dbUser.Username, + Email: dbUser.Email, + Role: dbUser.Role, + Theme: theme, + } + } else { + // Create empty user for unauthenticated users + user = templates.User{ + ID: "", + Username: "", + Email: "", + Role: "", + Theme: "tokyo-night", + } } // Render documentation page var buf bytes.Buffer - err = templates.DocsLayout(*nav, *doc, user).Render(c.Request().Context(), &buf) + currentPath := c.Request().URL.Path + err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf) if err != nil { fmt.Printf("DOCS RENDER ERROR: %v\n", err) return c.HTML(http.StatusInternalServerError, "Failed to render page: "+err.Error()) @@ -88,6 +110,7 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error // Get endpoint data for the explorer endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath) + _ = endpointInfo if err != nil { // Endpoint not found in explorer data, fall back to old behavior return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn) @@ -118,24 +141,38 @@ func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error // Get navigation nav := h.docs.BuildNavigation() - // Create empty user (docs are public) - user := templates.User{ - ID: "", - Username: "", - Email: "", - Role: "", - Theme: "tokyo-night", + // Check if user is authenticated via middleware + var user templates.User + if userID := c.Get("user"); userID != nil { + dbUser := userID.(database.Users) + var theme string + if dbUser.Theme.Valid { + theme = dbUser.Theme.String + } else { + theme = "tokyo-night" + } + user = templates.User{ + ID: uuid.UUID(dbUser.ID.Bytes).String(), + Username: dbUser.Username, + Email: dbUser.Email, + Role: dbUser.Role, + Theme: theme, + } + } else { + // Create empty user for unauthenticated users + user = templates.User{ + ID: "", + Username: "", + Email: "", + Role: "", + Theme: "tokyo-night", + } } - // Create API explorer data - explorerData := templates.APIExplorerData{ - Endpoint: *endpointInfo, - IsLoggedIn: isLoggedIn, - } - - // Render API endpoint page with explorer + // Render API endpoint page var buf bytes.Buffer - err = templates.DocsLayoutWithExplorer(*nav, *doc, user, explorerData).Render(c.Request().Context(), &buf) + currentPath := c.Request().URL.Path + err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf) if err != nil { return c.HTML(http.StatusInternalServerError, "Failed to render page") @@ -179,13 +216,33 @@ func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string, // Get navigation nav := h.docs.BuildNavigation() - // Create empty user - user := templates.User{ - ID: "", - Username: "", - Email: "", - Role: "", - Theme: "tokyo-night", + // Check if user is authenticated via middleware + var user templates.User + currentPath := c.Request().URL.Path + if userID := c.Get("user"); userID != nil { + dbUser := userID.(database.Users) + var theme string + if dbUser.Theme.Valid { + theme = dbUser.Theme.String + } else { + theme = "tokyo-night" + } + user = templates.User{ + ID: uuid.UUID(dbUser.ID.Bytes).String(), + Username: dbUser.Username, + Email: dbUser.Email, + Role: dbUser.Role, + Theme: theme, + } + } else { + // Create empty user for unauthenticated users + user = templates.User{ + ID: "", + Username: "", + Email: "", + Role: "", + Theme: "tokyo-night", + } } // Render API endpoint page @@ -202,7 +259,7 @@ func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string, }, Category: "API Reference", SourceFile: "api", - }, user).Render(c.Request().Context(), &buf) + }, user, currentPath).Render(c.Request().Context(), &buf) if err != nil { return c.HTML(http.StatusInternalServerError, "Failed to render page") @@ -221,12 +278,33 @@ func (h *HTTPHandler) APIHome(c echo.Context) error { endpoints := h.docs.GetAPIEndpoints() nav := h.docs.BuildNavigation() - user := templates.User{ - ID: "", - Username: "", - Email: "", - Role: "", - Theme: "tokyo-night", + // Check if user is authenticated via middleware + var user templates.User + currentPath := c.Request().URL.Path + if userID := c.Get("user"); userID != nil { + dbUser := userID.(database.Users) + var theme string + if dbUser.Theme.Valid { + theme = dbUser.Theme.String + } else { + theme = "tokyo-night" + } + user = templates.User{ + ID: uuid.UUID(dbUser.ID.Bytes).String(), + Username: dbUser.Username, + Email: dbUser.Email, + Role: dbUser.Role, + Theme: theme, + } + } else { + // Create empty user for unauthenticated users + user = templates.User{ + ID: "", + Username: "", + Email: "", + Role: "", + Theme: "tokyo-night", + } } // Build API documentation content @@ -255,7 +333,7 @@ func (h *HTTPHandler) APIHome(c echo.Context) error { } var buf bytes.Buffer - err := templates.DocsLayout(*nav, *apiDoc, user).Render(c.Request().Context(), &buf) + err := templates.DocsLayout(*nav, *apiDoc, user, currentPath).Render(c.Request().Context(), &buf) if err != nil { return c.HTML(http.StatusInternalServerError, "Failed to render page") diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 401dae9..4e5d992 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -357,14 +357,18 @@ func (h *AuthHandler) Login(c echo.Context) error { } if c.Request().Header.Get("HX-Request") == "true" { + redirect := c.FormValue("redirect") + if redirect == "" { + redirect = "/bookshelf" + } html := fmt.Sprintf(`