15 changed files with 960 additions and 193 deletions
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
name: kiddos |
||||
region: nyc |
||||
|
||||
# Backend Service |
||||
services: |
||||
- name: backend |
||||
source_dir: backend |
||||
github: |
||||
repo: YOUR_GITHUB_USERNAME/kiddos |
||||
branch: main |
||||
deploy_on_push: true |
||||
|
||||
build_command: npm install && npm run build |
||||
run_command: npm run migrate && npm start |
||||
|
||||
environment_slug: node-js |
||||
instance_size_slug: basic-xxs |
||||
instance_count: 1 |
||||
|
||||
http_port: 3000 |
||||
|
||||
envs: |
||||
- key: NODE_ENV |
||||
value: production |
||||
- key: PORT |
||||
value: "3000" |
||||
- key: TURSO_URL |
||||
value: ${TURSO_URL} |
||||
type: SECRET |
||||
- key: TURSO_AUTH_TOKEN |
||||
value: ${TURSO_AUTH_TOKEN} |
||||
type: SECRET |
||||
- key: YOUTUBE_API_KEY |
||||
value: ${YOUTUBE_API_KEY} |
||||
type: SECRET |
||||
- key: JWT_SECRET |
||||
value: ${JWT_SECRET} |
||||
type: SECRET |
||||
- key: JWT_REFRESH_SECRET |
||||
value: ${JWT_REFRESH_SECRET} |
||||
type: SECRET |
||||
- key: INITIAL_ADMIN_USERNAME |
||||
value: ${INITIAL_ADMIN_USERNAME} |
||||
- key: INITIAL_ADMIN_PASSWORD |
||||
value: ${INITIAL_ADMIN_PASSWORD} |
||||
type: SECRET |
||||
- key: CORS_ORIGIN |
||||
value: ${APP_URL} |
||||
|
||||
health_check: |
||||
http_path: /api/health |
||||
|
||||
# Frontend Static Site |
||||
static_sites: |
||||
- name: frontend |
||||
source_dir: frontend |
||||
github: |
||||
repo: YOUR_GITHUB_USERNAME/kiddos |
||||
branch: main |
||||
deploy_on_push: true |
||||
|
||||
build_command: npm install && npm run build |
||||
output_dir: dist |
||||
|
||||
envs: |
||||
- key: VITE_API_URL |
||||
value: ${backend.PUBLIC_URL} |
||||
|
||||
routes: |
||||
- path: / |
||||
|
||||
@ -0,0 +1,242 @@
@@ -0,0 +1,242 @@
|
||||
# Deployment Guide - DigitalOcean App Platform |
||||
|
||||
This guide will help you deploy the Kiddos app to DigitalOcean's App Platform. |
||||
|
||||
## Prerequisites |
||||
|
||||
1. **GitHub Repository**: Push your code to GitHub |
||||
2. **DigitalOcean Account**: Create an account at https://digitalocean.com |
||||
3. **Turso Database**: Have your Turso credentials ready |
||||
- Get from: https://turso.tech |
||||
4. **YouTube API Key**: Get from Google Cloud Console |
||||
- Visit: https://console.cloud.google.com |
||||
- Enable YouTube Data API v3 |
||||
- Create API key |
||||
|
||||
## Step 1: Prepare Your Repository |
||||
|
||||
1. **Update `.do/app.yaml`**: |
||||
```bash |
||||
# Edit .do/app.yaml |
||||
# Replace YOUR_GITHUB_USERNAME/kiddos with your actual GitHub repo |
||||
# Example: johndoe/kiddos |
||||
``` |
||||
|
||||
2. **Commit and push to GitHub**: |
||||
```bash |
||||
git add . |
||||
git commit -m "Add DigitalOcean deployment config" |
||||
git push origin main |
||||
``` |
||||
|
||||
## Step 2: Create App on DigitalOcean |
||||
|
||||
1. Go to https://cloud.digitalocean.com/apps |
||||
2. Click **"Create App"** |
||||
3. Select **"GitHub"** as source |
||||
4. Authorize DigitalOcean to access your repository |
||||
5. Select your `kiddos` repository |
||||
6. Choose your branch (usually `main`) |
||||
7. DigitalOcean will detect `.do/app.yaml` automatically |
||||
8. Click **"Next"** |
||||
|
||||
## Step 3: Configure Environment Variables |
||||
|
||||
DigitalOcean will prompt you to fill in the required environment variables (marked as `${VARIABLE}` in app.yaml). |
||||
|
||||
### Required Secrets: |
||||
|
||||
1. **TURSO_URL** |
||||
- Your Turso database URL |
||||
- Example: `libsql://your-database.turso.io` |
||||
|
||||
2. **TURSO_AUTH_TOKEN** |
||||
- Your Turso authentication token |
||||
- Get from Turso dashboard |
||||
|
||||
3. **YOUTUBE_API_KEY** |
||||
- Your YouTube Data API v3 key |
||||
- Get from Google Cloud Console |
||||
|
||||
4. **JWT_SECRET** |
||||
- A long random string (32+ characters) |
||||
- Generate with: `openssl rand -base64 32` |
||||
|
||||
5. **JWT_REFRESH_SECRET** |
||||
- A different long random string (32+ characters) |
||||
- Generate with: `openssl rand -base64 32` |
||||
|
||||
6. **INITIAL_ADMIN_USERNAME** |
||||
- Your admin username (e.g., "admin") |
||||
|
||||
7. **INITIAL_ADMIN_PASSWORD** |
||||
- A secure password for admin account |
||||
- Make it strong! |
||||
|
||||
### Optional Variables: |
||||
|
||||
These will be auto-populated by DigitalOcean: |
||||
- `APP_URL` - Automatically set to your frontend URL |
||||
- `backend.PUBLIC_URL` - Automatically set to your backend URL |
||||
|
||||
## Step 4: Review and Deploy |
||||
|
||||
1. Review the configuration: |
||||
- **Backend**: Node.js service on Basic XXS ($5/month) |
||||
- **Frontend**: Static site (FREE) |
||||
|
||||
2. Choose your datacenter region (default: NYC) |
||||
|
||||
3. Click **"Create Resources"** |
||||
|
||||
4. Wait for deployment (usually 5-10 minutes) |
||||
|
||||
## Step 5: Post-Deployment |
||||
|
||||
### Get Your URLs |
||||
|
||||
After deployment completes, you'll have: |
||||
- **Frontend URL**: `https://kiddos-xxxxx.ondigitalocean.app` |
||||
- **Backend URL**: `https://kiddos-backend-xxxxx.ondigitalocean.app` |
||||
|
||||
### Update CORS Settings (if needed) |
||||
|
||||
If you have multiple domains or custom domain: |
||||
|
||||
1. Go to your app in DigitalOcean |
||||
2. Click on **"backend"** component |
||||
3. Go to **"Environment Variables"** |
||||
4. Update `CORS_ORIGIN` to include your custom domain |
||||
5. Redeploy |
||||
|
||||
## Step 6: Test Your Deployment |
||||
|
||||
1. Visit your frontend URL |
||||
2. You should see the video grid |
||||
3. Click **"Login"** |
||||
4. Use your admin credentials |
||||
5. Go to **"Admin"** page |
||||
6. Try adding a YouTube channel |
||||
|
||||
## Troubleshooting |
||||
|
||||
### Backend Fails to Start |
||||
|
||||
**Check logs:** |
||||
1. Go to your app in DigitalOcean |
||||
2. Click on **"backend"** component |
||||
3. Go to **"Runtime Logs"** |
||||
|
||||
**Common issues:** |
||||
- Missing environment variables |
||||
- Invalid Turso credentials |
||||
- Invalid YouTube API key |
||||
|
||||
### Frontend Shows API Errors |
||||
|
||||
**Check:** |
||||
1. Frontend environment variables |
||||
2. Backend is running (check backend URL) |
||||
3. CORS is configured correctly |
||||
4. Network tab in browser DevTools |
||||
|
||||
### Database Migration Fails |
||||
|
||||
**Solution:** |
||||
1. Check Turso database is accessible |
||||
2. Verify `TURSO_URL` and `TURSO_AUTH_TOKEN` |
||||
3. Check backend logs for specific error |
||||
|
||||
## Monitoring |
||||
|
||||
### View Logs |
||||
|
||||
**Backend logs:** |
||||
``` |
||||
App Platform → Your App → backend → Runtime Logs |
||||
``` |
||||
|
||||
**Build logs:** |
||||
``` |
||||
App Platform → Your App → backend → Build Logs |
||||
``` |
||||
|
||||
### View Metrics |
||||
|
||||
``` |
||||
App Platform → Your App → Insights |
||||
``` |
||||
|
||||
Shows: |
||||
- CPU usage |
||||
- Memory usage |
||||
- Request count |
||||
- Response times |
||||
|
||||
## Updating Your App |
||||
|
||||
### Automatic Deployments |
||||
|
||||
When you push to your GitHub repository, DigitalOcean will automatically: |
||||
1. Detect the change |
||||
2. Build your app |
||||
3. Run tests (if configured) |
||||
4. Deploy new version |
||||
|
||||
### Manual Deployments |
||||
|
||||
1. Go to your app in DigitalOcean |
||||
2. Click **"Create Deployment"** |
||||
3. Select branch |
||||
4. Click **"Deploy"** |
||||
|
||||
## Custom Domain (Optional) |
||||
|
||||
1. Go to your app → **Settings** → **Domains** |
||||
2. Click **"Add Domain"** |
||||
3. Enter your domain name |
||||
4. Follow DNS configuration instructions |
||||
5. Wait for SSL certificate provisioning |
||||
|
||||
## Cost Breakdown |
||||
|
||||
- **Backend (Basic XXS)**: $5/month |
||||
- **Frontend (Static Site)**: FREE |
||||
- **Turso Database**: FREE tier (up to 9 GB) |
||||
- **Bandwidth**: 100 GB/month included |
||||
|
||||
**Total: ~$5/month** |
||||
|
||||
## Scaling |
||||
|
||||
To upgrade: |
||||
1. Go to your app |
||||
2. Click on **"backend"** component |
||||
3. Go to **"Resources"** |
||||
4. Choose a larger instance size |
||||
5. Click **"Save"** |
||||
|
||||
Available sizes: |
||||
- Basic XXS: $5/month (512 MB RAM) |
||||
- Basic XS: $12/month (1 GB RAM) |
||||
- Basic S: $25/month (2 GB RAM) |
||||
|
||||
## Support |
||||
|
||||
- **DigitalOcean Docs**: https://docs.digitalocean.com/products/app-platform/ |
||||
- **Turso Docs**: https://docs.turso.tech |
||||
- **YouTube API Docs**: https://developers.google.com/youtube/v3 |
||||
|
||||
## Security Checklist |
||||
|
||||
- ✅ All secrets stored as encrypted environment variables |
||||
- ✅ CORS configured to only allow your domain |
||||
- ✅ JWT tokens use httpOnly cookies |
||||
- ✅ Rate limiting enabled on API |
||||
- ✅ Admin password is strong |
||||
- ✅ HTTPS enabled automatically by DigitalOcean |
||||
|
||||
--- |
||||
|
||||
**Ready to deploy?** Follow the steps above and you'll be live in minutes! 🚀 |
||||
|
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
# Quick Start - Deploy to DigitalOcean |
||||
|
||||
**Time to deploy: ~10 minutes** |
||||
|
||||
## Before You Start |
||||
|
||||
Gather these items: |
||||
- [ ] GitHub repository URL |
||||
- [ ] Turso database URL and auth token ([Get from Turso](https://turso.tech)) |
||||
- [ ] YouTube API key ([Get from Google](https://console.cloud.google.com)) |
||||
- [ ] Admin username and password (you choose these) |
||||
|
||||
## Deploy in 4 Steps |
||||
|
||||
### 1. Update Configuration (2 minutes) |
||||
|
||||
Edit `.do/app.yaml` and replace `YOUR_GITHUB_USERNAME/kiddos` with your actual GitHub repo: |
||||
|
||||
```yaml |
||||
github: |
||||
repo: johndoe/kiddos # <-- Change this |
||||
branch: main |
||||
``` |
||||
|
||||
### 2. Push to GitHub (1 minute) |
||||
|
||||
```bash |
||||
git add . |
||||
git commit -m "Add DigitalOcean deployment config" |
||||
git push origin main |
||||
``` |
||||
|
||||
### 3. Create App on DigitalOcean (5 minutes) |
||||
|
||||
1. Go to https://cloud.digitalocean.com/apps |
||||
2. Click **"Create App"** |
||||
3. Select **GitHub** and authorize |
||||
4. Select your `kiddos` repository |
||||
5. Choose `main` branch |
||||
6. DigitalOcean detects `.do/app.yaml` ✨ |
||||
7. Click **"Next"** |
||||
|
||||
### 4. Add Environment Variables (2 minutes) |
||||
|
||||
Fill in these required values: |
||||
|
||||
``` |
||||
TURSO_URL = libsql://your-database.turso.io |
||||
TURSO_AUTH_TOKEN = your-token-here |
||||
YOUTUBE_API_KEY = your-youtube-api-key |
||||
JWT_SECRET = (generate with: openssl rand -base64 32) |
||||
JWT_REFRESH_SECRET = (generate with: openssl rand -base64 32) |
||||
INITIAL_ADMIN_USERNAME = admin |
||||
INITIAL_ADMIN_PASSWORD = your-secure-password |
||||
``` |
||||
|
||||
**Click "Create Resources"** and wait for deployment! |
||||
|
||||
--- |
||||
|
||||
## After Deployment |
||||
|
||||
1. Visit your app URL (shown in DigitalOcean) |
||||
2. Login with your admin credentials |
||||
3. Add YouTube channels in the admin panel |
||||
4. Done! 🎉 |
||||
|
||||
--- |
||||
|
||||
## Need Help? |
||||
|
||||
See [DEPLOYMENT.md](./DEPLOYMENT.md) for detailed instructions and troubleshooting. |
||||
|
||||
## Cost |
||||
|
||||
- **$5/month** for backend |
||||
- **FREE** for frontend |
||||
- **FREE** for database (Turso free tier) |
||||
|
||||
**Total: $5/month** |
||||
|
||||
@ -0,0 +1,228 @@
@@ -0,0 +1,228 @@
|
||||
# Move Search & Filters to Navbar - Refactoring Plan |
||||
|
||||
## Goal |
||||
Move the search bar and filter controls from the SearchFilter component into the Navbar for a cleaner, more YouTube-like interface. |
||||
|
||||
## Current Structure |
||||
|
||||
### Files Involved: |
||||
1. `frontend/src/components/Navbar/Navbar.tsx` - Navigation bar (Home, Admin, Login/Logout) |
||||
2. `frontend/src/components/SearchFilter/SearchFilter.tsx` - Search and filters |
||||
3. `frontend/src/pages/HomePage.tsx` - Manages state and passes to SearchFilter |
||||
|
||||
### Current Flow: |
||||
``` |
||||
HomePage (manages state) |
||||
├── Navbar (navigation only) |
||||
└── SearchFilter (search, sort, channel filter) |
||||
└── VideoGrid |
||||
``` |
||||
|
||||
## Proposed Structure |
||||
|
||||
### New Flow: |
||||
``` |
||||
App |
||||
└── Navbar (navigation + search + filters) |
||||
HomePage |
||||
└── VideoGrid (just videos and pagination) |
||||
``` |
||||
|
||||
## Changes Required |
||||
|
||||
### 1. Update Navbar Component |
||||
**File:** `frontend/src/components/Navbar/Navbar.tsx` |
||||
|
||||
**Add props:** |
||||
```typescript |
||||
interface NavbarProps { |
||||
// Search and filter props |
||||
onSearch?: (query: string) => void; |
||||
onSortChange?: (sort: 'newest' | 'oldest' | 'popular') => void; |
||||
onChannelChange?: (channelId: string | undefined) => void; |
||||
channels?: Array<{ id: string; name: string }>; |
||||
selectedChannel?: string; |
||||
currentSearch?: string; |
||||
|
||||
// Only show search/filters on home page |
||||
showSearch?: boolean; |
||||
} |
||||
``` |
||||
|
||||
**Add to navbar:** |
||||
- Search input (centered or right side) |
||||
- Sort dropdown |
||||
- Channel filter dropdown |
||||
- Clear filters button (only if filters active) |
||||
|
||||
**Layout considerations:** |
||||
- Mobile: Collapse to hamburger menu or second row |
||||
- Desktop: Horizontal layout after logo/nav links |
||||
|
||||
### 2. Update HomePage |
||||
**File:** `frontend/src/pages/HomePage.tsx` |
||||
|
||||
**Changes:** |
||||
- Remove `<SearchFilter />` component |
||||
- Pass search/filter props to `<Navbar />` instead |
||||
- Keep state management in HomePage |
||||
- Navbar becomes "controlled component" receiving callbacks |
||||
|
||||
**New structure:** |
||||
```typescript |
||||
export function HomePage() { |
||||
const [page, setPage] = useState(1); |
||||
const [search, setSearch] = useState(''); |
||||
const [sort, setSort] = useState<'newest' | 'oldest' | 'popular'>('newest'); |
||||
const [selectedChannel, setSelectedChannel] = useState<string | undefined>(); |
||||
|
||||
// ... existing hooks ... |
||||
|
||||
return ( |
||||
<> |
||||
{/* No SearchFilter here anymore */} |
||||
<VideoGrid ... /> |
||||
{selectedVideo && <VideoPlayer ... />} |
||||
</> |
||||
); |
||||
} |
||||
``` |
||||
|
||||
### 3. Update App.tsx to Pass Props |
||||
**File:** `frontend/src/App.tsx` |
||||
|
||||
**Option A - Pass through routes:** |
||||
```typescript |
||||
<Routes> |
||||
<Route |
||||
path="/" |
||||
element={<HomePage onNavbarPropsChange={setNavbarProps} />} |
||||
/> |
||||
</Routes> |
||||
``` |
||||
|
||||
**Option B - Conditional rendering in Navbar:** |
||||
```typescript |
||||
// Navbar checks current route |
||||
const location = useLocation(); |
||||
const isHomePage = location.pathname === '/'; |
||||
|
||||
{isHomePage && ( |
||||
<div className="navbar-search"> |
||||
{/* Search and filters */} |
||||
</div> |
||||
)} |
||||
``` |
||||
|
||||
**Recommended:** Option B is simpler |
||||
|
||||
### 4. Handle State Communication |
||||
|
||||
**Challenge:** HomePage has the state, but Navbar needs to trigger changes. |
||||
|
||||
**Solution:** Use React Router's `useLocation` and `useNavigate` with URL query params: |
||||
|
||||
```typescript |
||||
// In HomePage |
||||
const [searchParams, setSearchParams] = useSearchParams(); |
||||
const page = searchParams.get('page') || '1'; |
||||
const search = searchParams.get('search') || ''; |
||||
const sort = searchParams.get('sort') || 'newest'; |
||||
|
||||
// In Navbar |
||||
const handleSearch = (query: string) => { |
||||
const newParams = new URLSearchParams(window.location.search); |
||||
newParams.set('search', query); |
||||
newParams.set('page', '1'); |
||||
navigate({ search: newParams.toString() }); |
||||
}; |
||||
``` |
||||
|
||||
**Benefits:** |
||||
- Shareable URLs with filters |
||||
- Browser back/forward works |
||||
- No prop drilling needed |
||||
|
||||
### 5. Update Navbar CSS |
||||
**File:** `frontend/src/components/Navbar/Navbar.css` |
||||
|
||||
**Add styles for:** |
||||
- Search input container |
||||
- Filter dropdowns |
||||
- Clear button |
||||
- Responsive breakpoints |
||||
- YouTube-inspired styling |
||||
|
||||
**Layout:** |
||||
``` |
||||
Desktop: |
||||
[📺 Kiddos] [Home] [Admin] [🔍 Search...] [Sort ▼] [Channel ▼] [Clear] [Login] |
||||
|
||||
Mobile: |
||||
[📺 Kiddos] [☰] |
||||
[🔍 Search...] [Sort ▼] [Channel ▼] [Clear] |
||||
``` |
||||
|
||||
### 6. Remove SearchFilter Component (Optional) |
||||
**Files to delete/deprecate:** |
||||
- `frontend/src/components/SearchFilter/SearchFilter.tsx` |
||||
- `frontend/src/components/SearchFilter/SearchFilter.css` |
||||
|
||||
Or keep it for reuse elsewhere. |
||||
|
||||
## Implementation Steps |
||||
|
||||
1. ✅ Create this plan |
||||
2. Add URL query param management to HomePage (using `useSearchParams`) |
||||
3. Update Navbar to accept search/filter props |
||||
4. Add search/filter UI to Navbar component |
||||
5. Style Navbar with new layout |
||||
6. Update HomePage to remove SearchFilter and pass props to Navbar |
||||
7. Test on desktop and mobile |
||||
8. Remove SearchFilter component if no longer needed |
||||
9. Clean up unused CSS |
||||
|
||||
## Testing Checklist |
||||
|
||||
- [ ] Search works from navbar |
||||
- [ ] Sort dropdown changes video order |
||||
- [ ] Channel filter works |
||||
- [ ] Clear filters button appears when filters active |
||||
- [ ] Clear filters button resets everything |
||||
- [ ] Pagination resets to 1 when filters change |
||||
- [ ] URL updates with query params |
||||
- [ ] Browser back/forward works with filters |
||||
- [ ] Mobile responsive (hamburger menu or stacked layout) |
||||
- [ ] Navbar doesn't show search on Admin/Login pages |
||||
|
||||
## Alternative: Simpler Approach |
||||
|
||||
If URL params are too complex, keep state in HomePage and pass callbacks to Navbar: |
||||
|
||||
```typescript |
||||
// App.tsx |
||||
function App() { |
||||
const [navbarProps, setNavbarProps] = useState({}); |
||||
|
||||
return ( |
||||
<Navbar {...navbarProps} /> |
||||
<Routes> |
||||
<Route path="/" element={<HomePage setNavbar={setNavbarProps} />} /> |
||||
</Routes> |
||||
); |
||||
} |
||||
``` |
||||
|
||||
But this requires lifting state to App level and prop drilling. |
||||
|
||||
## Recommendation |
||||
|
||||
**Best approach:** |
||||
1. Use URL query params for filter state |
||||
2. Navbar reads from URL and updates URL on changes |
||||
3. HomePage reads from URL for fetching videos |
||||
4. Clean, shareable, no prop drilling |
||||
|
||||
**Estimated time:** 30-45 minutes |
||||
**Complexity:** Medium (URL params + responsive styling) |
||||
|
||||
@ -1,148 +0,0 @@
@@ -1,148 +0,0 @@
|
||||
# Pagination Bug Investigation Plan |
||||
|
||||
## Problem Statement |
||||
The `meta.page` property in API responses always shows `1`, even when requesting page 2, 3, etc. |
||||
|
||||
## Request Flow Analysis |
||||
|
||||
### Step 1: Frontend sends request |
||||
**File:** `frontend/src/services/apiClient.ts` line 113 |
||||
```typescript |
||||
getAll: (params?: any) => api.get('/videos', { params }) |
||||
``` |
||||
**What happens:** |
||||
- Axios converts params object to query string |
||||
- Request: `GET /api/videos?page=2&limit=12&sort=newest` |
||||
|
||||
### Step 2: Express receives request |
||||
**What happens:** |
||||
- Express parses query string into `req.query` object |
||||
- All values are **strings**: `{ page: '2', limit: '12', sort: 'newest' }` |
||||
|
||||
### Step 3: Validation Middleware |
||||
**File:** `backend/src/middleware/validation.ts` line 21-30 |
||||
```typescript |
||||
const validated = schema.parse(req.body || req.query); |
||||
if (req.method === 'GET') { |
||||
req.query = validated as any; |
||||
} |
||||
``` |
||||
|
||||
**Schema:** `backend/src/middleware/validation.ts` line 14 |
||||
```typescript |
||||
page: z.coerce.number().int().min(1).default(1) |
||||
``` |
||||
|
||||
**POTENTIAL BUG #1:** |
||||
- `.default(1)` only applies when value is `undefined` |
||||
- If page='2' (string), Zod should: |
||||
1. Check if undefined → NO (it's '2') |
||||
2. Coerce to number → page becomes 2 |
||||
3. Validate int and min(1) → passes |
||||
4. Result: `validated = { page: 2, ... }` |
||||
|
||||
**QUESTION:** Is req.query being properly replaced? |
||||
|
||||
### Step 4: Controller receives request |
||||
**File:** `backend/src/controllers/videos.controller.ts` line 10-13 |
||||
```typescript |
||||
const { page = 1, limit = 12, channelId, search, sort = 'newest' } = req.query as any; |
||||
|
||||
const pageNum = page as number; |
||||
const limitNum = limit as number; |
||||
``` |
||||
|
||||
**POTENTIAL BUG #2:** |
||||
- Destructuring defaults (= 1, = 12) only apply if value is `undefined` |
||||
- After validation, `page` should be a number (not undefined) |
||||
- So `pageNum` should equal whatever `page` is |
||||
|
||||
**QUESTION:** Is `req.query.page` actually the validated number? |
||||
|
||||
### Step 5: Response |
||||
**File:** `backend/src/controllers/videos.controller.ts` line 87-98 |
||||
```typescript |
||||
res.json({ |
||||
success: true, |
||||
data: { videos }, |
||||
meta: { |
||||
page: pageNum, // Should be 2 if we requested page 2 |
||||
... |
||||
} |
||||
}); |
||||
``` |
||||
|
||||
## Root Cause Hypotheses |
||||
|
||||
### Hypothesis 1: Validation middleware not working |
||||
**Evidence needed:** |
||||
- Add `console.log('Before validation:', req.query)` before line 24 in validation.ts |
||||
- Add `console.log('After validation:', validated)` after line 24 in validation.ts |
||||
- Check if validation is even running |
||||
|
||||
### Hypothesis 2: req.query not being replaced |
||||
**Evidence needed:** |
||||
- Add `console.log('req.query in controller:', req.query)` at line 11 in videos.controller.ts |
||||
- Check if req.query has numbers or strings |
||||
- Check if req.query.page is actually 2 when we request page 2 |
||||
|
||||
### Hypothesis 3: Type coercion issue |
||||
**Evidence needed:** |
||||
- Add `console.log('pageNum:', pageNum, 'type:', typeof pageNum)` at line 14 in videos.controller.ts |
||||
- Check if pageNum is actually a number or if it's somehow being converted back to default |
||||
|
||||
### Hypothesis 4: Multiple requests interfering |
||||
**Evidence needed:** |
||||
- Check browser network tab to see if there are duplicate requests |
||||
- One request might be page 2, another might be page 1 |
||||
- Frontend might be showing response from wrong request |
||||
|
||||
## Debugging Steps |
||||
|
||||
1. **Add logging to validation middleware** |
||||
- Log `req.query` before validation |
||||
- Log `validated` result after validation |
||||
- Verify Zod is correctly converting and not defaulting |
||||
|
||||
2. **Add logging to controller** |
||||
- Log `req.query` when controller receives it |
||||
- Log `page`, `pageNum`, `offset` calculations |
||||
- Verify the response meta.page value |
||||
|
||||
3. **Check browser network tab** |
||||
- Verify the request URL includes correct page parameter |
||||
- Verify the response meta.page value |
||||
- Check if there are multiple simultaneous requests |
||||
|
||||
4. **Test with direct curl** |
||||
- `curl "http://localhost:3000/api/videos?page=2&limit=12"` |
||||
- See if backend returns correct page in meta |
||||
- This isolates frontend vs backend issue |
||||
|
||||
## Expected Behavior |
||||
|
||||
Request: `GET /api/videos?page=2` |
||||
Response: |
||||
```json |
||||
{ |
||||
"success": true, |
||||
"data": { "videos": [...] }, |
||||
"meta": { |
||||
"page": 2, // Should be 2! |
||||
"limit": 12, |
||||
"total": 60, |
||||
"totalPages": 5, |
||||
... |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## Action Items |
||||
|
||||
1. Add debug logging to both validation middleware and controller |
||||
2. Test with page 2 request and check all console.logs |
||||
3. Based on logs, identify which hypothesis is correct |
||||
4. Fix the actual bug |
||||
5. Remove debug logging |
||||
6. Test pagination works correctly |
||||
|
||||
Loading…
Reference in new issue