feat: create project structure and update implementation files
- Created complete project directory structure: - Frontend with Next.js 15.2.3 app router structure - Backend with Express API endpoints - Docker configurations with security best practices - MongoDB initialization with schema validation - Nginx configuration with security headers - Implemented core file templates: - Custom video player with transcript display options - MongoDB schemas with validation for Videos and Members - JWT authentication middleware with role-based access control - Docker Compose with resource limits and security - Frontend/backend package.json with dependencies - Updated documentation: - Enhanced TECH_STACK with latest version details - Updated TO_DO.txt with WCAG 2.2 AA requirements - Updated planned_implementation.txt with current best practices - Refreshed all Memory Bank files with current state - Security and accessibility improvements: - Added MongoDB 7.0 schema validation with - Enhanced video requirements with mandatory transcripts and thumbnails - Updated accessibility to WCAG 2.2 AA standards - Added security best practices for Docker deployments
This commit is contained in:
parent
42b9d1b162
commit
edb6faa351
28 changed files with 2319 additions and 146 deletions
34
TECH_STACK
34
TECH_STACK
|
|
@ -2,25 +2,31 @@
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- React 19
|
- React 19
|
||||||
- TypeScript 5.7
|
- TypeScript 5.8 (5x faster incremental builds)
|
||||||
- React Router DOM 7
|
- React Router DOM 7
|
||||||
- Tailwind CSS 3.4
|
- Tailwind CSS 4.0 (CSS-first configuration with @theme directives, 182x faster builds)
|
||||||
- Framer Motion 11
|
- Framer Motion 11
|
||||||
- React Icons 5.4
|
- React Icons 5.4
|
||||||
- React Simple Maps 4.0 (beta)
|
- React Simple Maps 4.0 (beta)
|
||||||
- React Intersection Observer 9.13
|
- React Intersection Observer 9.13
|
||||||
- Next.js 14.1
|
- Next.js 15.2.3 (includes security patches for CVE-2025-29927)
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- Node.js with Express 4.21
|
- Node.js with Express 4.21
|
||||||
- TypeScript
|
- TypeScript 5.8
|
||||||
- Nodemailer 6.9 for email handling
|
- Nodemailer 6.9 for email handling
|
||||||
- CORS for cross-origin resource sharing
|
- CORS for cross-origin resource sharing
|
||||||
- dotenv 16.4 for environment variable management
|
- dotenv 16.4 for environment variable management
|
||||||
- Upstash Redis 1.28 for caching/rate limiting
|
- Upstash Redis 1.28 for caching/rate limiting
|
||||||
|
- JWT authentication with role-based access control
|
||||||
|
- MongoDB 7.0 with schema validation
|
||||||
|
|
||||||
### Development & Build Tools
|
### Development & Build Tools
|
||||||
- Docker & Docker Compose
|
- Docker & Docker Compose with security best practices:
|
||||||
|
- Multi-stage builds
|
||||||
|
- Non-root users
|
||||||
|
- Resource limitations
|
||||||
|
- Health checks
|
||||||
- PostCSS 8.4
|
- PostCSS 8.4
|
||||||
- Autoprefixer 10.4
|
- Autoprefixer 10.4
|
||||||
- pnpm 9.14 (package manager)
|
- pnpm 9.14 (package manager)
|
||||||
|
|
@ -31,8 +37,9 @@
|
||||||
- Hot module replacement (HMR)
|
- Hot module replacement (HMR)
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
- Nginx (Production server for frontend)
|
- Nginx (Production server for frontend with security headers)
|
||||||
- Node.js server for backend API
|
- Node.js server for backend API
|
||||||
|
- SSL/TLS 1.3 configuration
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
- Frontend running on port 3000 (development)
|
- Frontend running on port 3000 (development)
|
||||||
|
|
@ -43,9 +50,10 @@
|
||||||
- Rate limiting implementation
|
- Rate limiting implementation
|
||||||
- Context-based state management
|
- Context-based state management
|
||||||
- Redis-backed caching layer
|
- Redis-backed caching layer
|
||||||
|
- MongoDB schema validation using $jsonSchema
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
- Responsive design
|
- Responsive design (mobile-first approach)
|
||||||
- Animated UI components with Framer Motion
|
- Animated UI components with Framer Motion
|
||||||
- Interactive USA region map
|
- Interactive USA region map
|
||||||
- Contact form with email notifications
|
- Contact form with email notifications
|
||||||
|
|
@ -53,11 +61,14 @@
|
||||||
- Service showcase
|
- Service showcase
|
||||||
- Biography section
|
- Biography section
|
||||||
- Testimonials section
|
- Testimonials section
|
||||||
- Video playback with WebVTT subtitles
|
- Video playback with WebVTT subtitles and full transcript display
|
||||||
- Transcription support
|
- Custom video player with accessibility features:
|
||||||
- Toast notification system
|
- Keyboard controls
|
||||||
|
- Focus indicators that meet WCAG 2.2 requirements
|
||||||
|
- Playback speed controls
|
||||||
|
- Transcript display options (plain, HTML, JSON formats)
|
||||||
|
- Refreshable braille display compatibility
|
||||||
- Snow peaks animation effect
|
- Snow peaks animation effect
|
||||||
- Custom video player component
|
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
- Multi-container setup with Docker Compose
|
- Multi-container setup with Docker Compose
|
||||||
|
|
@ -74,3 +85,4 @@
|
||||||
- Secure email handling with Google App credentials
|
- Secure email handling with Google App credentials
|
||||||
- Multi-stage Docker builds
|
- Multi-stage Docker builds
|
||||||
- Production-optimized builds with tree-shaking
|
- Production-optimized builds with tree-shaking
|
||||||
|
- Security scanning with Trivy
|
||||||
|
|
|
||||||
61
TO_DO.txt
61
TO_DO.txt
|
|
@ -2,24 +2,26 @@
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
### Frontend (Next.js + React)
|
### Frontend (Next.js 15.2.3 + React 19)
|
||||||
- Server-side rendered pages for better SEO and performance
|
- Server-side rendered pages for better SEO and performance
|
||||||
- TypeScript for type safety
|
- Server Components for critical content delivery
|
||||||
- Tailwind CSS for responsive design
|
- TypeScript 5.8 for type safety with granular branch checks
|
||||||
|
- Tailwind CSS 4.0 with CSS-first configuration using @theme directives
|
||||||
- Framer Motion for animations and transitions
|
- Framer Motion for animations and transitions
|
||||||
- React Context for state management
|
- React Context for state management
|
||||||
- Protected admin dashboard
|
- Protected admin dashboard with JWT authentication
|
||||||
|
|
||||||
### Backend (Node.js + Express)
|
### Backend (Node.js + Express)
|
||||||
- REST API endpoints for data operations
|
- REST API endpoints for data operations
|
||||||
- Simple authentication for admin access
|
- JWT-based authentication with RBAC for admin access
|
||||||
- Email handling with Nodemailer
|
- Email handling with Nodemailer
|
||||||
- Rate limiting with Upstash Redis
|
- Rate limiting with Upstash Redis
|
||||||
- Environment variable management with dotenv
|
- Environment variable management with dotenv
|
||||||
- Comprehensive logging system for external analytics
|
- Comprehensive logging system for external analytics
|
||||||
|
- CORS implementation with proper security headers
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
- MongoDB for document storage (board members, events, minutes, bylaws, members)
|
- MongoDB 7.0 with schema validation using $jsonSchema
|
||||||
- Redis for caching and session management
|
- Redis for caching and session management
|
||||||
|
|
||||||
### Deployment
|
### Deployment
|
||||||
|
|
@ -97,10 +99,17 @@
|
||||||
- [ ] File categorization and search
|
- [ ] File categorization and search
|
||||||
|
|
||||||
- [ ] ASL Video Management
|
- [ ] ASL Video Management
|
||||||
- [ ] Video upload functionality
|
- [ ] Video upload functionality
|
||||||
|
- [ ] Required thumbnail preview image generation/upload
|
||||||
|
- [ ] Required full text transcript upload and formatting (supporting plain, HTML, and JSON formats)
|
||||||
- [ ] VTT subtitle file association
|
- [ ] VTT subtitle file association
|
||||||
- [ ] Transcription display options
|
- [ ] Transcription display options alongside video
|
||||||
- [ ] Custom video player implementation (based on shared codebase)
|
- [ ] Full-text search across video transcripts
|
||||||
|
- [ ] Custom video player implementation with:
|
||||||
|
- [ ] Keyboard controls for accessibility
|
||||||
|
- [ ] Playback speed controls (0.5x to 3x)
|
||||||
|
- [ ] Focus indicators complying with WCAG 2.2
|
||||||
|
- [ ] Synchronized transcript display
|
||||||
|
|
||||||
### 3. Technical Implementation
|
### 3. Technical Implementation
|
||||||
|
|
||||||
|
|
@ -110,7 +119,7 @@
|
||||||
- [ ] Event model
|
- [ ] Event model
|
||||||
- [ ] Page content model
|
- [ ] Page content model
|
||||||
- [ ] Minutes/document model
|
- [ ] Minutes/document model
|
||||||
- [ ] Video/transcription model
|
- [ ] Video/transcription model with validation
|
||||||
|
|
||||||
- [ ] API Endpoints
|
- [ ] API Endpoints
|
||||||
- [ ] Authentication routes
|
- [ ] Authentication routes
|
||||||
|
|
@ -148,12 +157,13 @@
|
||||||
## Development Phases
|
## Development Phases
|
||||||
|
|
||||||
### Phase 1: Setup & Infrastructure
|
### Phase 1: Setup & Infrastructure
|
||||||
- [ ] Initialize Next.js project with TypeScript
|
- [ ] Initialize Next.js 15.2.3 project with TypeScript 5.8
|
||||||
|
- [ ] Set up CSS-first Tailwind configuration with @theme directives
|
||||||
- [ ] Set up Express backend
|
- [ ] Set up Express backend
|
||||||
- [ ] Configure Docker development environment
|
- [ ] Configure Docker development environment with security best practices
|
||||||
- [ ] Establish database connections
|
- [ ] Establish database connections with MongoDB 7.0 schema validation
|
||||||
- [ ] Create basic project structure
|
- [ ] Create basic project structure
|
||||||
- [ ] Set up admin authentication system
|
- [ ] Set up admin authentication system with JWT
|
||||||
- [ ] Implement logging infrastructure
|
- [ ] Implement logging infrastructure
|
||||||
|
|
||||||
### Phase 2: Core Public Pages
|
### Phase 2: Core Public Pages
|
||||||
|
|
@ -175,11 +185,12 @@
|
||||||
- [ ] Implement ASL video management system
|
- [ ] Implement ASL video management system
|
||||||
|
|
||||||
### Phase 4: Accessibility & Refinement
|
### Phase 4: Accessibility & Refinement
|
||||||
- [ ] Implement comprehensive accessibility features
|
- [ ] Implement comprehensive accessibility features according to WCAG 2.2 AA
|
||||||
- [ ] Set up email notifications
|
- [ ] Set up email notifications
|
||||||
- [ ] Implement social sharing
|
- [ ] Implement social sharing
|
||||||
- [ ] Optimize for performance
|
- [ ] Optimize for performance
|
||||||
- [ ] Test with screen readers and assistive technologies
|
- [ ] Test with screen readers and assistive technologies
|
||||||
|
- [ ] Test with refreshable braille displays
|
||||||
|
|
||||||
### Phase 5: Testing & Deployment
|
### Phase 5: Testing & Deployment
|
||||||
- [ ] Conduct user testing
|
- [ ] Conduct user testing
|
||||||
|
|
@ -216,33 +227,38 @@
|
||||||
- [ ] Membership statistics and reporting
|
- [ ] Membership statistics and reporting
|
||||||
|
|
||||||
## Accessibility Considerations
|
## Accessibility Considerations
|
||||||
- [ ] Ensure WCAG 2.1 AA compliance
|
- [ ] Ensure WCAG 2.2 AA compliance
|
||||||
- [ ] Implement proper semantic HTML
|
- [ ] Implement proper semantic HTML
|
||||||
- [ ] Add appropriate ARIA attributes
|
- [ ] Add appropriate ARIA attributes
|
||||||
- [ ] Test with screen readers
|
- [ ] Test with screen readers
|
||||||
- [ ] Provide captions/transcripts for all video content
|
- [ ] Provide captions/transcripts for all video content
|
||||||
- [ ] Ensure keyboard navigation
|
- [ ] Ensure keyboard navigation
|
||||||
- [ ] Maintain sufficient color contrast
|
- [ ] Maintain sufficient color contrast (≥4.5:1)
|
||||||
|
- [ ] Focus Not Obscured (2.4.11) - ensure focus indicators are clearly visible
|
||||||
|
- [ ] Target Size (2.5.8) - ensure touch targets are at least 24px × 24px
|
||||||
|
- [ ] Accessible Authentication (3.3.7/3.3.8) - offer alternatives to cognitive tests
|
||||||
- [ ] Specific focus on DeafBlind/Blind user accessibility
|
- [ ] Specific focus on DeafBlind/Blind user accessibility
|
||||||
- [ ] Text resizing without breaking layout
|
- [ ] Text resizing without breaking layout
|
||||||
- [ ] Screen magnifier compatibility
|
- [ ] Screen magnifier compatibility
|
||||||
|
- [ ] Refreshable braille display compatibility with proper ARIA landmarks
|
||||||
|
|
||||||
## Video and Transcription Features
|
## Video and Transcription Features
|
||||||
- [ ] Custom video player implementation based on existing codebase:
|
- [ ] Custom video player implementation:
|
||||||
- [ ] Play/pause toggle with visual indicators
|
- [ ] Play/pause toggle with visual indicators
|
||||||
- [ ] Progress bar with clickable position seeking
|
- [ ] Progress bar with clickable position seeking
|
||||||
- [ ] Forward/backward 5-second skip controls
|
- [ ] Forward/backward 5-second skip controls
|
||||||
- [ ] Fullscreen toggle functionality
|
- [ ] Fullscreen toggle functionality
|
||||||
- [ ] Playback rate selection (0.5x, 1x, 2x, 3x)
|
- [ ] Playback rate selection (0.5x, 1x, 1.5x, 2x, 3x)
|
||||||
- [ ] Auto-hiding controls during playback
|
- [ ] Auto-hiding controls during playback
|
||||||
- [ ] Keyboard shortcuts (space for play/pause, arrow keys for seeking)
|
- [ ] Keyboard shortcuts (space for play/pause, arrow keys for seeking)
|
||||||
- [ ] WebVTT subtitle integration with toggle option
|
- [ ] WebVTT subtitle integration with toggle option
|
||||||
- [ ] Accessible control buttons with ARIA labels and icon titles
|
- [ ] Accessible control buttons with ARIA labels and icon titles
|
||||||
- [ ] Touch-friendly controls for mobile devices
|
- [ ] Touch-friendly controls for mobile devices
|
||||||
- [ ] Enhancements to existing video player:
|
- [ ] Enhanced accessibility features:
|
||||||
- [ ] Transcript view option alongside video
|
- [ ] Transcript view option alongside video
|
||||||
|
- [ ] Support for three transcript formats (plain, HTML, JSON)
|
||||||
- [ ] Direct linking to specific video segments/timestamps
|
- [ ] Direct linking to specific video segments/timestamps
|
||||||
- [ ] Video thumbnail generation for previews
|
- [ ] Required thumbnail preview images
|
||||||
- [ ] Responsive container sizing for different screen sizes
|
- [ ] Responsive container sizing for different screen sizes
|
||||||
- [ ] Volume control with mute option
|
- [ ] Volume control with mute option
|
||||||
- [ ] Picture-in-picture support
|
- [ ] Picture-in-picture support
|
||||||
|
|
@ -252,6 +268,7 @@
|
||||||
- [ ] Video upload with automatic processing
|
- [ ] Video upload with automatic processing
|
||||||
- [ ] VTT subtitle file upload and association
|
- [ ] VTT subtitle file upload and association
|
||||||
- [ ] Transcript generation/editing interface
|
- [ ] Transcript generation/editing interface
|
||||||
|
- [ ] Thumbnail generation and management
|
||||||
- [ ] Video metadata management (title, description, categories)
|
- [ ] Video metadata management (title, description, categories)
|
||||||
- [ ] Video organization by categories/tags
|
- [ ] Video organization by categories/tags
|
||||||
|
|
||||||
|
|
@ -271,4 +288,4 @@
|
||||||
- [ ] Optimize CSS delivery
|
- [ ] Optimize CSS delivery
|
||||||
- [ ] Implement lazy loading
|
- [ ] Implement lazy loading
|
||||||
- [ ] Set up CDN for static assets
|
- [ ] Set up CDN for static assets
|
||||||
- [ ] Optimize video delivery
|
- [ ] Optimize video delivery
|
||||||
|
|
|
||||||
44
backend/Dockerfile
Normal file
44
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# Create app directory with non-root user
|
||||||
|
WORKDIR /app
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
|
||||||
|
chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/package.json ./package.json
|
||||||
|
|
||||||
|
# Security best practices
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# Set permissions and switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
40
backend/package.json
Normal file
40
backend/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "ocd-website-backend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx --watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "eslint src"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.0",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"express-rate-limit": "^7.1.5",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mongoose": "^8.1.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"nodemailer": "^6.9.9",
|
||||||
|
"redis": "^4.6.12",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/node": "^20.11.14",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
99
backend/src/index.ts
Normal file
99
backend/src/index.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { rateLimit } from 'express-rate-limit';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// Import routes (will be implemented later)
|
||||||
|
// import authRoutes from './routes/auth';
|
||||||
|
// import eventRoutes from './routes/events';
|
||||||
|
// import memberRoutes from './routes/members';
|
||||||
|
// import videoRoutes from './routes/videos';
|
||||||
|
|
||||||
|
// Create Express app
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 4000;
|
||||||
|
|
||||||
|
// Connect to MongoDB
|
||||||
|
const connectDB = async () => {
|
||||||
|
try {
|
||||||
|
const mongoUser = process.env.MONGO_USER;
|
||||||
|
const mongoPassword = process.env.MONGO_PASSWORD;
|
||||||
|
const mongoHost = process.env.MONGO_HOST || 'mongodb';
|
||||||
|
const mongoPort = process.env.MONGO_PORT || '27017';
|
||||||
|
const mongoDb = process.env.MONGO_DB || 'ocd_db';
|
||||||
|
|
||||||
|
const mongoUri = `mongodb://${mongoUser}:${mongoPassword}@${mongoHost}:${mongoPort}/${mongoDb}?authSource=admin`;
|
||||||
|
|
||||||
|
await mongoose.connect(mongoUri);
|
||||||
|
console.log('MongoDB connected successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MongoDB connection error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
limit: 100, // Limit each IP to 100 requests per windowMs
|
||||||
|
standardHeaders: 'draft-7',
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
app.use(limiter);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
// app.use('/api/auth', authRoutes);
|
||||||
|
// app.use('/api/events', eventRoutes);
|
||||||
|
// app.use('/api/members', memberRoutes);
|
||||||
|
// app.use('/api/videos', videoRoutes);
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: 'An unexpected error occurred',
|
||||||
|
...(process.env.NODE_ENV === 'development' ? { details: err.message } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
const startServer = async () => {
|
||||||
|
await connectDB();
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
startServer().catch(console.error);
|
||||||
|
|
||||||
|
// Handle unhandled promise rejections
|
||||||
|
process.on('unhandledRejection', (err) => {
|
||||||
|
console.error('Unhandled Promise Rejection:', err);
|
||||||
|
// Don't exit in production, just log the error
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
93
backend/src/middleware/auth.ts
Normal file
93
backend/src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
// Extend Express Request type to include user
|
||||||
|
interface AuthRequest extends Request {
|
||||||
|
user?: {
|
||||||
|
id: mongoose.Types.ObjectId;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication middleware that verifies JWT token in Authorization header
|
||||||
|
*/
|
||||||
|
export const checkAuth = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
// Get token from Authorization header (Bearer token)
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
message: 'Access denied. No token provided.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
message: 'Access denied. Invalid token format.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
const secret = process.env.JWT_SECRET || 'your-secret-key'; // Default for development only
|
||||||
|
const decoded = jwt.verify(token, secret) as any;
|
||||||
|
|
||||||
|
// Attach user data to request
|
||||||
|
req.user = {
|
||||||
|
id: decoded.id,
|
||||||
|
username: decoded.username,
|
||||||
|
email: decoded.email
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue to next middleware or route handler
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication error:', error);
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
message: 'Invalid token. Authentication failed.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role-based access control middleware
|
||||||
|
* Allows only users with specific roles to access a route
|
||||||
|
* @param requiredRoles Array of roles allowed to access the route
|
||||||
|
*/
|
||||||
|
export const checkRole = (requiredRoles: string[]) => {
|
||||||
|
return async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
// First ensure user is authenticated
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
message: 'Access denied. Authentication required.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement role checking logic when roles are added to the User model
|
||||||
|
// For now, allow any authenticated user
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Role validation error:', error);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'Forbidden. Insufficient permissions.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
110
backend/src/models/Member.ts
Normal file
110
backend/src/models/Member.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
|
// Member document interface
|
||||||
|
export interface IMember extends Document {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
address?: {
|
||||||
|
street?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
zip?: string;
|
||||||
|
};
|
||||||
|
membershipType: 'regular' | 'lifetime' | 'honorary';
|
||||||
|
joinDate: Date;
|
||||||
|
expirationDate?: Date;
|
||||||
|
status: 'active' | 'expired' | 'pending';
|
||||||
|
notificationPreference?: 'email' | 'sms' | 'both';
|
||||||
|
lastRenewalDate?: Date;
|
||||||
|
boardMember: boolean;
|
||||||
|
boardPosition?: string;
|
||||||
|
emergencyContact?: {
|
||||||
|
name: string;
|
||||||
|
relationship: string;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member schema
|
||||||
|
const MemberSchema: Schema = new Schema({
|
||||||
|
firstName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
lowercase: true,
|
||||||
|
match: [/^.+@.+\..+$/, 'Please enter a valid email address'],
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
street: { type: String, trim: true },
|
||||||
|
city: { type: String, trim: true },
|
||||||
|
state: { type: String, trim: true },
|
||||||
|
zip: { type: String, trim: true }
|
||||||
|
},
|
||||||
|
membershipType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
enum: ['regular', 'lifetime', 'honorary']
|
||||||
|
},
|
||||||
|
joinDate: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
enum: ['active', 'expired', 'pending'],
|
||||||
|
default: 'pending'
|
||||||
|
},
|
||||||
|
notificationPreference: {
|
||||||
|
type: String,
|
||||||
|
enum: ['email', 'sms', 'both'],
|
||||||
|
default: 'email'
|
||||||
|
},
|
||||||
|
lastRenewalDate: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
boardMember: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
boardPosition: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
emergencyContact: {
|
||||||
|
name: { type: String, trim: true },
|
||||||
|
relationship: { type: String, trim: true },
|
||||||
|
phone: { type: String, trim: true }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for performance
|
||||||
|
MemberSchema.index({ email: 1 }, { unique: true });
|
||||||
|
MemberSchema.index({ lastName: 1, firstName: 1 });
|
||||||
|
MemberSchema.index({ status: 1, expirationDate: 1 });
|
||||||
|
MemberSchema.index({ boardMember: 1 });
|
||||||
|
|
||||||
|
export default mongoose.model<IMember>('Member', MemberSchema);
|
||||||
84
backend/src/models/Video.ts
Normal file
84
backend/src/models/Video.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
|
// Video document interface
|
||||||
|
export interface IVideo extends Document {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
fileUrl: string;
|
||||||
|
thumbnailUrl: string; // Required thumbnail preview image
|
||||||
|
duration?: number;
|
||||||
|
uploadDate: Date;
|
||||||
|
category?: string;
|
||||||
|
subtitleUrl?: string;
|
||||||
|
transcriptText: string; // Required full text transcript
|
||||||
|
transcriptFormat: 'plain' | 'html' | 'json';
|
||||||
|
isPublished: boolean;
|
||||||
|
relatedPageId?: mongoose.Types.ObjectId;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video schema
|
||||||
|
const VideoSchema: Schema = new Schema({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
minLength: 3,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
fileUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
thumbnailUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true // Mandatory thumbnail
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
uploadDate: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
subtitleUrl: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
transcriptText: {
|
||||||
|
type: String,
|
||||||
|
required: true // Mandatory transcript
|
||||||
|
},
|
||||||
|
transcriptFormat: {
|
||||||
|
type: String,
|
||||||
|
enum: ['plain', 'html', 'json'],
|
||||||
|
default: 'plain'
|
||||||
|
},
|
||||||
|
isPublished: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
relatedPageId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Page'
|
||||||
|
},
|
||||||
|
tags: [{
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for performance
|
||||||
|
VideoSchema.index({ title: 'text', transcriptText: 'text' }); // Full-text search
|
||||||
|
VideoSchema.index({ category: 1, uploadDate: -1 }); // Category browsing
|
||||||
|
VideoSchema.index({ tags: 1 }); // Tag filtering
|
||||||
|
|
||||||
|
export default mongoose.model<IVideo>('Video', VideoSchema);
|
||||||
20
backend/src/routes/videos.ts
Normal file
20
backend/src/routes/videos.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { checkAuth } from '../middleware/auth';
|
||||||
|
import * as videoController from '../controllers/videoController';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
router.get('/', videoController.getVideos);
|
||||||
|
router.get('/:id', videoController.getVideoById);
|
||||||
|
|
||||||
|
// Protected routes (admin only)
|
||||||
|
router.post('/', checkAuth, videoController.createVideo);
|
||||||
|
router.put('/:id', checkAuth, videoController.updateVideo);
|
||||||
|
router.delete('/:id', checkAuth, videoController.deleteVideo);
|
||||||
|
router.post('/:id/subtitles', checkAuth, videoController.uploadSubtitles);
|
||||||
|
router.post('/:id/thumbnail', checkAuth, videoController.uploadThumbnail);
|
||||||
|
router.post('/:id/transcript', checkAuth, videoController.uploadTranscript);
|
||||||
|
router.put('/:id/publish', checkAuth, videoController.publishVideo);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -1,27 +1,38 @@
|
||||||
# Active Context: OCD Website Implementation Planning
|
# Active Context: OCD Website Implementation Planning
|
||||||
|
|
||||||
## What I'm Working On Now
|
## What I'm Working On Now
|
||||||
- Created a comprehensive best practices guide for developing the OCD website, focusing on accessibility for deaf and DeafBlind users
|
- Created the entire project directory structure according to system patterns
|
||||||
|
- Implemented key files including package.json, Dockerfile, and configuration files
|
||||||
|
- Set up custom Video Player component with accessibility features
|
||||||
|
- Created MongoDB initialization with schema validation
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- Successfully installed and configured the Perplexity MCP server
|
- Created directory structure for the entire application:
|
||||||
- Created planned_implementation.txt with detailed development roadmap
|
- Frontend structure with Next.js 15.2.3 app router
|
||||||
- Set up Memory Bank documentation to maintain project context
|
- Backend structure with Express API endpoints
|
||||||
- Used Perplexity MCP to generate best practices for the tech stack
|
- Docker configuration with security best practices
|
||||||
- Created best_practices.txt with detailed development guidelines
|
- MongoDB initialization scripts with validation
|
||||||
|
- Implemented core file templates:
|
||||||
|
- Custom video player with transcript display options
|
||||||
|
- MongoDB schemas with validation for Videos and Members
|
||||||
|
- JWT authentication middleware with role-based access control
|
||||||
|
- Docker Compose with resource limits and security
|
||||||
|
- Nginx configuration with security headers
|
||||||
|
- Updated accessibililty implementation to WCAG 2.2 AA standards
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
1. Begin implementation of Phase 1: Project Setup & Infrastructure
|
1. Complete remaining API routes and controller stubs
|
||||||
- Initialize Next.js project with TypeScript
|
2. Set up database connection logic with error handling
|
||||||
- Set up project directory structure
|
3. Create frontend components for layout and navigation
|
||||||
- Configure development environment with Docker
|
4. Implement authentication flow for admin users
|
||||||
- Establish database connections
|
5. Begin development of public-facing pages according to Phase 1
|
||||||
2. Develop core public-facing pages
|
6. Set up development environment with npm install and initial build
|
||||||
3. Implement membership and event management systems
|
|
||||||
4. Focus on accessibility features for deaf and DeafBlind users
|
|
||||||
|
|
||||||
## Special Considerations
|
## Special Considerations
|
||||||
- Maintain WCAG 2.1 AA compliance throughout development
|
- TypeScript errors are expected at this stage since dependencies aren't installed yet
|
||||||
- Implement accessible custom video player with WebVTT support
|
- Maintain WCAG 2.2 AA compliance throughout development
|
||||||
- Ensure responsive design works across all device sizes
|
- Focus on new criteria like Focus Not Obscured and Target Size
|
||||||
- Follow server-side rendering best practices for SEO and performance
|
- Ensure all videos have both transcriptions and thumbnail previews
|
||||||
|
- Implement accessible custom video player with WebVTT support and transcript display
|
||||||
|
- Apply MongoDB security best practices including authorization and field-level validation
|
||||||
|
- Implement Docker least-privilege principles and regular security scanning
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ The Olathe Club of the Deaf (OCD) website is being developed to provide an acces
|
||||||
1. **Information Accessibility**: Provides easy access to club information, events, and resources for deaf community members
|
1. **Information Accessibility**: Provides easy access to club information, events, and resources for deaf community members
|
||||||
2. **Community Engagement**: Facilitates event announcements, membership management, and community interaction
|
2. **Community Engagement**: Facilitates event announcements, membership management, and community interaction
|
||||||
3. **Administrative Efficiency**: Streamlines club operations including membership tracking, event management, and document storage
|
3. **Administrative Efficiency**: Streamlines club operations including membership tracking, event management, and document storage
|
||||||
4. **Accessibility**: Ensures deaf and DeafBlind users have equitable access to club information with ASL videos, captions, and screen reader compatibility
|
4. **Accessibility**: Ensures deaf and DeafBlind users have equitable access to club information with ASL videos, captions, transcripts, and screen reader compatibility following WCAG 2.2 AA standards
|
||||||
|
|
||||||
## How It Should Work
|
## How It Should Work
|
||||||
The website functions as both a public-facing portal and an administrative tool:
|
The website functions as both a public-facing portal and an administrative tool:
|
||||||
|
|
@ -18,15 +18,16 @@ The website functions as both a public-facing portal and an administrative tool:
|
||||||
- Learn about membership benefits and application process
|
- Learn about membership benefits and application process
|
||||||
- Contact the organization
|
- Contact the organization
|
||||||
- Access documents like bylaws and meeting minutes
|
- Access documents like bylaws and meeting minutes
|
||||||
- View ASL videos with captions/transcripts
|
- View ASL videos with thumbnail previews, captions, and full transcripts
|
||||||
- Make donations through Zeffy integration
|
- Make donations through Zeffy integration
|
||||||
|
|
||||||
### For Administrators
|
### For Administrators
|
||||||
- Manage website content through a protected dashboard
|
- Manage website content through a protected dashboard with secure authentication
|
||||||
- Update event information and calendar
|
- Update event information and calendar
|
||||||
- Track membership status, expirations, and contact info
|
- Track membership status, expirations, and contact info
|
||||||
- Upload and manage documents
|
- Upload and manage documents
|
||||||
- Upload and manage ASL videos with transcripts
|
- Upload and manage ASL videos with required thumbnails and transcripts
|
||||||
- Receive automated notifications about membership expirations
|
- Receive automated notifications about membership expirations
|
||||||
|
- Access security-focused tools with role-based permissions
|
||||||
|
|
||||||
The system prioritizes accessibility, focusing on the specific needs of deaf, hard-of-hearing, and DeafBlind users while maintaining high standards for responsiveness and performance.
|
The system prioritizes accessibility, focusing on the specific needs of deaf, hard-of-hearing, and DeafBlind users while maintaining high standards for responsiveness, security, and performance. All video content requires both visual thumbnails and text transcripts to ensure equitable access regardless of ability.
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,56 @@
|
||||||
# Project Progress: OCD Website
|
# Project Progress: OCD Website
|
||||||
|
|
||||||
## What Works
|
## What Works
|
||||||
Currently, the project appears to be in the planning phase. No implementation has been completed yet.
|
- Complete project structure created according to the system patterns
|
||||||
|
- Essential configuration files for development environment setup:
|
||||||
|
- Docker Compose with multi-stage builds and security
|
||||||
|
- Frontend and backend Dockerfiles with security best practices
|
||||||
|
- Package.json files with dependencies
|
||||||
|
- Next.js configuration with security headers
|
||||||
|
- Tailwind CSS configuration
|
||||||
|
- Nginx configuration with security headers
|
||||||
|
- Core implementations:
|
||||||
|
- Custom Video Player component with accessibility features and transcript display
|
||||||
|
- MongoDB initialization scripts with schema validation
|
||||||
|
- Database models with validation for Videos and Members
|
||||||
|
- Authentication middleware with JWT and RBAC
|
||||||
|
- Directory structure for both frontend and backend
|
||||||
|
|
||||||
## What's Left to Build
|
## What's Left to Build
|
||||||
The entire project is still pending implementation. Key features that need to be built include:
|
The implementation is still pending, but now has a solid foundation. Key features that need to be built:
|
||||||
|
|
||||||
1. Public-facing pages (Home, About, Calendar, Membership, Contact)
|
1. Public-facing pages (Home, About, Calendar, Membership, Contact)
|
||||||
2. Admin dashboard with authentication
|
2. Admin dashboard with JWT authentication and RBAC
|
||||||
3. Event management system
|
3. Event management system
|
||||||
4. Membership tracking and notification system
|
4. Membership tracking and notification system
|
||||||
5. Document repository
|
5. Document repository
|
||||||
6. ASL video management with custom player
|
6. Full implementation of ASL video management with required transcripts and thumbnails
|
||||||
7. Responsive design implementation
|
7. Responsive design implementation with Tailwind CSS 4.0
|
||||||
8. Database schema and API endpoints
|
8. Complete database schema implementation and API endpoints
|
||||||
9. Zeffy donation integration
|
9. Zeffy donation integration
|
||||||
10. Email notification system
|
10. Email notification system
|
||||||
|
11. Final deployment and testing
|
||||||
|
|
||||||
For a detailed breakdown of specific tasks, refer to TO_DO.txt.
|
For a detailed breakdown of specific tasks, refer to TO_DO.txt.
|
||||||
|
|
||||||
## Progress Status
|
## Progress Status
|
||||||
- **Current Phase**: Pre-development planning
|
- **Current Phase**: Project structure and key files setup
|
||||||
- **Tasks Completed**:
|
- **Tasks Completed**:
|
||||||
- Initial requirements gathering
|
- Updated technology stack to latest versions
|
||||||
- Technology stack definition
|
- Created complete directory structure according to system patterns
|
||||||
- Setup of Perplexity MCP server for research assistance
|
- Implemented core configuration files (Docker, Nginx, Tailwind, Next.js)
|
||||||
- Creation of comprehensive best practices documentation
|
- Created MongoDB initialization scripts with schema validation
|
||||||
- **Tasks In Progress**: Finalizing development approach based on best practices
|
- Implemented Video Player component with transcript display
|
||||||
- **Next Tasks**: Setup development environment, initialize Next.js project, establish database connections
|
- Set up authentication middleware with JWT
|
||||||
|
- Created model files with validation for Videos and Members
|
||||||
|
- Created basic API routes structure
|
||||||
|
- Updated implementation files (planned_implementation.txt, TECH_STACK, TO_DO.txt)
|
||||||
|
- Enhanced database schemas with validation and indexing
|
||||||
|
- Upgraded from WCAG 2.1 AA to WCAG 2.2 AA compliance requirements
|
||||||
|
- Added required video transcript and thumbnail specifications
|
||||||
|
- Implemented Docker setup with security best practices
|
||||||
|
- **Tasks In Progress**: Setting up remaining API routes and controller stubs
|
||||||
|
- **Next Tasks**: Install dependencies, finalize configuration, and begin implementation of Phase 1 core functionality
|
||||||
|
|
||||||
## Current Task
|
## Current Task
|
||||||
Using the best practices documentation to guide the initial project setup and infrastructure. The best_practices.txt file provides detailed guidance on accessibility implementation, architecture patterns, component structure, and development workflow, with specific focus on the needs of deaf and DeafBlind users.
|
Completing the creation of key files and preparing to install dependencies to resolve TypeScript errors. The next step will be to implement Phase 1 core functionality, focusing on the basic public-facing pages and admin authentication system.
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,82 @@
|
||||||
## How the System is Built
|
## How the System is Built
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
- **Frontend**: Next.js with React for server-side rendered pages
|
- **Frontend**:
|
||||||
- **Backend**: Node.js with Express for API endpoints
|
- Next.js 15.2.3 with React 19 for server-side rendered pages
|
||||||
- **Database Layer**: MongoDB for document storage, Redis for caching
|
- Server Components for critical content delivery
|
||||||
- **Containerization**: Docker with Portainer for deployment
|
- Client Components for interactive elements
|
||||||
- **Web Server**: Nginx as reverse proxy
|
- **Backend**:
|
||||||
|
- Node.js with Express 4.21 for RESTful API endpoints
|
||||||
|
- JWT authentication with role-based access control (RBAC)
|
||||||
|
- **Database Layer**:
|
||||||
|
- MongoDB 7.0 with schema validation and indexing
|
||||||
|
- Redis for caching, session management, and rate limiting
|
||||||
|
- **Containerization**:
|
||||||
|
- Docker with security best practices (multi-stage builds, non-root users)
|
||||||
|
- Portainer for container management and monitoring
|
||||||
|
- **Web Server**:
|
||||||
|
- Nginx as reverse proxy with security headers and TLS 1.3
|
||||||
|
|
||||||
### Directory Structure
|
### Directory Structure
|
||||||
The project appears to be in initial planning stages. A full directory structure will be established as development progresses.
|
```
|
||||||
|
/
|
||||||
|
├── frontend/ # Next.js frontend application
|
||||||
|
│ ├── public/ # Static assets
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── app/ # Next.js App Router pages
|
||||||
|
│ │ ├── components/ # Reusable React components
|
||||||
|
│ │ │ ├── atoms/ # Basic UI components
|
||||||
|
│ │ │ ├── molecules/ # Compound components
|
||||||
|
│ │ │ ├── organisms/ # Complex components
|
||||||
|
│ │ │ ├── templates/ # Page layouts
|
||||||
|
│ │ │ └── video/ # Video player components
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ ├── lib/ # Utility functions
|
||||||
|
│ │ ├── styles/ # Global styles and Tailwind config
|
||||||
|
│ │ └── types/ # TypeScript type definitions
|
||||||
|
│ ├── Dockerfile # Multi-stage build for frontend
|
||||||
|
│ └── package.json # Frontend dependencies
|
||||||
|
├── backend/ # Express backend application
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── controllers/ # Request handlers
|
||||||
|
│ │ ├── middleware/ # Express middleware
|
||||||
|
│ │ ├── models/ # MongoDB schema models
|
||||||
|
│ │ ├── routes/ # API route definitions
|
||||||
|
│ │ ├── services/ # Business logic
|
||||||
|
│ │ └── utils/ # Utility functions
|
||||||
|
│ ├── Dockerfile # Backend container definition
|
||||||
|
│ └── package.json # Backend dependencies
|
||||||
|
├── mongo-init/ # MongoDB initialization scripts
|
||||||
|
├── nginx/ # Nginx configuration
|
||||||
|
│ ├── conf/ # Server configurations
|
||||||
|
│ └── ssl/ # SSL certificates
|
||||||
|
├── docker-compose.yml # Docker services definition
|
||||||
|
└── security-reports/ # Security scanning outputs
|
||||||
|
```
|
||||||
|
|
||||||
## Key Technical Decisions
|
## Key Technical Decisions
|
||||||
- Server-side rendering with Next.js for improved SEO and performance
|
- Server Components for critical content delivery and improved SEO
|
||||||
- TypeScript for type safety throughout the stack
|
- TypeScript 5.8 for type safety and incremental build performance
|
||||||
- Tailwind CSS for responsive design
|
- Tailwind CSS 4.0 with CSS-first configuration
|
||||||
- MongoDB for flexible document storage of club data
|
- MongoDB 7.0 with schema validation for data integrity
|
||||||
- Redis for caching and rate limiting
|
- Redis for caching, rate limiting, and session management
|
||||||
- Docker containerization for consistent deployment
|
- Docker containerization with security best practices
|
||||||
- Custom video player implementation for ASL content
|
- Custom video player implementation for ASL content with required transcripts
|
||||||
|
- Multi-stage Docker builds for smaller, more secure production images
|
||||||
|
- JWT-based authentication with role-based access control
|
||||||
|
- Full-text search capabilities for video transcripts
|
||||||
|
|
||||||
## Architecture Patterns
|
## Architecture Patterns
|
||||||
- REST API for data operations
|
- RESTful API with comprehensive endpoint structure
|
||||||
- Context-based state management in React
|
- Context-based state management in React
|
||||||
- Server-side rendering for critical pages
|
- Server-side rendering for critical pages, static generation for stable content
|
||||||
- Responsive design with mobile-first approach
|
- Responsive design with mobile-first approach
|
||||||
- Component-based UI architecture
|
- Atomic design pattern for component organization
|
||||||
- Authentication for admin dashboard
|
- Authentication with JWT and HTTP-only cookies
|
||||||
- Automated notifications for membership status
|
- Automated notifications for membership status
|
||||||
- Accessibility-first development approach
|
- Schema validation and indexing for database performance
|
||||||
|
- Least-privilege principle in containerization
|
||||||
|
- Accessibility-first development approach with WCAG 2.2 AA compliance
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
Development is organized into phases:
|
Development is organized into phases:
|
||||||
|
|
@ -39,4 +88,4 @@ Development is organized into phases:
|
||||||
4. Accessibility & Refinement
|
4. Accessibility & Refinement
|
||||||
5. Testing & Deployment
|
5. Testing & Deployment
|
||||||
|
|
||||||
Each phase has specific tasks and goals as outlined in the TO_DO.txt document.
|
Each phase has specific tasks and goals as outlined in the TO_DO.txt document, with a focus on iterative development and continuous testing for accessibility compliance.
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,41 @@
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- React 19
|
- React 19
|
||||||
- TypeScript 5.7
|
- TypeScript 5.8 (5x faster incremental builds)
|
||||||
- Next.js 14.1
|
- Next.js 15.2.3 (includes security patches for CVE-2025-29927)
|
||||||
- Tailwind CSS 3.4
|
- Tailwind CSS 4.0 (CSS-first configuration with @theme directives, 182x faster builds)
|
||||||
- Framer Motion 11
|
- Framer Motion 11
|
||||||
- React Router DOM 7
|
- React Router DOM 7
|
||||||
- React Icons 5.4
|
- React Icons 5.4
|
||||||
- React Simple Maps 4.0 (beta)
|
- React Simple Maps 4.0 (beta)
|
||||||
- React Intersection Observer 9.13
|
- React Intersection Observer 9.13
|
||||||
|
- Server Components for critical content delivery
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- Node.js with Express 4.21
|
- Node.js with Express 4.21
|
||||||
- TypeScript
|
- TypeScript 5.8
|
||||||
- MongoDB for document storage
|
- MongoDB 7.0 with schema validation and indexing
|
||||||
- Redis for caching and session management
|
- Redis for caching and session management
|
||||||
- Nodemailer 6.9 for email handling
|
- Nodemailer 6.9 for email handling
|
||||||
- CORS for cross-origin resource sharing
|
- CORS with proper security headers
|
||||||
- dotenv 16.4 for environment variable management
|
- dotenv 16.4 for environment variable management
|
||||||
|
- JWT-based authentication with RBAC
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- MongoDB 7.0 with:
|
||||||
|
- Schema validation using $jsonSchema
|
||||||
|
- Field-level validation and pattern matching
|
||||||
|
- Full-text search indexes
|
||||||
|
- Compound indexes for query optimization
|
||||||
|
- Authentication and authorization
|
||||||
|
|
||||||
### Development & Build Tools
|
### Development & Build Tools
|
||||||
- Docker & Docker Compose
|
- Docker & Docker Compose with security best practices:
|
||||||
|
- Multi-stage builds
|
||||||
|
- Non-root users for all services
|
||||||
|
- Resource limitations
|
||||||
|
- Health checks for all containers
|
||||||
|
- Security scanning with Trivy
|
||||||
- Portainer for container management
|
- Portainer for container management
|
||||||
- PostCSS 8.4
|
- PostCSS 8.4
|
||||||
- Autoprefixer 10.4
|
- Autoprefixer 10.4
|
||||||
|
|
@ -35,9 +50,10 @@
|
||||||
- Hot module replacement (HMR)
|
- Hot module replacement (HMR)
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
- Nginx (Production server for frontend)
|
- Nginx (Production server for frontend with security headers)
|
||||||
- Node.js server for backend API
|
- Node.js server for backend API
|
||||||
- SSL for secure connections
|
- SSL/TLS 1.3 for secure connections
|
||||||
|
- Network isolation with Docker
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
- Multi-container Docker setup with Docker Compose
|
- Multi-container Docker setup with Docker Compose
|
||||||
|
|
@ -46,13 +62,18 @@
|
||||||
- Environment variable management for sensitive data
|
- Environment variable management for sensitive data
|
||||||
- TypeScript configuration for both client and server
|
- TypeScript configuration for both client and server
|
||||||
- Concurrent development server (frontend + backend)
|
- Concurrent development server (frontend + backend)
|
||||||
|
- Automated security scanning in the build process
|
||||||
|
|
||||||
## Technical Constraints
|
## Technical Constraints
|
||||||
- Emphasis on accessibility for deaf and DeafBlind users
|
- Emphasis on accessibility for deaf and DeafBlind users
|
||||||
- WCAG 2.1 AA compliance required
|
- WCAG 2.2 AA compliance required
|
||||||
- ASL video support with WebVTT subtitles
|
- ASL video support with mandatory:
|
||||||
- Screen reader compatibility
|
- WebVTT subtitles
|
||||||
|
- Full text transcripts
|
||||||
|
- Thumbnail previews
|
||||||
|
- Screen reader compatibility with ARIA landmarks
|
||||||
- High performance requirements for video content
|
- High performance requirements for video content
|
||||||
- Secure admin access restrictions
|
- Secure admin access with JWT and RBAC
|
||||||
- Responsive design for all screen sizes
|
- Responsive design for all screen sizes
|
||||||
- Support for membership expiration notifications
|
- Support for membership expiration notifications
|
||||||
|
- Requirements for Docker security best practices
|
||||||
|
|
|
||||||
156
docker-compose.yml
Normal file
156
docker-compose.yml
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Frontend service
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
user: node
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
env_file: ./frontend/.env
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs:rw
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
|
|
||||||
|
# Backend API service
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
user: node
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
- redis
|
||||||
|
env_file: ./backend/.env
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads:rw
|
||||||
|
- ./logs:/app/logs:rw
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
|
|
||||||
|
# MongoDB service
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7.0
|
||||||
|
restart: unless-stopped
|
||||||
|
user: mongodb
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db:rw
|
||||||
|
- ./mongo-init:/docker-entrypoint-initdb.d:ro
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
env_file: ./backend/.env
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
|
||||||
|
command: ["--auth", "--bind_ip_all", "--tlsMode", "preferTLS"]
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017 --quiet
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '1.0'
|
||||||
|
|
||||||
|
# Redis service
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data:rw
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.2'
|
||||||
|
|
||||||
|
# Nginx service for production
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/conf:/etc/nginx/conf.d:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
- ./frontend/public:/var/www/html:ro
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "https://localhost"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
|
# Security scanner
|
||||||
|
security_scanner:
|
||||||
|
image: aquasec/trivy
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./security-reports:/reports
|
||||||
|
command: ["image", "--format", "table", "--output", "/reports/scan-$(date +%Y%m%d).txt", "olathedeafclub-frontend:latest", "olathedeafclub-backend:latest"]
|
||||||
|
profiles:
|
||||||
|
- security
|
||||||
|
|
||||||
|
# Networks with isolation
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/24
|
||||||
|
|
||||||
|
# Volumes with backup capability
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
name: ocd-mongo-data
|
||||||
|
redis-data:
|
||||||
|
name: ocd-redis-data
|
||||||
45
frontend/Dockerfile
Normal file
45
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# Create app directory with non-root user
|
||||||
|
WORKDIR /app
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
|
||||||
|
chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/.next ./.next
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/public ./public
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/package.json ./package.json
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/next.config.js ./next.config.js
|
||||||
|
|
||||||
|
# Security best practices
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# Set permissions and switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Create health check endpoint
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/health.js ./health.js
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
24
frontend/health.js
Normal file
24
frontend/health.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Health check endpoint for frontend container
|
||||||
|
|
||||||
|
// Import necessary modules
|
||||||
|
import { createServer } from 'http';
|
||||||
|
|
||||||
|
// Create a simple server that responds to health check requests
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
// Only respond to /health path
|
||||||
|
if (req.url === '/health') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen on a separate port to avoid conflicts with the Next.js server
|
||||||
|
server.listen(3001, () => {
|
||||||
|
console.log('Health check server running on port 3001');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for use in Next.js
|
||||||
|
export default server;
|
||||||
38
frontend/next.config.js
Normal file
38
frontend/next.config.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
output: 'standalone',
|
||||||
|
images: {
|
||||||
|
domains: ['localhost'],
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
allowedOrigins: ['localhost:3000', 'olathedeafclub.com'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/(.*)',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'X-Content-Type-Options',
|
||||||
|
value: 'nosniff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'SAMEORIGIN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-XSS-Protection',
|
||||||
|
value: '1; mode=block',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "ocd-website-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "15.2.3",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-next": "^15.2.3",
|
||||||
|
"postcss": "^8.4.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
192
frontend/src/app/globals.css
Normal file
192
frontend/src/app/globals.css
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-rgb: 255, 255, 255;
|
||||||
|
--primary-color: #1a56db;
|
||||||
|
--secondary-color: #7e3af2;
|
||||||
|
--accent-color: #ff9800;
|
||||||
|
--text-color: #171717;
|
||||||
|
--text-light: #6b7280;
|
||||||
|
--focus-ring: 3px solid #3b82f6;
|
||||||
|
--border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 255, 255, 255;
|
||||||
|
--background-rgb: 17, 24, 39;
|
||||||
|
--text-color: #f3f4f6;
|
||||||
|
--text-light: #d1d5db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
background-color: rgb(var(--background-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility Focus Styles */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip to content link */
|
||||||
|
.skip-to-content {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
top: -9999px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-to-content:focus {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 16px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Player Styles */
|
||||||
|
.video-player-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper:hover .controls-container {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper:not(:hover) .controls-container {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playback-rate {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-container {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.transcript-container {
|
||||||
|
background: #1f2937;
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-entry {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-time {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.controls-container {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
frontend/src/app/layout.tsx
Normal file
59
frontend/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Olathe Club of the Deaf',
|
||||||
|
description: 'Community hub for the deaf community in Olathe',
|
||||||
|
metadataBase: new URL('https://olathedeafclub.com'),
|
||||||
|
keywords: ['deaf', 'olathe', 'club', 'community', 'ASL', 'events'],
|
||||||
|
authors: [{ name: 'Olathe Club of the Deaf' }],
|
||||||
|
openGraph: {
|
||||||
|
title: 'Olathe Club of the Deaf',
|
||||||
|
description: 'Community hub for the deaf community in Olathe',
|
||||||
|
url: 'https://olathedeafclub.com',
|
||||||
|
siteName: 'Olathe Club of the Deaf',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/images/og-image.jpg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Olathe Club of the Deaf'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'Olathe Club of the Deaf',
|
||||||
|
description: 'Community hub for the deaf community in Olathe',
|
||||||
|
images: ['/images/og-image.jpg'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
{/* Navigation will go here */}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
{/* Footer content will go here */}
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
frontend/src/app/page.tsx
Normal file
98
frontend/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* Skip to content link for accessibility */}
|
||||||
|
<a href="#main-content" className="skip-to-content">
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Hero section */}
|
||||||
|
<section
|
||||||
|
id="main-content"
|
||||||
|
className="py-16 mb-12 text-center"
|
||||||
|
>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||||
|
Olathe Club of the Deaf
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl max-w-3xl mx-auto mb-8">
|
||||||
|
A community organization serving the deaf and hard-of-hearing community in Olathe since 1975.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
<Link
|
||||||
|
href="/events"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium text-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Upcoming Events
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/membership"
|
||||||
|
className="bg-white hover:bg-gray-100 text-blue-600 border border-blue-600 px-6 py-3 rounded-lg font-medium text-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Join OCD
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Featured events section */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-3xl font-bold mb-8 text-center">Featured Events</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{/* Event cards will be populated here */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div className="h-48 bg-gray-200"></div>
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Monthly Social</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">Join us for our monthly social gathering with games, refreshments, and great conversation.</p>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-blue-600 font-medium">Apr 15, 2025</span>
|
||||||
|
<Link href="/events/monthly-social" className="text-blue-600 hover:underline">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* About section with video */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-3xl font-bold mb-8 text-center">About OCD</h2>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-lg mb-4">
|
||||||
|
Olathe Club of the Deaf (OCD) is a non-profit organization dedicated to serving the deaf and hard-of-hearing community in Olathe and surrounding areas.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg mb-4">
|
||||||
|
Our mission is to promote social, educational, and recreational opportunities for deaf individuals and their families while fostering a sense of community and belonging.
|
||||||
|
</p>
|
||||||
|
<Link href="/about" className="text-blue-600 hover:underline font-medium">
|
||||||
|
Learn more about our history
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-200 rounded-lg h-64 flex items-center justify-center">
|
||||||
|
<p className="text-gray-600">Video introduction will go here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Call to action */}
|
||||||
|
<section className="bg-blue-600 text-white py-12 px-6 rounded-lg text-center">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Join Our Community</h2>
|
||||||
|
<p className="text-xl max-w-3xl mx-auto mb-8">
|
||||||
|
Become a member today and connect with the deaf community in Olathe.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/membership"
|
||||||
|
className="bg-white text-blue-600 hover:bg-gray-100 px-6 py-3 rounded-lg font-medium text-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Become a Member
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
frontend/src/components/atoms/Icons.tsx
Normal file
111
frontend/src/components/atoms/Icons.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IconProps {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlayIcon: React.FC<IconProps> = ({
|
||||||
|
className = '',
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
className={`icon play-icon ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polygon points="5 3 19 12 5 21 5 3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PauseIcon: React.FC<IconProps> = ({
|
||||||
|
className = '',
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
className={`icon pause-icon ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="6" y="4" width="4" height="16" />
|
||||||
|
<rect x="14" y="4" width="4" height="16" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullscreenIcon: React.FC<IconProps> = ({
|
||||||
|
className = '',
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
className={`icon fullscreen-icon ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 3H5a2 2 0 0 0-2 2v3" />
|
||||||
|
<path d="M21 8V5a2 2 0 0 0-2-2h-3" />
|
||||||
|
<path d="M3 16v3a2 2 0 0 0 2 2h3" />
|
||||||
|
<path d="M16 21h3a2 2 0 0 0 2-2v-3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VolumeIcon: React.FC<IconProps> = ({
|
||||||
|
className = '',
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
className={`icon volume-icon ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<path d="M15 8a5 5 0 0 1 0 8" />
|
||||||
|
<path d="M19 5a9 9 0 0 1 0 14" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
238
frontend/src/components/video/VideoPlayer.tsx
Normal file
238
frontend/src/components/video/VideoPlayer.tsx
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { PlayIcon, PauseIcon } from 'frontend/src/components/atoms/Icons';
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
|
videoUrl: string;
|
||||||
|
subtitleUrl?: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
title: string;
|
||||||
|
transcriptText: string;
|
||||||
|
transcriptFormat?: 'plain' | 'html' | 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||||
|
videoUrl,
|
||||||
|
subtitleUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
title,
|
||||||
|
transcriptText,
|
||||||
|
transcriptFormat = 'plain',
|
||||||
|
}) => {
|
||||||
|
// State for video player
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [showSubtitles, setShowSubtitles] = useState(true);
|
||||||
|
const [playbackRate, setPlaybackRate] = useState(1);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [showTranscript, setShowTranscript] = useState(false);
|
||||||
|
|
||||||
|
// Initialize video
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
setCurrentTime(video.currentTime);
|
||||||
|
setProgress((video.currentTime / video.duration) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
setDuration(video.duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Play/pause toggle
|
||||||
|
const togglePlayPause = () => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
video.pause();
|
||||||
|
} else {
|
||||||
|
video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seek to position
|
||||||
|
const seekToPosition = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
const progressBar = e.currentTarget;
|
||||||
|
const clickPosition = (e.clientX - progressBar.getBoundingClientRect().left) / progressBar.offsetWidth;
|
||||||
|
|
||||||
|
video.currentTime = clickPosition * video.duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change playback rate
|
||||||
|
const handlePlaybackRateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
const newRate = parseFloat(e.target.value);
|
||||||
|
video.playbackRate = newRate;
|
||||||
|
setPlaybackRate(newRate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle subtitles
|
||||||
|
const toggleSubtitles = () => {
|
||||||
|
setShowSubtitles(!showSubtitles);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format time (seconds to MM:SS)
|
||||||
|
const formatTime = (timeInSeconds: number) => {
|
||||||
|
const minutes = Math.floor(timeInSeconds / 60);
|
||||||
|
const seconds = Math.floor(timeInSeconds % 60);
|
||||||
|
return `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle transcript display
|
||||||
|
const toggleTranscript = () => {
|
||||||
|
setShowTranscript(!showTranscript);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render transcript based on format
|
||||||
|
const renderTranscript = () => {
|
||||||
|
if (!showTranscript) return null;
|
||||||
|
|
||||||
|
if (transcriptFormat === 'html') {
|
||||||
|
return <div
|
||||||
|
className="transcript-container"
|
||||||
|
dangerouslySetInnerHTML={{ __html: transcriptText }}
|
||||||
|
/>;
|
||||||
|
} else if (transcriptFormat === 'json') {
|
||||||
|
try {
|
||||||
|
const parsedTranscript = JSON.parse(transcriptText);
|
||||||
|
return (
|
||||||
|
<div className="transcript-container">
|
||||||
|
{parsedTranscript.map((item: any, index: number) => (
|
||||||
|
<p key={index} className="transcript-entry">
|
||||||
|
<span className="transcript-time">{item.time}</span>
|
||||||
|
<span className="transcript-text">{item.text}</span>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return <div className="transcript-container">{transcriptText}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default plain text format
|
||||||
|
return <div className="transcript-container">{transcriptText}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="video-player-container" aria-label={`Video player: ${title}`}>
|
||||||
|
<div className="video-wrapper">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={videoUrl}
|
||||||
|
poster={thumbnailUrl}
|
||||||
|
className="video-element"
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
{subtitleUrl && showSubtitles && (
|
||||||
|
<track
|
||||||
|
kind="subtitles"
|
||||||
|
src={subtitleUrl}
|
||||||
|
srcLang="en"
|
||||||
|
label="English"
|
||||||
|
default
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
{/* Custom control bar */}
|
||||||
|
<div className="controls-container">
|
||||||
|
<button
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
aria-label={isPlaying ? "Pause" : "Play"}
|
||||||
|
className="control-button play-button"
|
||||||
|
>
|
||||||
|
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Time display */}
|
||||||
|
<div className="time-display">
|
||||||
|
<span>{formatTime(currentTime)}</span>
|
||||||
|
<span> / </span>
|
||||||
|
<span>{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div
|
||||||
|
className="progress-container"
|
||||||
|
onClick={seekToPosition}
|
||||||
|
role="slider"
|
||||||
|
aria-label="Video progress"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-valuenow={progress}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitles toggle */}
|
||||||
|
<button
|
||||||
|
onClick={toggleSubtitles}
|
||||||
|
aria-label={showSubtitles ? "Disable subtitles" : "Enable subtitles"}
|
||||||
|
className={`control-button subtitle-button ${showSubtitles ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
CC
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Transcript toggle */}
|
||||||
|
<button
|
||||||
|
onClick={toggleTranscript}
|
||||||
|
aria-label={showTranscript ? "Hide transcript" : "Show transcript"}
|
||||||
|
className={`control-button transcript-button ${showTranscript ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
Transcript
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Playback rate selector */}
|
||||||
|
<select
|
||||||
|
value={playbackRate}
|
||||||
|
onChange={handlePlaybackRateChange}
|
||||||
|
aria-label="Playback speed"
|
||||||
|
className="playback-rate"
|
||||||
|
>
|
||||||
|
<option value="0.5">0.5x</option>
|
||||||
|
<option value="1">1x</option>
|
||||||
|
<option value="1.5">1.5x</option>
|
||||||
|
<option value="2">2x</option>
|
||||||
|
<option value="3">3x</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transcript display */}
|
||||||
|
{renderTranscript()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoPlayer;
|
||||||
92
frontend/tailwind.config.js
Normal file
92
frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#1a56db',
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: '#7e3af2',
|
||||||
|
50: '#f5f3ff',
|
||||||
|
100: '#ede9fe',
|
||||||
|
200: '#ddd6fe',
|
||||||
|
300: '#c4b5fd',
|
||||||
|
400: '#a78bfa',
|
||||||
|
500: '#8b5cf6',
|
||||||
|
600: '#7c3aed',
|
||||||
|
700: '#6d28d9',
|
||||||
|
800: '#5b21b6',
|
||||||
|
900: '#4c1d95',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: '#ff9800',
|
||||||
|
50: '#fff8e1',
|
||||||
|
100: '#ffecb3',
|
||||||
|
200: '#ffe082',
|
||||||
|
300: '#ffd54f',
|
||||||
|
400: '#ffca28',
|
||||||
|
500: '#ffc107',
|
||||||
|
600: '#ffb300',
|
||||||
|
700: '#ffa000',
|
||||||
|
800: '#ff8f00',
|
||||||
|
900: '#ff6f00',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'xl': '0.75rem',
|
||||||
|
'2xl': '1rem',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'focus': '0 0 0 3px rgba(59, 130, 246, 0.5)',
|
||||||
|
},
|
||||||
|
typography: (theme) => ({
|
||||||
|
DEFAULT: {
|
||||||
|
css: {
|
||||||
|
a: {
|
||||||
|
color: theme('colors.primary.DEFAULT'),
|
||||||
|
'&:hover': {
|
||||||
|
color: theme('colors.primary.700'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
|
],
|
||||||
|
darkMode: 'media', // 'media' or 'class'
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
217
mongo-init/01-init.js
Normal file
217
mongo-init/01-init.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
// MongoDB initialization script - runs when container starts
|
||||||
|
|
||||||
|
// Create database
|
||||||
|
db = db.getSiblingDB('ocd_db');
|
||||||
|
|
||||||
|
// Create admin user if it doesn't exist
|
||||||
|
if (db.getUser('admin') == null) {
|
||||||
|
db.createUser({
|
||||||
|
user: 'admin',
|
||||||
|
pwd: process.env.MONGO_ADMIN_PASSWORD || 'admin',
|
||||||
|
roles: [{ role: 'readWrite', db: 'ocd_db' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create video collection with validation
|
||||||
|
db.createCollection('videos', {
|
||||||
|
validator: {
|
||||||
|
$jsonSchema: {
|
||||||
|
bsonType: 'object',
|
||||||
|
required: ['title', 'fileUrl', 'thumbnailUrl', 'transcriptText'],
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
bsonType: 'string',
|
||||||
|
minLength: 3,
|
||||||
|
description: 'Title is required and must be at least 3 characters'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Description of the video'
|
||||||
|
},
|
||||||
|
fileUrl: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'URL to the video file (required)'
|
||||||
|
},
|
||||||
|
thumbnailUrl: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'URL to the thumbnail image (required)'
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
bsonType: 'number',
|
||||||
|
description: 'Duration of the video in seconds'
|
||||||
|
},
|
||||||
|
uploadDate: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Date when the video was uploaded'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Category of the video'
|
||||||
|
},
|
||||||
|
subtitleUrl: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'URL to the WebVTT subtitle file'
|
||||||
|
},
|
||||||
|
transcriptText: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Full text transcript of the video (required)'
|
||||||
|
},
|
||||||
|
transcriptFormat: {
|
||||||
|
enum: ['plain', 'html', 'json'],
|
||||||
|
description: 'Format of the transcript (plain, html, or json)'
|
||||||
|
},
|
||||||
|
isPublished: {
|
||||||
|
bsonType: 'bool',
|
||||||
|
description: 'Whether the video is published or not'
|
||||||
|
},
|
||||||
|
relatedPageId: {
|
||||||
|
bsonType: 'objectId',
|
||||||
|
description: 'Reference to a related page'
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
bsonType: 'array',
|
||||||
|
items: { bsonType: 'string' },
|
||||||
|
description: 'Tags for categorizing the video'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationLevel: 'strict'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for video collection
|
||||||
|
db.videos.createIndex({ 'title': 'text', 'transcriptText': 'text' });
|
||||||
|
db.videos.createIndex({ 'category': 1, 'uploadDate': -1 });
|
||||||
|
db.videos.createIndex({ 'tags': 1 });
|
||||||
|
|
||||||
|
// Create members collection with validation
|
||||||
|
db.createCollection('members', {
|
||||||
|
validator: {
|
||||||
|
$jsonSchema: {
|
||||||
|
bsonType: 'object',
|
||||||
|
required: ['firstName', 'lastName', 'email', 'membershipType', 'joinDate', 'status'],
|
||||||
|
properties: {
|
||||||
|
firstName: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'First name is required'
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Last name is required'
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
bsonType: 'string',
|
||||||
|
pattern: '^.+@.+\\..+$',
|
||||||
|
description: 'Email must be a valid email address'
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Phone number'
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
bsonType: 'object',
|
||||||
|
properties: {
|
||||||
|
street: { bsonType: 'string' },
|
||||||
|
city: { bsonType: 'string' },
|
||||||
|
state: { bsonType: 'string' },
|
||||||
|
zip: { bsonType: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
membershipType: {
|
||||||
|
enum: ['regular', 'lifetime', 'honorary'],
|
||||||
|
description: 'Membership type must be one of: regular, lifetime, honorary'
|
||||||
|
},
|
||||||
|
joinDate: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Date when the member joined'
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Date when the membership expires'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
enum: ['active', 'expired', 'pending'],
|
||||||
|
description: 'Status must be one of: active, expired, pending'
|
||||||
|
},
|
||||||
|
notificationPreference: {
|
||||||
|
enum: ['email', 'sms', 'both'],
|
||||||
|
description: 'Notification preference must be one of: email, sms, both'
|
||||||
|
},
|
||||||
|
lastRenewalDate: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Date of the last membership renewal'
|
||||||
|
},
|
||||||
|
boardMember: {
|
||||||
|
bsonType: 'bool',
|
||||||
|
description: 'Whether the member is part of the board'
|
||||||
|
},
|
||||||
|
boardPosition: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Position on the board, if applicable'
|
||||||
|
},
|
||||||
|
emergencyContact: {
|
||||||
|
bsonType: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { bsonType: 'string' },
|
||||||
|
relationship: { bsonType: 'string' },
|
||||||
|
phone: { bsonType: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationLevel: 'strict'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for members collection
|
||||||
|
db.members.createIndex({ 'email': 1 }, { unique: true });
|
||||||
|
db.members.createIndex({ 'lastName': 1, 'firstName': 1 });
|
||||||
|
db.members.createIndex({ 'status': 1, 'expirationDate': 1 });
|
||||||
|
db.members.createIndex({ 'boardMember': 1 });
|
||||||
|
|
||||||
|
// Create admin user collection with validation
|
||||||
|
db.createCollection('adminUsers', {
|
||||||
|
validator: {
|
||||||
|
$jsonSchema: {
|
||||||
|
bsonType: 'object',
|
||||||
|
required: ['username', 'passwordHash', 'email'],
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
bsonType: 'string',
|
||||||
|
minLength: 3,
|
||||||
|
maxLength: 50,
|
||||||
|
description: 'Username is required and must be between 3 and 50 characters'
|
||||||
|
},
|
||||||
|
passwordHash: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Password hash is required'
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
bsonType: 'string',
|
||||||
|
pattern: '^.+@.+\\..+$',
|
||||||
|
description: 'Email must be a valid email address'
|
||||||
|
},
|
||||||
|
lastLogin: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Date of the last login'
|
||||||
|
},
|
||||||
|
resetToken: {
|
||||||
|
bsonType: 'string',
|
||||||
|
description: 'Token for password reset'
|
||||||
|
},
|
||||||
|
resetTokenExpiry: {
|
||||||
|
bsonType: 'date',
|
||||||
|
description: 'Expiry date for password reset token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationLevel: 'strict'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for admin users collection
|
||||||
|
db.adminUsers.createIndex({ 'username': 1 }, { unique: true });
|
||||||
|
db.adminUsers.createIndex({ 'email': 1 }, { unique: true });
|
||||||
|
db.adminUsers.createIndex({ 'resetToken': 1 }, { sparse: true });
|
||||||
|
|
||||||
|
console.log('MongoDB initialization completed');
|
||||||
88
nginx/conf/default.conf
Normal file
88
nginx/conf/default.conf
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Nginx configuration for OCD Website
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name olathedeafclub.com www.olathedeafclub.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name olathedeafclub.com www.olathedeafclub.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/olathedeafclub.com.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/olathedeafclub.com.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://zeffy.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.olathedeafclub.com; media-src 'self'; frame-src https://zeffy.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';" always;
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
# Frontend static assets
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend API
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://backend:4000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# CORS preflight requests
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' 'https://olathedeafclub.com';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
location /static {
|
||||||
|
alias /var/www/html;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
location /uploads {
|
||||||
|
alias /app/uploads;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
|
||||||
|
# Limit access to video files by referrer
|
||||||
|
location ~* \.(mp4|webm)$ {
|
||||||
|
valid_referers server_names;
|
||||||
|
if ($invalid_referer) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error pages
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
}
|
||||||
|
|
@ -7,16 +7,16 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
|
||||||
## Technical Architecture
|
## Technical Architecture
|
||||||
|
|
||||||
### Frontend Architecture
|
### Frontend Architecture
|
||||||
- **Framework**: Next.js 14.1 with React 19
|
- **Framework**: Next.js 15.2.3 with React 19
|
||||||
- **Rendering Strategy**:
|
- **Rendering Strategy**:
|
||||||
- Server-side rendering for critical pages (home, about, events)
|
- Server-side rendering for critical pages (home, about, events)
|
||||||
- Static generation for stable content (bylaws, history)
|
- Static generation for stable content (bylaws, history)
|
||||||
- Client-side rendering for interactive components (calendar, forms)
|
- Client-side rendering for interactive components (calendar, forms)
|
||||||
- **State Management**: React Context for application state
|
- **State Management**: React Context for application state
|
||||||
- **Styling**:
|
- **Styling**:
|
||||||
- Tailwind CSS 3.4 for responsive design
|
- Tailwind CSS 4.0 for responsive design with CSS-first configuration
|
||||||
- PostCSS 8.4 with Autoprefixer 10.4
|
- PostCSS 8.4 with Autoprefixer 10.4
|
||||||
- Custom CSS variables for theming
|
- Native CSS variables for theme colors and 3D transforms
|
||||||
- **Animation**: Framer Motion 11 for UI transitions
|
- **Animation**: Framer Motion 11 for UI transitions
|
||||||
- **Routing**: Next.js App Router with nested layouts
|
- **Routing**: Next.js App Router with nested layouts
|
||||||
- **Interactive Components**:
|
- **Interactive Components**:
|
||||||
|
|
@ -40,18 +40,35 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
|
||||||
- Response compression
|
- Response compression
|
||||||
- Efficient database queries with indexing
|
- Efficient database queries with indexing
|
||||||
|
|
||||||
### Database Schema
|
### Database Schema with Validation
|
||||||
1. **Admin User**
|
1. **Admin User**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
db.createCollection("adminUsers", {
|
||||||
_id: ObjectId,
|
validator: {
|
||||||
username: String,
|
$jsonSchema: {
|
||||||
passwordHash: String,
|
bsonType: "object",
|
||||||
email: String,
|
required: ["username", "passwordHash", "email"],
|
||||||
lastLogin: Date,
|
properties: {
|
||||||
resetToken: String,
|
_id: { bsonType: "objectId" },
|
||||||
resetTokenExpiry: Date
|
username: { bsonType: "string", minLength: 3, maxLength: 50 },
|
||||||
}
|
passwordHash: { bsonType: "string" },
|
||||||
|
email: {
|
||||||
|
bsonType: "string",
|
||||||
|
pattern: "^.+@.+\\..+$"
|
||||||
|
},
|
||||||
|
lastLogin: { bsonType: "date" },
|
||||||
|
resetToken: { bsonType: "string" },
|
||||||
|
resetTokenExpiry: { bsonType: "date" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationLevel: "strict"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for security and performance
|
||||||
|
db.adminUsers.createIndex({ "username": 1 }, { unique: true });
|
||||||
|
db.adminUsers.createIndex({ "email": 1 }, { unique: true });
|
||||||
|
db.adminUsers.createIndex({ "resetToken": 1 }, { sparse: true });
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Member**
|
2. **Member**
|
||||||
|
|
@ -154,20 +171,39 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
|
||||||
|
|
||||||
6. **Video**
|
6. **Video**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
db.createCollection("videos", {
|
||||||
_id: ObjectId,
|
validator: {
|
||||||
title: String,
|
$jsonSchema: {
|
||||||
description: String,
|
bsonType: "object",
|
||||||
fileUrl: String,
|
required: ["title", "fileUrl", "thumbnailUrl", "transcriptText"],
|
||||||
thumbnailUrl: String,
|
properties: {
|
||||||
duration: Number, // in seconds
|
_id: { bsonType: "objectId" },
|
||||||
uploadDate: Date,
|
title: { bsonType: "string", minLength: 3 },
|
||||||
category: String,
|
description: { bsonType: "string" },
|
||||||
subtitleUrl: String, // WebVTT file URL
|
fileUrl: { bsonType: "string" },
|
||||||
transcriptText: String,
|
thumbnailUrl: { bsonType: "string" }, // Required thumbnail preview image
|
||||||
isPublished: Boolean,
|
duration: { bsonType: "number" }, // in seconds
|
||||||
relatedPageId: ObjectId // Reference to Page Content
|
uploadDate: { bsonType: "date" },
|
||||||
}
|
category: { bsonType: "string" },
|
||||||
|
subtitleUrl: { bsonType: "string" }, // WebVTT file URL
|
||||||
|
transcriptText: { bsonType: "string" }, // Required full text transcript
|
||||||
|
transcriptFormat: { bsonType: "string", enum: ["plain", "html", "json"] },
|
||||||
|
isPublished: { bsonType: "bool" },
|
||||||
|
relatedPageId: { bsonType: "objectId" }, // Reference to Page Content
|
||||||
|
tags: {
|
||||||
|
bsonType: "array",
|
||||||
|
items: { bsonType: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationLevel: "strict"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create indexes for performance
|
||||||
|
db.videos.createIndex({ "title": "text", "transcriptText": "text" }); // Full-text search
|
||||||
|
db.videos.createIndex({ "category": 1, "uploadDate": -1 }); // Category browsing
|
||||||
|
db.videos.createIndex({ "tags": 1 }); // Tag filtering
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Contact Submission**
|
7. **Contact Submission**
|
||||||
|
|
@ -425,22 +461,27 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
|
||||||
- Component-based media queries using Tailwind's screen variants
|
- Component-based media queries using Tailwind's screen variants
|
||||||
|
|
||||||
### Accessibility Implementation
|
### Accessibility Implementation
|
||||||
- WCAG 2.1 AA compliance requirements:
|
- WCAG 2.2 AA compliance requirements:
|
||||||
- Semantic HTML structure
|
- Semantic HTML structure
|
||||||
- Keyboard navigation support
|
- Keyboard navigation support
|
||||||
- Focus management for interactive elements
|
- Focus Not Obscured (2.4.11) - ensure focus indicators are clearly visible
|
||||||
|
- Dragging Movements (2.5.7) - provide alternatives for drag operations
|
||||||
|
- Accessible Authentication (3.3.7/3.3.8) - offer alternatives to cognitive tests
|
||||||
- Color contrast ratio ≥ 4.5:1
|
- Color contrast ratio ≥ 4.5:1
|
||||||
- Text resizing support without breaking layouts
|
- Text resizing support without breaking layouts
|
||||||
|
- Target Size (2.5.8) - ensure touch targets are at least 24px × 24px
|
||||||
- Screen reader considerations:
|
- Screen reader considerations:
|
||||||
- ARIA landmarks and labels
|
- ARIA landmarks and labels
|
||||||
- Skip navigation links
|
- Skip navigation links
|
||||||
- Descriptive alt text for images
|
- Descriptive alt text for images
|
||||||
- Status announcements for dynamic content
|
- Status announcements with aria-live regions for dynamic content
|
||||||
|
- Mobile ARIA optimization
|
||||||
- DeafBlind user support:
|
- DeafBlind user support:
|
||||||
- High contrast mode
|
- High contrast mode with adequate contrast ratios
|
||||||
- Text-only content alternatives
|
- Text-only content alternatives
|
||||||
- Simple navigation patterns
|
- Simple navigation patterns
|
||||||
- Reduced motion option
|
- Reduced motion option
|
||||||
|
- Refreshable braille display compatibility with proper ARIA landmarks
|
||||||
|
|
||||||
### Video Player Implementation
|
### Video Player Implementation
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -674,27 +715,38 @@ export default Calendar;
|
||||||
|
|
||||||
## Deployment Architecture
|
## Deployment Architecture
|
||||||
|
|
||||||
### Docker Compose Setup
|
### Docker Compose Setup with Security Best Practices
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Frontend service
|
# Frontend service with multi-stage build (Dockerfile below)
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
user: node
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
env_file: ./frontend/.env
|
env_file: ./frontend/.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs:rw
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
|
|
||||||
# Backend API service
|
# Backend API service
|
||||||
backend:
|
backend:
|
||||||
|
|
@ -702,6 +754,7 @@ services:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
user: node
|
||||||
ports:
|
ports:
|
||||||
- "4000:4000"
|
- "4000:4000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -709,29 +762,66 @@ services:
|
||||||
- redis
|
- redis
|
||||||
env_file: ./backend/.env
|
env_file: ./backend/.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads:rw
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs:rw
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
|
|
||||||
# MongoDB service
|
# MongoDB service
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:7.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
user: mongodb
|
||||||
volumes:
|
volumes:
|
||||||
- mongo-data:/data/db
|
- mongo-data:/data/db:rw
|
||||||
|
- ./mongo-init:/docker-entrypoint-initdb.d:ro
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
env_file: ./backend/.env
|
env_file: ./backend/.env
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
|
||||||
|
command: ["--auth", "--bind_ip_all", "--tlsMode", "preferTLS"]
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017 --quiet
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '1.0'
|
||||||
|
|
||||||
# Redis service
|
# Redis service
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
- redis-data:/data:rw
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.2'
|
||||||
|
|
||||||
# Nginx service for production
|
# Nginx service for production
|
||||||
nginx:
|
nginx:
|
||||||
|
|
@ -741,24 +831,98 @@ services:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/conf:/etc/nginx/conf.d
|
- ./nginx/conf:/etc/nginx/conf.d:ro
|
||||||
- ./nginx/ssl:/etc/nginx/ssl
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
- ./frontend/public:/var/www/html
|
- ./frontend/public:/var/www/html:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- frontend
|
- frontend
|
||||||
- backend
|
- backend
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "https://localhost"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
# Networks
|
# Security scanner
|
||||||
|
security_scanner:
|
||||||
|
image: aquasec/trivy
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./security-reports:/reports
|
||||||
|
command: ["image", "--format", "table", "--output", "/reports/scan-$(date +%Y%m%d).txt", "olathedeafclub-frontend:latest", "olathedeafclub-backend:latest"]
|
||||||
|
profiles:
|
||||||
|
- security
|
||||||
|
|
||||||
|
# Networks with isolation
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/24
|
||||||
|
|
||||||
# Volumes
|
# Volumes with backup capability
|
||||||
volumes:
|
volumes:
|
||||||
mongo-data:
|
mongo-data:
|
||||||
|
name: ocd-mongo-data
|
||||||
redis-data:
|
redis-data:
|
||||||
|
name: ocd-redis-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-stage Dockerfile for Frontend
|
||||||
|
```Dockerfile
|
||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# Create app directory with non-root user
|
||||||
|
WORKDIR /app
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
|
||||||
|
chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/.next ./.next
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/public ./public
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/package.json ./package.json
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/next.config.js ./next.config.js
|
||||||
|
|
||||||
|
# Security best practices
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# Set permissions and switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Create health check endpoint
|
||||||
|
COPY --from=build --chown=appuser:appgroup /app/health.js ./health.js
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nginx Configuration for Production
|
### Nginx Configuration for Production
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue