15 changed files with 960 additions and 193 deletions
@ -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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
# 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 @@ |
|||||||
# 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