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
|
||||
- React 19
|
||||
- TypeScript 5.7
|
||||
- TypeScript 5.8 (5x faster incremental builds)
|
||||
- React Router DOM 7
|
||||
- Tailwind CSS 3.4
|
||||
- Tailwind CSS 4.0 (CSS-first configuration with @theme directives, 182x faster builds)
|
||||
- Framer Motion 11
|
||||
- React Icons 5.4
|
||||
- React Simple Maps 4.0 (beta)
|
||||
- React Intersection Observer 9.13
|
||||
- Next.js 14.1
|
||||
- Next.js 15.2.3 (includes security patches for CVE-2025-29927)
|
||||
|
||||
### Backend
|
||||
- Node.js with Express 4.21
|
||||
- TypeScript
|
||||
- TypeScript 5.8
|
||||
- Nodemailer 6.9 for email handling
|
||||
- CORS for cross-origin resource sharing
|
||||
- dotenv 16.4 for environment variable management
|
||||
- Upstash Redis 1.28 for caching/rate limiting
|
||||
- JWT authentication with role-based access control
|
||||
- MongoDB 7.0 with schema validation
|
||||
|
||||
### 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
|
||||
- Autoprefixer 10.4
|
||||
- pnpm 9.14 (package manager)
|
||||
|
|
@ -31,8 +37,9 @@
|
|||
- Hot module replacement (HMR)
|
||||
|
||||
### Production
|
||||
- Nginx (Production server for frontend)
|
||||
- Nginx (Production server for frontend with security headers)
|
||||
- Node.js server for backend API
|
||||
- SSL/TLS 1.3 configuration
|
||||
|
||||
### Architecture
|
||||
- Frontend running on port 3000 (development)
|
||||
|
|
@ -43,9 +50,10 @@
|
|||
- Rate limiting implementation
|
||||
- Context-based state management
|
||||
- Redis-backed caching layer
|
||||
- MongoDB schema validation using $jsonSchema
|
||||
|
||||
### Key Features
|
||||
- Responsive design
|
||||
- Responsive design (mobile-first approach)
|
||||
- Animated UI components with Framer Motion
|
||||
- Interactive USA region map
|
||||
- Contact form with email notifications
|
||||
|
|
@ -53,11 +61,14 @@
|
|||
- Service showcase
|
||||
- Biography section
|
||||
- Testimonials section
|
||||
- Video playback with WebVTT subtitles
|
||||
- Transcription support
|
||||
- Toast notification system
|
||||
- Video playback with WebVTT subtitles and full transcript display
|
||||
- Custom video player with accessibility features:
|
||||
- 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
|
||||
- Custom video player component
|
||||
|
||||
### Development Environment
|
||||
- Multi-container setup with Docker Compose
|
||||
|
|
@ -74,3 +85,4 @@
|
|||
- Secure email handling with Google App credentials
|
||||
- Multi-stage Docker builds
|
||||
- Production-optimized builds with tree-shaking
|
||||
- Security scanning with Trivy
|
||||
|
|
|
|||
57
TO_DO.txt
57
TO_DO.txt
|
|
@ -2,24 +2,26 @@
|
|||
|
||||
## Architecture Overview
|
||||
|
||||
### Frontend (Next.js + React)
|
||||
### Frontend (Next.js 15.2.3 + React 19)
|
||||
- Server-side rendered pages for better SEO and performance
|
||||
- TypeScript for type safety
|
||||
- Tailwind CSS for responsive design
|
||||
- Server Components for critical content delivery
|
||||
- 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
|
||||
- React Context for state management
|
||||
- Protected admin dashboard
|
||||
- Protected admin dashboard with JWT authentication
|
||||
|
||||
### Backend (Node.js + Express)
|
||||
- REST API endpoints for data operations
|
||||
- Simple authentication for admin access
|
||||
- JWT-based authentication with RBAC for admin access
|
||||
- Email handling with Nodemailer
|
||||
- Rate limiting with Upstash Redis
|
||||
- Environment variable management with dotenv
|
||||
- Comprehensive logging system for external analytics
|
||||
- CORS implementation with proper security headers
|
||||
|
||||
### 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
|
||||
|
||||
### Deployment
|
||||
|
|
@ -98,9 +100,16 @@
|
|||
|
||||
- [ ] ASL Video Management
|
||||
- [ ] 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
|
||||
- [ ] Transcription display options
|
||||
- [ ] Custom video player implementation (based on shared codebase)
|
||||
- [ ] Transcription display options alongside video
|
||||
- [ ] 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
|
||||
|
||||
|
|
@ -110,7 +119,7 @@
|
|||
- [ ] Event model
|
||||
- [ ] Page content model
|
||||
- [ ] Minutes/document model
|
||||
- [ ] Video/transcription model
|
||||
- [ ] Video/transcription model with validation
|
||||
|
||||
- [ ] API Endpoints
|
||||
- [ ] Authentication routes
|
||||
|
|
@ -148,12 +157,13 @@
|
|||
## Development Phases
|
||||
|
||||
### 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
|
||||
- [ ] Configure Docker development environment
|
||||
- [ ] Establish database connections
|
||||
- [ ] Configure Docker development environment with security best practices
|
||||
- [ ] Establish database connections with MongoDB 7.0 schema validation
|
||||
- [ ] Create basic project structure
|
||||
- [ ] Set up admin authentication system
|
||||
- [ ] Set up admin authentication system with JWT
|
||||
- [ ] Implement logging infrastructure
|
||||
|
||||
### Phase 2: Core Public Pages
|
||||
|
|
@ -175,11 +185,12 @@
|
|||
- [ ] Implement ASL video management system
|
||||
|
||||
### Phase 4: Accessibility & Refinement
|
||||
- [ ] Implement comprehensive accessibility features
|
||||
- [ ] Implement comprehensive accessibility features according to WCAG 2.2 AA
|
||||
- [ ] Set up email notifications
|
||||
- [ ] Implement social sharing
|
||||
- [ ] Optimize for performance
|
||||
- [ ] Test with screen readers and assistive technologies
|
||||
- [ ] Test with refreshable braille displays
|
||||
|
||||
### Phase 5: Testing & Deployment
|
||||
- [ ] Conduct user testing
|
||||
|
|
@ -216,33 +227,38 @@
|
|||
- [ ] Membership statistics and reporting
|
||||
|
||||
## Accessibility Considerations
|
||||
- [ ] Ensure WCAG 2.1 AA compliance
|
||||
- [ ] Ensure WCAG 2.2 AA compliance
|
||||
- [ ] Implement proper semantic HTML
|
||||
- [ ] Add appropriate ARIA attributes
|
||||
- [ ] Test with screen readers
|
||||
- [ ] Provide captions/transcripts for all video content
|
||||
- [ ] 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
|
||||
- [ ] Text resizing without breaking layout
|
||||
- [ ] Screen magnifier compatibility
|
||||
- [ ] Refreshable braille display compatibility with proper ARIA landmarks
|
||||
|
||||
## Video and Transcription Features
|
||||
- [ ] Custom video player implementation based on existing codebase:
|
||||
- [ ] Custom video player implementation:
|
||||
- [ ] Play/pause toggle with visual indicators
|
||||
- [ ] Progress bar with clickable position seeking
|
||||
- [ ] Forward/backward 5-second skip controls
|
||||
- [ ] 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
|
||||
- [ ] Keyboard shortcuts (space for play/pause, arrow keys for seeking)
|
||||
- [ ] WebVTT subtitle integration with toggle option
|
||||
- [ ] Accessible control buttons with ARIA labels and icon titles
|
||||
- [ ] Touch-friendly controls for mobile devices
|
||||
- [ ] Enhancements to existing video player:
|
||||
- [ ] Enhanced accessibility features:
|
||||
- [ ] Transcript view option alongside video
|
||||
- [ ] Support for three transcript formats (plain, HTML, JSON)
|
||||
- [ ] Direct linking to specific video segments/timestamps
|
||||
- [ ] Video thumbnail generation for previews
|
||||
- [ ] Required thumbnail preview images
|
||||
- [ ] Responsive container sizing for different screen sizes
|
||||
- [ ] Volume control with mute option
|
||||
- [ ] Picture-in-picture support
|
||||
|
|
@ -252,6 +268,7 @@
|
|||
- [ ] Video upload with automatic processing
|
||||
- [ ] VTT subtitle file upload and association
|
||||
- [ ] Transcript generation/editing interface
|
||||
- [ ] Thumbnail generation and management
|
||||
- [ ] Video metadata management (title, description, categories)
|
||||
- [ ] Video organization by categories/tags
|
||||
|
||||
|
|
|
|||
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
|
||||
|
||||
## 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
|
||||
- Successfully installed and configured the Perplexity MCP server
|
||||
- Created planned_implementation.txt with detailed development roadmap
|
||||
- Set up Memory Bank documentation to maintain project context
|
||||
- Used Perplexity MCP to generate best practices for the tech stack
|
||||
- Created best_practices.txt with detailed development guidelines
|
||||
- Created directory structure for the entire application:
|
||||
- Frontend structure with Next.js 15.2.3 app router
|
||||
- Backend structure with Express API endpoints
|
||||
- Docker configuration with security best practices
|
||||
- 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
|
||||
1. Begin implementation of Phase 1: Project Setup & Infrastructure
|
||||
- Initialize Next.js project with TypeScript
|
||||
- Set up project directory structure
|
||||
- Configure development environment with Docker
|
||||
- Establish database connections
|
||||
2. Develop core public-facing pages
|
||||
3. Implement membership and event management systems
|
||||
4. Focus on accessibility features for deaf and DeafBlind users
|
||||
1. Complete remaining API routes and controller stubs
|
||||
2. Set up database connection logic with error handling
|
||||
3. Create frontend components for layout and navigation
|
||||
4. Implement authentication flow for admin users
|
||||
5. Begin development of public-facing pages according to Phase 1
|
||||
6. Set up development environment with npm install and initial build
|
||||
|
||||
## Special Considerations
|
||||
- Maintain WCAG 2.1 AA compliance throughout development
|
||||
- Implement accessible custom video player with WebVTT support
|
||||
- Ensure responsive design works across all device sizes
|
||||
- Follow server-side rendering best practices for SEO and performance
|
||||
- TypeScript errors are expected at this stage since dependencies aren't installed yet
|
||||
- Maintain WCAG 2.2 AA compliance throughout development
|
||||
- Focus on new criteria like Focus Not Obscured and Target Size
|
||||
- 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
|
||||
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
|
||||
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
|
||||
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
|
||||
- Contact the organization
|
||||
- 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
|
||||
|
||||
### For Administrators
|
||||
- Manage website content through a protected dashboard
|
||||
- Manage website content through a protected dashboard with secure authentication
|
||||
- Update event information and calendar
|
||||
- Track membership status, expirations, and contact info
|
||||
- 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
|
||||
- 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
|
||||
|
||||
## 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
|
||||
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)
|
||||
2. Admin dashboard with authentication
|
||||
2. Admin dashboard with JWT authentication and RBAC
|
||||
3. Event management system
|
||||
4. Membership tracking and notification system
|
||||
5. Document repository
|
||||
6. ASL video management with custom player
|
||||
7. Responsive design implementation
|
||||
8. Database schema and API endpoints
|
||||
6. Full implementation of ASL video management with required transcripts and thumbnails
|
||||
7. Responsive design implementation with Tailwind CSS 4.0
|
||||
8. Complete database schema implementation and API endpoints
|
||||
9. Zeffy donation integration
|
||||
10. Email notification system
|
||||
11. Final deployment and testing
|
||||
|
||||
For a detailed breakdown of specific tasks, refer to TO_DO.txt.
|
||||
|
||||
## Progress Status
|
||||
- **Current Phase**: Pre-development planning
|
||||
- **Current Phase**: Project structure and key files setup
|
||||
- **Tasks Completed**:
|
||||
- Initial requirements gathering
|
||||
- Technology stack definition
|
||||
- Setup of Perplexity MCP server for research assistance
|
||||
- Creation of comprehensive best practices documentation
|
||||
- **Tasks In Progress**: Finalizing development approach based on best practices
|
||||
- **Next Tasks**: Setup development environment, initialize Next.js project, establish database connections
|
||||
- Updated technology stack to latest versions
|
||||
- Created complete directory structure according to system patterns
|
||||
- Implemented core configuration files (Docker, Nginx, Tailwind, Next.js)
|
||||
- Created MongoDB initialization scripts with schema validation
|
||||
- Implemented Video Player component with transcript display
|
||||
- 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
|
||||
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
|
||||
|
||||
### Architecture
|
||||
- **Frontend**: Next.js with React for server-side rendered pages
|
||||
- **Backend**: Node.js with Express for API endpoints
|
||||
- **Database Layer**: MongoDB for document storage, Redis for caching
|
||||
- **Containerization**: Docker with Portainer for deployment
|
||||
- **Web Server**: Nginx as reverse proxy
|
||||
- **Frontend**:
|
||||
- Next.js 15.2.3 with React 19 for server-side rendered pages
|
||||
- Server Components for critical content delivery
|
||||
- Client Components for interactive elements
|
||||
- **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
|
||||
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
|
||||
- Server-side rendering with Next.js for improved SEO and performance
|
||||
- TypeScript for type safety throughout the stack
|
||||
- Tailwind CSS for responsive design
|
||||
- MongoDB for flexible document storage of club data
|
||||
- Redis for caching and rate limiting
|
||||
- Docker containerization for consistent deployment
|
||||
- Custom video player implementation for ASL content
|
||||
- Server Components for critical content delivery and improved SEO
|
||||
- TypeScript 5.8 for type safety and incremental build performance
|
||||
- Tailwind CSS 4.0 with CSS-first configuration
|
||||
- MongoDB 7.0 with schema validation for data integrity
|
||||
- Redis for caching, rate limiting, and session management
|
||||
- Docker containerization with security best practices
|
||||
- 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
|
||||
- REST API for data operations
|
||||
- RESTful API with comprehensive endpoint structure
|
||||
- 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
|
||||
- Component-based UI architecture
|
||||
- Authentication for admin dashboard
|
||||
- Atomic design pattern for component organization
|
||||
- Authentication with JWT and HTTP-only cookies
|
||||
- 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 is organized into phases:
|
||||
|
|
@ -39,4 +88,4 @@ Development is organized into phases:
|
|||
4. Accessibility & Refinement
|
||||
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
|
||||
- React 19
|
||||
- TypeScript 5.7
|
||||
- Next.js 14.1
|
||||
- Tailwind CSS 3.4
|
||||
- TypeScript 5.8 (5x faster incremental builds)
|
||||
- Next.js 15.2.3 (includes security patches for CVE-2025-29927)
|
||||
- Tailwind CSS 4.0 (CSS-first configuration with @theme directives, 182x faster builds)
|
||||
- Framer Motion 11
|
||||
- React Router DOM 7
|
||||
- React Icons 5.4
|
||||
- React Simple Maps 4.0 (beta)
|
||||
- React Intersection Observer 9.13
|
||||
- Server Components for critical content delivery
|
||||
|
||||
### Backend
|
||||
- Node.js with Express 4.21
|
||||
- TypeScript
|
||||
- MongoDB for document storage
|
||||
- TypeScript 5.8
|
||||
- MongoDB 7.0 with schema validation and indexing
|
||||
- Redis for caching and session management
|
||||
- Nodemailer 6.9 for email handling
|
||||
- CORS for cross-origin resource sharing
|
||||
- CORS with proper security headers
|
||||
- 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
|
||||
- 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
|
||||
- PostCSS 8.4
|
||||
- Autoprefixer 10.4
|
||||
|
|
@ -35,9 +50,10 @@
|
|||
- Hot module replacement (HMR)
|
||||
|
||||
### Production
|
||||
- Nginx (Production server for frontend)
|
||||
- Nginx (Production server for frontend with security headers)
|
||||
- Node.js server for backend API
|
||||
- SSL for secure connections
|
||||
- SSL/TLS 1.3 for secure connections
|
||||
- Network isolation with Docker
|
||||
|
||||
## Development Setup
|
||||
- Multi-container Docker setup with Docker Compose
|
||||
|
|
@ -46,13 +62,18 @@
|
|||
- Environment variable management for sensitive data
|
||||
- TypeScript configuration for both client and server
|
||||
- Concurrent development server (frontend + backend)
|
||||
- Automated security scanning in the build process
|
||||
|
||||
## Technical Constraints
|
||||
- Emphasis on accessibility for deaf and DeafBlind users
|
||||
- WCAG 2.1 AA compliance required
|
||||
- ASL video support with WebVTT subtitles
|
||||
- Screen reader compatibility
|
||||
- WCAG 2.2 AA compliance required
|
||||
- ASL video support with mandatory:
|
||||
- WebVTT subtitles
|
||||
- Full text transcripts
|
||||
- Thumbnail previews
|
||||
- Screen reader compatibility with ARIA landmarks
|
||||
- High performance requirements for video content
|
||||
- Secure admin access restrictions
|
||||
- Secure admin access with JWT and RBAC
|
||||
- Responsive design for all screen sizes
|
||||
- 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
|
||||
|
||||
### Frontend Architecture
|
||||
- **Framework**: Next.js 14.1 with React 19
|
||||
- **Framework**: Next.js 15.2.3 with React 19
|
||||
- **Rendering Strategy**:
|
||||
- Server-side rendering for critical pages (home, about, events)
|
||||
- Static generation for stable content (bylaws, history)
|
||||
- Client-side rendering for interactive components (calendar, forms)
|
||||
- **State Management**: React Context for application state
|
||||
- **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
|
||||
- Custom CSS variables for theming
|
||||
- Native CSS variables for theme colors and 3D transforms
|
||||
- **Animation**: Framer Motion 11 for UI transitions
|
||||
- **Routing**: Next.js App Router with nested layouts
|
||||
- **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
|
||||
- Efficient database queries with indexing
|
||||
|
||||
### Database Schema
|
||||
### Database Schema with Validation
|
||||
1. **Admin User**
|
||||
```javascript
|
||||
{
|
||||
_id: ObjectId,
|
||||
username: String,
|
||||
passwordHash: String,
|
||||
email: String,
|
||||
lastLogin: Date,
|
||||
resetToken: String,
|
||||
resetTokenExpiry: Date
|
||||
}
|
||||
db.createCollection("adminUsers", {
|
||||
validator: {
|
||||
$jsonSchema: {
|
||||
bsonType: "object",
|
||||
required: ["username", "passwordHash", "email"],
|
||||
properties: {
|
||||
_id: { bsonType: "objectId" },
|
||||
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**
|
||||
|
|
@ -154,20 +171,39 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
|
|||
|
||||
6. **Video**
|
||||
```javascript
|
||||
{
|
||||
_id: ObjectId,
|
||||
title: String,
|
||||
description: String,
|
||||
fileUrl: String,
|
||||
thumbnailUrl: String,
|
||||
duration: Number, // in seconds
|
||||
uploadDate: Date,
|
||||
category: String,
|
||||
subtitleUrl: String, // WebVTT file URL
|
||||
transcriptText: String,
|
||||
isPublished: Boolean,
|
||||
relatedPageId: ObjectId // Reference to Page Content
|
||||
}
|
||||
db.createCollection("videos", {
|
||||
validator: {
|
||||
$jsonSchema: {
|
||||
bsonType: "object",
|
||||
required: ["title", "fileUrl", "thumbnailUrl", "transcriptText"],
|
||||
properties: {
|
||||
_id: { bsonType: "objectId" },
|
||||
title: { bsonType: "string", minLength: 3 },
|
||||
description: { bsonType: "string" },
|
||||
fileUrl: { bsonType: "string" },
|
||||
thumbnailUrl: { bsonType: "string" }, // Required thumbnail preview image
|
||||
duration: { bsonType: "number" }, // in seconds
|
||||
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**
|
||||
|
|
@ -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
|
||||
|
||||
### Accessibility Implementation
|
||||
- WCAG 2.1 AA compliance requirements:
|
||||
- WCAG 2.2 AA compliance requirements:
|
||||
- Semantic HTML structure
|
||||
- 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
|
||||
- Text resizing support without breaking layouts
|
||||
- Target Size (2.5.8) - ensure touch targets are at least 24px × 24px
|
||||
- Screen reader considerations:
|
||||
- ARIA landmarks and labels
|
||||
- Skip navigation links
|
||||
- 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:
|
||||
- High contrast mode
|
||||
- High contrast mode with adequate contrast ratios
|
||||
- Text-only content alternatives
|
||||
- Simple navigation patterns
|
||||
- Reduced motion option
|
||||
- Refreshable braille display compatibility with proper ARIA landmarks
|
||||
|
||||
### Video Player Implementation
|
||||
```typescript
|
||||
|
|
@ -674,27 +715,38 @@ export default Calendar;
|
|||
|
||||
## Deployment Architecture
|
||||
|
||||
### Docker Compose Setup
|
||||
### Docker Compose Setup with Security Best Practices
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Frontend service
|
||||
# Frontend service with multi-stage build (Dockerfile below)
|
||||
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
|
||||
- ./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:
|
||||
|
|
@ -702,6 +754,7 @@ services:
|
|||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
user: node
|
||||
ports:
|
||||
- "4000:4000"
|
||||
depends_on:
|
||||
|
|
@ -709,29 +762,66 @@ services:
|
|||
- redis
|
||||
env_file: ./backend/.env
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
- ./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:latest
|
||||
image: mongo:7.0
|
||||
restart: unless-stopped
|
||||
user: mongodb
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
- 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
|
||||
- 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:
|
||||
|
|
@ -741,24 +831,98 @@ services:
|
|||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/conf:/etc/nginx/conf.d
|
||||
- ./nginx/ssl:/etc/nginx/ssl
|
||||
- ./frontend/public:/var/www/html
|
||||
- ./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'
|
||||
|
||||
# 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:
|
||||
app-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/24
|
||||
|
||||
# Volumes
|
||||
# Volumes with backup capability
|
||||
volumes:
|
||||
mongo-data:
|
||||
name: ocd-mongo-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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue