4 KiB
4 KiB
System Patterns
Architecture
Single-container Express app serves both the React SPA and the REST API. MongoDB runs in a separate container on an internal Docker network.
Browser
└─► Caddy (caddy_network)
└─► deafmissoulaorg-app-1:801
├─ GET /api/* → Express routes
├─ GET /uploads/* → multer upload volume
├─ GET /photos/*, /videos/*, etc. → client/build/ static assets
└─ GET * → client/build/index.html (SPA catch-all)
└─► deafmissoulaorg-mongodb-1:27017 (internal network only)
Data Flow
All frontend data is fetched from the API at runtime — nothing is hardcoded in components. Each component does a useEffect → fetch('/api/...') → useState pattern.
File Organization
client/src/components/
├── Manage.tsx # /manage dashboard (auth + all admin tabs)
├── App.tsx # Routing: /manage/* outside Header/Footer
├── Home.tsx # Landing page (fetches events)
├── Calendar.tsx # Events calendar (fetches events)
├── Meet-Board.tsx # Board member profiles (fetches /api/board)
├── Gallery.tsx # Photo gallery (fetches /api/gallery)
├── VideoGallery.tsx # Video gallery (hardcoded paths in client/public/videos/)
├── Minutes.tsx # Meeting minutes table (fetches /api/minutes)
├── Sponsors.tsx # Sponsors (fetches /api/sponsors)
├── Bylaws.tsx # Static PDF links (hardcoded paths in client/public/)
└── Resources.tsx # Montana/National Deaf resources directory (static)
server/src/
├── models/ # Mongoose schemas
│ ├── BoardMember.ts # Includes auth fields (password, mustChangePassword, resetToken)
│ ├── Event.ts
│ ├── GalleryItem.ts
│ ├── Minute.ts # Has 'order' field for drag-and-drop sorting
│ ├── Sponsor.ts
│ ├── Member.ts
│ └── User.ts # Admin-only account (not BoardMember)
├── routes/
│ ├── auth.ts # Login, forgot/reset password, change password
│ ├── board.ts # CRUD + reorder + welcome email on create
│ ├── minutes.ts # CRUD + reorder (new entries auto-insert at top)
│ ├── events.ts
│ ├── gallery.ts
│ ├── sponsors.ts
│ ├── members.ts
│ └── email.ts # Contact form
├── middleware/
│ ├── auth.ts # JWT verification, exports AuthRequest
│ ├── upload.ts # Multer config for headshots/photos/sponsors/minutes
│ └── errorHandler.ts # ZodError → 400, others → 500
├── db.ts # mongoose.connect()
├── index.ts # Express app setup, static serving, route mounting
└── seed.ts # One-time data population (idempotent — skips if data exists)
Dashboard (/manage) Patterns
- Route security:
/manageis not linked from public nav (obscurity) + JWT required for all write operations - Login: Checks BoardMember by email first, then User (admin fallback)
- Drag-and-drop:
useDragReorder<T>custom hook using HTML5 drag API +useReffor drag index tracking; callsPATCH /api/[resource]/reorderwith{ ids: string[] }on drop - Image preview:
URL.createObjectURL(file)inImagePickercomponent before upload - Forced password change:
mustChangePassword: true→ server issuestempToken(1-hour) → client shows change-password form before dashboard loads
Deployment Pattern
- Code changes pushed to Gitea (
git push origin main) - SSH to production server (
ssh chaulmark@10.4.0.205) git pull origin main && docker compose down && docker compose up -d --build
No Portainer. No webhooks. Manual pull + rebuild.
Rollback Pattern
v1 tag in Git points to the last hardcoded version (no database):
git checkout v1
docker compose down
docker compose up -d --build