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:
TheMaddax 2025-03-25 09:28:10 -05:00
parent 42b9d1b162
commit edb6faa351
28 changed files with 2319 additions and 146 deletions

View file

@ -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

View file

@ -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
View 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
View 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
View 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);
}
});

View 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.'
}
});
}
};
};

View 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);

View 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);

View 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;

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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
View 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
View 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
View 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
View 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
View 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"
}
}

View 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;
}
}

View 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
View 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>
);
}

View 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>
);
};

View 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;

View 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
View 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
View 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;
}

View file

@ -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