Compare commits
98 commits
temp-branc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3cb7795b7 | ||
| f91bbd384d | |||
| 54bfdbedce | |||
|
|
95c7ad944c | ||
|
|
3906d069a4 | ||
|
|
4f9a7ac018 | ||
|
|
8cc11c3352 | ||
|
|
9a9be40151 | ||
|
|
036fdb93f2 | ||
|
|
833fb12305 | ||
|
|
2e990e72a9 | ||
|
|
107b8831b4 | ||
|
|
ea27cfa5fb | ||
|
|
02989ce809 | ||
|
|
be03ba9f7c | ||
|
|
d9156a9f21 | ||
|
|
e660b6d8cc | ||
|
|
1537926e0c | ||
|
|
4c768e0edf | ||
|
|
a410bb853e | ||
|
|
bc05b0106e | ||
|
|
abb6f511fb | ||
|
|
a25e858a35 | ||
|
|
ee5f7bcb6e | ||
|
|
1d63dfddef | ||
| 107f2de66c | |||
|
|
efae0c098c | ||
| 7d079f9b37 | |||
|
|
596c5033a0 | ||
| 78aade0c94 | |||
| 4755cd3fd6 | |||
| a565932eeb | |||
| ae7c52db20 | |||
| a0cf3e9f35 | |||
| df6c69684a | |||
| 379c4e1e44 | |||
| 8b2b421156 | |||
| 5d49c9ec72 | |||
| 67c775a66f | |||
| 86096d99e3 | |||
| b1e7d29677 | |||
| b214427fda | |||
| a2b3b1756d | |||
| 7a752771c8 | |||
| e3930cf87a | |||
| 3e1892a6f7 | |||
| b6f727be5d | |||
| 6e4db3ff67 | |||
| 0fa61e4c55 | |||
| 4ee567e9af | |||
| 28c341ea00 | |||
| 8709bf66f1 | |||
| d65f8a7e80 | |||
| 03286c9f75 | |||
| 53ddc334f1 | |||
| f18ceeb663 | |||
| fd00d16c37 | |||
| f0135e4e54 | |||
| 13d137847c | |||
| 28c81bddaf | |||
| 4bdde33534 | |||
| dcce8c0b3e | |||
| 78c1422fe4 | |||
| 3f23aa5e93 | |||
| d13838758e | |||
| 3bf017543c | |||
| e238c56480 | |||
| 195aeea56b | |||
| 185c70e76e | |||
| d20a4f74ed | |||
| a09c831b2e | |||
| f56c7ab126 | |||
| 4c0fde6415 | |||
| 545b49b65f | |||
| da49f378c6 | |||
| 6365a2ecb2 | |||
| 547b3b1440 | |||
| 33a2488869 | |||
| 0823a15744 | |||
| 620666e7f1 | |||
| 7b27845812 | |||
| a85d4e2ee9 | |||
| b4a89287bd | |||
| 2dfa0daa42 | |||
| 05786a9517 | |||
| 55c2bc5e12 | |||
| 90ca68e939 | |||
| aec55f02c8 | |||
| d426b49670 | |||
| e41ce01162 | |||
| a29b2bb2b5 | |||
| bf4b633935 | |||
| f3f144fdab | |||
| 8389c4a040 | |||
| aa355375e9 | |||
| 140e9a532c | |||
| c0bb3a6e91 | |||
| 0b10547bd5 |
65 changed files with 8388 additions and 1035 deletions
|
|
@ -2,4 +2,7 @@ node_modules
|
|||
dist
|
||||
.git
|
||||
*.log
|
||||
public/
|
||||
public/videos/*
|
||||
!public/videos/.gitkeep
|
||||
**/.DS_Store
|
||||
|
||||
|
|
|
|||
7
.env
Normal file
7
.env
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
RECIPIENT_EMAIL=eliza@deafgain.org
|
||||
GOOGLE_EMAIL=system@deafgain.org
|
||||
GOOGLE_APP_PASSWORD=idjcxxwzvloorfuk
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
13
.env.example
Normal file
13
.env.example
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Environment Configuration for Production
|
||||
NODE_ENV=production
|
||||
|
||||
# Gmail SMTP Configuration
|
||||
GMAIL_USER=your-gmail-username@gmail.com
|
||||
GMAIL_PASS=your-app-specific-password
|
||||
|
||||
# Redis Configuration (Upstash)
|
||||
REDIS_URL=redis://your-redis-url
|
||||
REDIS_TOKEN=your-redis-token
|
||||
|
||||
# API Configuration
|
||||
API_URL=http://localhost:804
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.mov filter=lfs diff=lfs merge=lfs -text
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
|
|
@ -21,6 +21,8 @@ build
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
|
@ -35,5 +37,25 @@ yarn-error.log*
|
|||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Video files (too large for git)
|
||||
public/videos/*.mp4
|
||||
public/videos/*.webm
|
||||
|
||||
# Development files
|
||||
new.ass
|
||||
matomo_log_format.conf
|
||||
|
||||
# Docker volumes and temporary files
|
||||
.docker/
|
||||
docker-compose.override.yml
|
||||
|
||||
# OS generated files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
*.lnk
|
||||
|
|
|
|||
127
Caddyfile
Normal file
127
Caddyfile
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
{
|
||||
admin 0.0.0.0:2019
|
||||
email chris@sigd.net
|
||||
}
|
||||
|
||||
# Internal Services
|
||||
gitea.sigd.net {
|
||||
reverse_proxy 10.4.0.156:3000
|
||||
}
|
||||
|
||||
statistics.deafgain.org {
|
||||
reverse_proxy matomo:80
|
||||
}
|
||||
|
||||
archives.sigd.net {
|
||||
reverse_proxy 10.4.0.159:8001
|
||||
}
|
||||
|
||||
upload.sigd.net {
|
||||
reverse_proxy 10.4.0.159:4174
|
||||
}
|
||||
|
||||
preview.sigd.net {
|
||||
reverse_proxy 10.4.0.83:3000
|
||||
}
|
||||
|
||||
droppy.sigd.net {
|
||||
reverse_proxy 10.4.0.159:6969
|
||||
}
|
||||
|
||||
upload.sigd.net/api/* {
|
||||
# Forward API requests to the backend API service
|
||||
uri strip_prefix /api
|
||||
reverse_proxy 10.4.0.159:3334
|
||||
}
|
||||
|
||||
requests.sigd.net {
|
||||
reverse_proxy 10.4.0.157:5055
|
||||
}
|
||||
|
||||
documents.deafgain.org {
|
||||
reverse_proxy 10.4.0.156:8001
|
||||
}
|
||||
|
||||
documents.sigd.net {
|
||||
reverse_proxy 10.4.0.159:8001
|
||||
}
|
||||
|
||||
doculens.deafgain.org {
|
||||
reverse_proxy 10.4.0.156:5015
|
||||
}
|
||||
|
||||
firewall.sigd.net {
|
||||
reverse_proxy 10.4.0.1:9080
|
||||
}
|
||||
|
||||
# Public Websites (on this VM - 10.4.0.205)
|
||||
|
||||
# Deaf Summit Website
|
||||
deafsummit.org {
|
||||
# Route API requests to backend
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:808
|
||||
}
|
||||
|
||||
# Route everything else to frontend
|
||||
handle {
|
||||
reverse_proxy localhost:807
|
||||
}
|
||||
}
|
||||
|
||||
www.deafsummit.org {
|
||||
redir https://deafsummit.org{uri} permanent
|
||||
}
|
||||
|
||||
# DeafMissoula.org
|
||||
www.deafmissoula.org {
|
||||
redir https://deafmissoula.org{uri} permanent
|
||||
}
|
||||
|
||||
deafmissoula.org {
|
||||
reverse_proxy 172.18.0.7:801
|
||||
}
|
||||
|
||||
# ChrisHaulmark.com
|
||||
www.chrishaulmark.com {
|
||||
redir https://chrishaulmark.com{uri} permanent
|
||||
}
|
||||
|
||||
chrishaulmark.com {
|
||||
reverse_proxy chrishaulmark-website-web-1:803
|
||||
}
|
||||
|
||||
# DeafGain.org
|
||||
www.deafgain.org {
|
||||
redir https://deafgain.org{uri} permanent
|
||||
}
|
||||
|
||||
deafgain.org {
|
||||
reverse_proxy 172.18.0.4:804
|
||||
}
|
||||
|
||||
# FinLion domains
|
||||
finlion.com {
|
||||
reverse_proxy finlion-website-web-1:805
|
||||
}
|
||||
|
||||
www.finlion.com {
|
||||
redir https://finlion.com{uri} permanent
|
||||
}
|
||||
|
||||
finlion.net, www.finlion.net {
|
||||
redir https://finlion.com{uri} permanent
|
||||
}
|
||||
|
||||
finlion.org, www.finlion.org {
|
||||
redir https://finlion.com{uri} permanent
|
||||
}
|
||||
|
||||
# MAD Lawsuit
|
||||
www.mad-lawsuit.org {
|
||||
redir https://mad-lawsuit.org{uri} permanent
|
||||
}
|
||||
|
||||
mad-lawsuit.org {
|
||||
reverse_proxy mad-lawsuit-frontend-1:806
|
||||
}
|
||||
492
DEVELOPMENT_DOCUMENTATION.txt
Normal file
492
DEVELOPMENT_DOCUMENTATION.txt
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
# DeafGain Website - Complete Development Documentation
|
||||
## Technical Stack Overview & Recent Enhancements
|
||||
|
||||
================================================================================
|
||||
PROJECT OVERVIEW
|
||||
================================================================================
|
||||
|
||||
DeafGain LLC Website serves as a comprehensive digital platform dedicated to
|
||||
empowering the Deaf community through innovative communication solutions,
|
||||
professional training resources, and accessibility services. The website
|
||||
focuses on providing ADA-compliant content with full accessibility features
|
||||
including WebVTT subtitles, transcriptions, and universal design principles.
|
||||
|
||||
Live Production URL: https://deafgain.org/
|
||||
Development Server: http://localhost:5173
|
||||
|
||||
================================================================================
|
||||
TECHNOLOGY STACK - COMPREHENSIVE BREAKDOWN
|
||||
================================================================================
|
||||
|
||||
FRONTEND TECHNOLOGIES
|
||||
---------------------
|
||||
- React 19.0.0 - Core UI framework with latest features
|
||||
- TypeScript 5.7.2 - Type safety and enhanced development experience
|
||||
- React Router 7.6.1 - Client-side routing with latest security fixes
|
||||
- Tailwind CSS 3.4.16 - Utility-first CSS framework for rapid styling
|
||||
- Framer Motion 11.13.5 - Advanced animations and micro-interactions
|
||||
- React Simple Maps 4.0.0-beta.6 - Geographic visualization components
|
||||
- React Icons 5.4.0 - Comprehensive icon library
|
||||
- React Intersection Observer 9.13.1 - Scroll detection and lazy loading
|
||||
|
||||
BACKEND TECHNOLOGIES
|
||||
--------------------
|
||||
- Express.js 4.21.2 - Web application framework for Node.js
|
||||
- Node.js (Latest LTS) - JavaScript runtime environment
|
||||
- Nodemailer 6.9.9 - Email service integration
|
||||
- Upstash Redis 1.28.4 - Cloud Redis for caching and rate limiting
|
||||
- TypeScript 5.7.2 - Backend type safety
|
||||
- dotenv 16.4.7 - Environment variable management
|
||||
|
||||
DEVELOPMENT & BUILD TOOLS
|
||||
-------------------------
|
||||
- Vite 6.3.5 - Lightning-fast build tool and dev server
|
||||
- pnpm 9.14.2 - Efficient package manager
|
||||
- ESLint 9.15.0 - Code quality and consistency enforcement
|
||||
- PostCSS 8.4.49 - CSS processing and optimization
|
||||
- Autoprefixer 10.4.20 - CSS vendor prefix automation
|
||||
- Concurrently 9.1.2 - Parallel process management
|
||||
- tsx 4.19.4 - TypeScript execution for development
|
||||
|
||||
DEPLOYMENT & INFRASTRUCTURE
|
||||
----------------------------
|
||||
- Docker & Docker Compose - Containerization and orchestration
|
||||
- Nginx - Reverse proxy and static file serving
|
||||
- Redis - Session management and caching
|
||||
- Gmail SMTP - Email service integration
|
||||
- Caddy Reverse Proxy - Production load balancing
|
||||
|
||||
================================================================================
|
||||
ARCHITECTURE OVERVIEW
|
||||
================================================================================
|
||||
|
||||
FRONTEND ARCHITECTURE
|
||||
---------------------
|
||||
The application follows a modern React architecture with:
|
||||
|
||||
1. Component Structure:
|
||||
- Layout components (Navbar, Footer) for consistent structure
|
||||
- Page components for route-specific content
|
||||
- Shared components for reusability
|
||||
- Context providers for global state management
|
||||
|
||||
2. State Management:
|
||||
- React Context for global application state
|
||||
- Local component state for UI-specific data
|
||||
- Notification context for toast messaging system
|
||||
|
||||
3. Routing System:
|
||||
- React Router v7 for client-side navigation
|
||||
- Protected routes and dynamic routing
|
||||
- Direct video linking with URL parameters
|
||||
|
||||
BACKEND ARCHITECTURE
|
||||
--------------------
|
||||
Express.js RESTful API with:
|
||||
|
||||
1. API Structure:
|
||||
- RESTful endpoints for contact and subscription
|
||||
- Rate-limited routes for security
|
||||
- Error handling and input validation
|
||||
|
||||
2. Services:
|
||||
- Email service with professional templates
|
||||
- Redis-backed caching and session management
|
||||
- Rate limiting middleware
|
||||
|
||||
SECURITY IMPLEMENTATION
|
||||
-----------------------
|
||||
- Input validation and sanitization
|
||||
- Rate limiting on API endpoints (Redis-backed)
|
||||
- CORS policy configuration
|
||||
- Environment variable protection
|
||||
- Secure email handling with app passwords
|
||||
|
||||
================================================================================
|
||||
RECENT ENHANCEMENTS - VIDEO CARD INTERFACE IMPROVEMENTS
|
||||
================================================================================
|
||||
|
||||
OVERVIEW OF MODIFICATIONS
|
||||
-------------------------
|
||||
Recent updates to the Resources page video library interface focus on enhanced
|
||||
user experience, modern visual design, and improved accessibility. These changes
|
||||
maintain full functionality while significantly upgrading the visual appeal.
|
||||
|
||||
DETAILED CHANGES IMPLEMENTED
|
||||
----------------------------
|
||||
|
||||
1. ENHANCED CARD DESIGN
|
||||
- Border Radius: Upgraded from 'rounded-lg' to 'rounded-xl' for modern aesthetics
|
||||
- Shadow System: Enhanced from 'shadow-md' to 'shadow-lg' with 'hover:shadow-2xl'
|
||||
- Subtle Borders: Added 'border border-gray-100' for better card definition
|
||||
- Hover Effects: Implemented lift animation 'hover:-translate-y-2' for interactivity
|
||||
|
||||
2. ADVANCED ANIMATION SYSTEM
|
||||
- Staggered Entrance: Cards animate in sequentially with index-based delays
|
||||
- Smooth Transitions: Enhanced image scaling from 'scale-105' to 'scale-110'
|
||||
- Micro-interactions: Added Framer Motion 'whileHover' and 'whileTap' effects
|
||||
- Duration Control: Optimized animation timing for professional feel
|
||||
|
||||
3. IMPROVED VISUAL HIERARCHY
|
||||
- Always-Visible Play Icon: Added semi-transparent play button in top-right
|
||||
- Gradient Overlays: Implemented 'bg-gradient-to-t from-black/70' for readability
|
||||
- Typography Enhancement: Larger titles with improved line-height and spacing
|
||||
- Color Transitions: Added hover color changes for better user feedback
|
||||
|
||||
4. ENHANCED BUTTON INTERFACE
|
||||
- Primary Action Styling: "Watch Video" button uses gradient background
|
||||
- Secondary Button Design: "Share" and "Transcript" use backdrop blur effects
|
||||
- Consistent Spacing: Improved button layouts and grouping
|
||||
- Professional Appearance: Added borders and enhanced shadow effects
|
||||
|
||||
5. CONTENT LAYOUT IMPROVEMENTS
|
||||
- Increased Grid Spacing: Changed from 'gap-6' to 'gap-8' for better breathing room
|
||||
- Enhanced Padding: More generous content padding throughout cards
|
||||
- Action Bar Addition: Bottom section with professional training indicators
|
||||
- Visual Elements: Added decorative dot patterns for polish
|
||||
|
||||
TECHNICAL IMPLEMENTATION DETAILS
|
||||
--------------------------------
|
||||
|
||||
File Modified: src/pages/Resources.tsx
|
||||
Lines Changed: Approximately 150-250 (video card rendering section)
|
||||
|
||||
Key Code Changes:
|
||||
- Added staggered animation with index-based delays
|
||||
- Implemented gradient overlays for better text contrast
|
||||
- Enhanced button styling with Framer Motion animations
|
||||
- Added professional indicators and visual elements
|
||||
- Improved responsive design considerations
|
||||
|
||||
Performance Considerations:
|
||||
- Animations are GPU-accelerated using CSS transforms
|
||||
- Lazy loading maintained for video thumbnails
|
||||
- No impact on existing functionality or loading times
|
||||
- Accessibility features preserved and enhanced
|
||||
|
||||
================================================================================
|
||||
DEVELOPMENT SETUP INSTRUCTIONS
|
||||
================================================================================
|
||||
|
||||
PREREQUISITES
|
||||
-------------
|
||||
- Node.js LTS version (18.x or higher)
|
||||
- pnpm 9.14.2 (package manager)
|
||||
- Docker & Docker Compose (for containerized development)
|
||||
- Git for version control
|
||||
|
||||
LOCAL DEVELOPMENT SETUP
|
||||
-----------------------
|
||||
1. Clone Repository:
|
||||
git clone [repository-url]
|
||||
cd deafgain-website
|
||||
|
||||
2. Install Dependencies:
|
||||
pnpm install
|
||||
|
||||
3. Environment Configuration:
|
||||
cp .env.example .env
|
||||
# Configure the following variables:
|
||||
- GMAIL_USER=your-email@gmail.com
|
||||
- GMAIL_PASS=your-app-password
|
||||
- REDIS_URL=your-redis-url
|
||||
- REDIS_TOKEN=your-redis-token
|
||||
|
||||
4. Start Development Servers:
|
||||
pnpm dev
|
||||
# This starts both frontend (port 5173) and backend (port 804)
|
||||
|
||||
5. Access Application:
|
||||
Frontend: http://localhost:5173
|
||||
API: http://localhost:804
|
||||
|
||||
DOCKER DEVELOPMENT
|
||||
------------------
|
||||
Alternative development setup using Docker:
|
||||
|
||||
1. Development Environment:
|
||||
docker-compose -f docker-compose.dev.yml up
|
||||
|
||||
2. Production Build:
|
||||
docker-compose up --build
|
||||
|
||||
AVAILABLE SCRIPTS
|
||||
-----------------
|
||||
- pnpm dev - Start development servers (frontend + backend)
|
||||
- pnpm build - Build for production
|
||||
- pnpm preview - Preview production build locally
|
||||
- pnpm lint - Run ESLint code quality checks
|
||||
|
||||
================================================================================
|
||||
PRODUCTION DEPLOYMENT
|
||||
================================================================================
|
||||
|
||||
DEPLOYMENT ARCHITECTURE
|
||||
-----------------------
|
||||
Production deployment utilizes a multi-container Docker setup:
|
||||
|
||||
1. Web Container (Nginx):
|
||||
- Serves static frontend files
|
||||
- Handles routing for single-page application
|
||||
- Configured reverse proxy for API endpoints
|
||||
|
||||
2. API Container (Node.js):
|
||||
- Express.js server handling API requests
|
||||
- Email service integration
|
||||
- Rate limiting and security middleware
|
||||
|
||||
3. Redis Container:
|
||||
- Caching layer for improved performance
|
||||
- Session management
|
||||
- Rate limiting storage
|
||||
|
||||
PRODUCTION CONFIGURATION
|
||||
------------------------
|
||||
- Container Names: deafgain-website-web-1, deafgain-website-api-1
|
||||
- Network: Integrated with Caddy reverse proxy (caddy_network)
|
||||
- Ports: Web (804), API (3000), Redis (internal)
|
||||
- Volumes: Video content mounted from host system
|
||||
|
||||
ENVIRONMENT VARIABLES (Production)
|
||||
----------------------------------
|
||||
Required environment variables for production:
|
||||
- GMAIL_USER: Email service authentication
|
||||
- GMAIL_PASS: Gmail app password
|
||||
- REDIS_URL: Production Redis connection string
|
||||
- REDIS_TOKEN: Redis authentication token
|
||||
- NODE_ENV: Set to 'production'
|
||||
|
||||
================================================================================
|
||||
KEY FEATURES & FUNCTIONALITY
|
||||
================================================================================
|
||||
|
||||
VIDEO RESOURCE LIBRARY
|
||||
-----------------------
|
||||
- 6 Professional development videos with full accessibility
|
||||
- WebVTT subtitle support for hearing accessibility
|
||||
- Complete transcriptions with scene descriptions
|
||||
- Modal video player with native HTML5 controls
|
||||
- Share functionality with direct video linking
|
||||
- Responsive grid layout (1/2/3 columns based on screen size)
|
||||
|
||||
EMAIL SUBSCRIPTION SYSTEM
|
||||
--------------------------
|
||||
- Professional subscription form with validation
|
||||
- Rate-limited API endpoints for security
|
||||
- Gmail SMTP integration with branded templates
|
||||
- Redis-backed subscriber management
|
||||
- Success/error notification system
|
||||
|
||||
ACCESSIBILITY FEATURES
|
||||
----------------------
|
||||
- WCAG 2.1 Level AA compliance
|
||||
- Screen reader compatibility
|
||||
- Keyboard navigation support
|
||||
- High contrast color ratios
|
||||
- Alternative text for all images
|
||||
- Semantic HTML structure
|
||||
|
||||
INTERACTIVE COMPONENTS
|
||||
----------------------
|
||||
- Custom video player with accessibility controls
|
||||
- Snow peaks animation effect
|
||||
- Contact form with real-time validation
|
||||
- Toast notification system
|
||||
- Responsive navigation with mobile support
|
||||
|
||||
================================================================================
|
||||
FILE STRUCTURE & ORGANIZATION
|
||||
================================================================================
|
||||
|
||||
ROOT LEVEL
|
||||
----------
|
||||
├── src/ # Source code directory
|
||||
├── public/ # Static assets
|
||||
├── cline_docs/ # Development documentation
|
||||
├── docker-compose.yml # Production container config
|
||||
├── docker-compose.dev.yml # Development container config
|
||||
├── package.json # Dependencies and scripts
|
||||
├── tailwind.config.js # Tailwind CSS configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── vite.config.ts # Vite build configuration
|
||||
|
||||
SOURCE CODE STRUCTURE
|
||||
---------------------
|
||||
src/
|
||||
├── api/ # Backend API routes
|
||||
│ ├── contact.ts # Contact form handler
|
||||
│ └── subscribe.ts # Email subscription handler
|
||||
├── components/ # React components
|
||||
│ ├── Layout/ # Layout components
|
||||
│ │ ├── Navbar.tsx # Navigation component
|
||||
│ │ ├── Footer.tsx # Footer component
|
||||
│ │ └── Layout.tsx # Main layout wrapper
|
||||
│ ├── shared/ # Reusable components
|
||||
│ │ └── Toast.tsx # Notification component
|
||||
│ ├── VideoPlayer.tsx # Custom video player
|
||||
│ └── SnowPeaks.tsx # Animation component
|
||||
├── context/ # React Context providers
|
||||
│ ├── notification-context.ts
|
||||
│ └── NotificationProvider.tsx
|
||||
├── lib/ # Utility functions
|
||||
│ ├── email.ts # Email service utilities
|
||||
│ └── rate-limit.ts # Rate limiting logic
|
||||
├── pages/ # Route components
|
||||
│ ├── Home.tsx # Landing page
|
||||
│ ├── About.tsx # About page
|
||||
│ ├── Services.tsx # Services showcase
|
||||
│ ├── Resources.tsx # Video library (recently enhanced)
|
||||
│ └── Contact.tsx # Contact form
|
||||
└── main.tsx # Application entry point
|
||||
|
||||
PUBLIC ASSETS STRUCTURE
|
||||
-----------------------
|
||||
public/
|
||||
├── images/
|
||||
│ ├── thumbnails/ # Video thumbnail images
|
||||
│ ├── eliza.jpg # Profile images
|
||||
│ └── scene.jpg # Background images
|
||||
├── subtitles/ # WebVTT subtitle files
|
||||
├── transcriptions/ # Full text transcriptions
|
||||
└── videos/ # Video content files
|
||||
|
||||
================================================================================
|
||||
PERFORMANCE OPTIMIZATION
|
||||
================================================================================
|
||||
|
||||
FRONTEND OPTIMIZATIONS
|
||||
----------------------
|
||||
- Tree shaking for minimal bundle size
|
||||
- Code splitting for faster initial loads
|
||||
- Image optimization and lazy loading
|
||||
- CSS purging for reduced stylesheet size
|
||||
- Modern JavaScript features for better performance
|
||||
|
||||
BACKEND OPTIMIZATIONS
|
||||
---------------------
|
||||
- Redis caching for frequently accessed data
|
||||
- Rate limiting to prevent abuse
|
||||
- Optimized database queries
|
||||
- Gzip compression for API responses
|
||||
- Static file serving through Nginx
|
||||
|
||||
DEPLOYMENT OPTIMIZATIONS
|
||||
------------------------
|
||||
- Multi-stage Docker builds for smaller images
|
||||
- Nginx for efficient static file serving
|
||||
- CDN-ready asset organization
|
||||
- Production build optimization
|
||||
- Container resource allocation
|
||||
|
||||
================================================================================
|
||||
SECURITY CONSIDERATIONS
|
||||
================================================================================
|
||||
|
||||
API SECURITY
|
||||
------------
|
||||
- Rate limiting on all public endpoints
|
||||
- Input validation and sanitization
|
||||
- CORS policy configuration
|
||||
- Environment variable protection
|
||||
- Secure email handling
|
||||
|
||||
DATA PROTECTION
|
||||
---------------
|
||||
- No sensitive data stored in client-side code
|
||||
- Secure credential management
|
||||
- Redis session security
|
||||
- Email data encryption in transit
|
||||
|
||||
DEPLOYMENT SECURITY
|
||||
-------------------
|
||||
- Container isolation
|
||||
- Secure environment variable handling
|
||||
- Network segmentation
|
||||
- SSL/TLS termination at proxy level
|
||||
|
||||
================================================================================
|
||||
MAINTENANCE & MONITORING
|
||||
================================================================================
|
||||
|
||||
REGULAR MAINTENANCE TASKS
|
||||
--------------------------
|
||||
1. Dependency Updates:
|
||||
- Monthly security patch reviews
|
||||
- Quarterly major version updates
|
||||
- Browser compatibility testing
|
||||
|
||||
2. Performance Monitoring:
|
||||
- Load time analysis
|
||||
- Core Web Vitals tracking
|
||||
- Error rate monitoring
|
||||
|
||||
3. Content Management:
|
||||
- Video content updates
|
||||
- Subtitle accuracy verification
|
||||
- Transcription quality checks
|
||||
|
||||
TROUBLESHOOTING GUIDE
|
||||
---------------------
|
||||
1. Development Issues:
|
||||
- Clear node_modules and reinstall dependencies
|
||||
- Check environment variable configuration
|
||||
- Verify port availability (5173, 804)
|
||||
|
||||
2. Production Issues:
|
||||
- Check container logs: docker logs [container-name]
|
||||
- Verify environment variables in production
|
||||
- Monitor Redis connection health
|
||||
|
||||
3. Email Service Issues:
|
||||
- Verify Gmail app password configuration
|
||||
- Check SMTP connection settings
|
||||
- Review rate limiting logs
|
||||
|
||||
================================================================================
|
||||
FUTURE DEVELOPMENT ROADMAP
|
||||
================================================================================
|
||||
|
||||
PLANNED ENHANCEMENTS
|
||||
--------------------
|
||||
1. Additional Video Categories:
|
||||
- Communication training modules
|
||||
- Leadership development series
|
||||
- Technical skill workshops
|
||||
|
||||
2. User Experience Improvements:
|
||||
- Video progress tracking
|
||||
- User preference settings
|
||||
- Advanced search functionality
|
||||
|
||||
3. Administrative Features:
|
||||
- Content management system
|
||||
- Analytics dashboard
|
||||
- User engagement metrics
|
||||
|
||||
TECHNICAL IMPROVEMENTS
|
||||
----------------------
|
||||
1. Performance Enhancements:
|
||||
- Service worker implementation
|
||||
- Advanced caching strategies
|
||||
- Progressive web app features
|
||||
|
||||
2. Accessibility Enhancements:
|
||||
- Enhanced screen reader support
|
||||
- Voice navigation capabilities
|
||||
- Multi-language subtitle support
|
||||
|
||||
================================================================================
|
||||
CONTACT & SUPPORT
|
||||
================================================================================
|
||||
|
||||
For technical questions or development support:
|
||||
- Production URL: https://deafgain.org/
|
||||
- Development Environment: http://localhost:5173
|
||||
- Documentation Location: cline_docs/ directory
|
||||
- Recent Changes: See Git commit history for detailed change log
|
||||
|
||||
This documentation should be updated with each major release or significant
|
||||
feature addition to maintain accuracy and usefulness for the development team.
|
||||
|
||||
Last Updated: May 30, 2025
|
||||
Version: 1.0 (Post Video Card Enhancement)
|
||||
30
Dockerfile
30
Dockerfile
|
|
@ -1,12 +1,9 @@
|
|||
FROM node:20-alpine AS base
|
||||
|
||||
# Enable pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
# Copy package files first
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
FROM base AS deps
|
||||
|
|
@ -19,11 +16,24 @@ COPY . .
|
|||
RUN pnpm build
|
||||
|
||||
# Production image
|
||||
FROM base AS runner
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY package.json ./
|
||||
FROM nginx:alpine AS runner
|
||||
|
||||
# Create required directories for web content and logs
|
||||
RUN mkdir -p /usr/share/nginx/html/images/thumbnails \
|
||||
/usr/share/nginx/html/subtitles \
|
||||
/usr/share/nginx/html/transcriptions \
|
||||
/usr/share/nginx/html/videos \
|
||||
/var/log/nginx/deafgain-website
|
||||
|
||||
# Copy built app and static assets
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /app/public/images/ /usr/share/nginx/html/images/
|
||||
COPY --from=builder /app/public/subtitles /usr/share/nginx/html/subtitles
|
||||
COPY --from=builder /app/public/transcriptions /usr/share/nginx/html/transcriptions
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 804
|
||||
|
||||
CMD ["pnpm", "preview", "--port", "804", "--host"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
|
|||
20
Dockerfile.api
Normal file
20
Dockerfile.api
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
FROM node:20-alpine
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install
|
||||
|
||||
COPY server.ts ./
|
||||
COPY src/api ./src/api
|
||||
COPY src/lib ./src/lib
|
||||
COPY src/declarations.d.ts ./src/
|
||||
COPY tsconfig.json tsconfig.server.json ./
|
||||
|
||||
RUN pnpm tsc -p tsconfig.server.json
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
||||
13
Dockerfile.dev.api
Normal file
13
Dockerfile.dev.api
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:20-alpine
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install
|
||||
|
||||
# We'll mount the source code as a volume, so we don't need to copy it here
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["pnpm", "dev:server"]
|
||||
207
README.md
207
README.md
|
|
@ -1,102 +1,163 @@
|
|||
# DeafGain LLC Website 🌐
|
||||
# DeafGain Website
|
||||
|
||||
[](https://pnpm.io/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://reactjs.org/)
|
||||
Professional website for DeafGain LLC featuring accessibility-focused design, video resources, and communication tools for the Deaf community.
|
||||
|
||||
## Overview
|
||||
## 🚀 Production Deployment via Portainer
|
||||
|
||||
A dynamic web platform dedicated to empowering the Deaf community through innovative communication solutions, training, and accessibility services. This website serves as the digital presence for DeafGain LLC, showcasing our comprehensive services including ADA-compliant transcripts, universal website design, board communication protocols, and leadership training.
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose installed on target server
|
||||
- Portainer deployed and accessible
|
||||
- Git repository access
|
||||
- Environment variables configured
|
||||
|
||||
## 🚀 Quick Start
|
||||
### Quick Deploy with Portainer
|
||||
|
||||
\`\`\`bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
\`\`\`
|
||||
1. **Create Stack in Portainer:**
|
||||
- Go to Stacks → Add Stack
|
||||
- Choose "Repository" as build method
|
||||
- Repository URL: `[YOUR_GIT_REPO_URL]`
|
||||
- Compose path: `docker-compose.yml`
|
||||
|
||||
Visit \`localhost:3000\` in your browser.
|
||||
2. **Configure Environment Variables:**
|
||||
```env
|
||||
NODE_ENV=production
|
||||
GMAIL_USER=your-gmail-username@gmail.com
|
||||
GMAIL_PASS=your-app-specific-password
|
||||
REDIS_URL=redis://your-redis-url
|
||||
REDIS_TOKEN=your-redis-token
|
||||
API_URL=http://localhost:804
|
||||
```
|
||||
|
||||
## 🛠 Technical Stack
|
||||
3. **Deploy Stack:**
|
||||
- Click "Deploy the stack"
|
||||
- Wait for containers to build and start
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| React | 18.2.0 | Core Framework |
|
||||
| TypeScript | 4.9.5 | Type Safety |
|
||||
| Tailwind CSS | 3.x | Styling |
|
||||
| Framer Motion | 10.x | Animations |
|
||||
| React Simple Maps | 3.0.0 | Geographic Visualization |
|
||||
| Vite | 4.x | Build System |
|
||||
| Docker | - | Containerization |
|
||||
| Nginx | - | Web Server |
|
||||
### Manual Docker Deployment
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone [YOUR_GIT_REPO_URL]
|
||||
cd deafgain-website
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your production values
|
||||
|
||||
# Build and deploy
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Services
|
||||
- **Frontend:** React 19 + TypeScript + Vite (Nginx served)
|
||||
- **Backend:** Node.js + Express API server
|
||||
- **Cache:** Redis for rate limiting and sessions
|
||||
- **Proxy:** Nginx reverse proxy
|
||||
|
||||
### Ports
|
||||
- **80:** Main website (Nginx)
|
||||
- **804:** API server (internal)
|
||||
- **6379:** Redis (internal)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
\`\`\`
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── layout/ # Layout components
|
||||
│ ├── ui/ # UI elements
|
||||
│ └── maps/ # Map visualization components
|
||||
├── pages/ # Main application pages
|
||||
├── hooks/ # Custom React hooks
|
||||
├── utils/ # Utility functions
|
||||
├── types/ # TypeScript definitions
|
||||
├── assets/ # Static assets
|
||||
└── styles/ # Global styles
|
||||
\`\`\`
|
||||
```
|
||||
├── src/ # Frontend React application
|
||||
│ ├── components/ # UI components
|
||||
│ ├── pages/ # Route pages
|
||||
│ ├── api/ # API endpoints
|
||||
│ └── lib/ # Utilities
|
||||
├── public/ # Static assets
|
||||
│ ├── images/ # Images and thumbnails
|
||||
│ ├── subtitles/ # WebVTT subtitle files
|
||||
│ ├── transcriptions/ # Text transcripts
|
||||
│ └── videos/ # Video files (add manually)
|
||||
├── docker-compose.yml # Production orchestration
|
||||
├── Dockerfile # Frontend container
|
||||
├── Dockerfile.api # Backend container
|
||||
├── default.conf # Nginx configuration
|
||||
└── server.ts # API server
|
||||
```
|
||||
|
||||
## 🎯 Core Features
|
||||
## 🔧 Features
|
||||
|
||||
- ⚡️ Intuitive user interface with responsive design
|
||||
- 🗺️ Interactive USA region map displaying network and relationships
|
||||
- 🎥 Video resources with accessible playback controls and captions
|
||||
- 📱 Mobile-first responsive design
|
||||
- 🔒 Secure contact system
|
||||
- 🌐 Key sections: Home, Services, About, Resources, and Contact
|
||||
### Core Functionality
|
||||
- ✅ Responsive design (mobile/tablet/desktop)
|
||||
- ✅ Accessibility compliance (WCAG 2.1 AA)
|
||||
- ✅ Video player with WebVTT subtitles
|
||||
- ✅ Email subscription system
|
||||
- ✅ Contact form with notifications
|
||||
- ✅ Rate limiting and security
|
||||
|
||||
## 🔧 Development
|
||||
### Video Library
|
||||
- 6 professional development videos
|
||||
- Full transcriptions available
|
||||
- Copy/share functionality
|
||||
- Modal video player
|
||||
- Thumbnail previews
|
||||
|
||||
### Environment Setup
|
||||
- Development: Vite dev server with hot reload
|
||||
- Production: Nginx configuration with optimized cache settings
|
||||
- Docker containers for consistent deployment
|
||||
### Technical Features
|
||||
- Docker containerization
|
||||
- Redis caching
|
||||
- Email notifications (Gmail SMTP)
|
||||
- TypeScript throughout
|
||||
- Security audited (0 vulnerabilities)
|
||||
|
||||
### Scripts
|
||||
\`\`\`bash
|
||||
pnpm dev # Start development server
|
||||
pnpm build # Build for production
|
||||
pnpm preview # Preview production build
|
||||
pnpm lint # Run ESLint
|
||||
pnpm test # Run tests
|
||||
\`\`\`
|
||||
## 🛠️ Development
|
||||
|
||||
## 🚢 Production Deployment
|
||||
### Local Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
\`\`\`bash
|
||||
docker-compose up --build
|
||||
\`\`\`
|
||||
# Start development servers
|
||||
pnpm dev
|
||||
|
||||
Application will be served on port 80.
|
||||
# Frontend: http://localhost:5173
|
||||
# Backend: http://localhost:804
|
||||
```
|
||||
|
||||
## <20><> Contributing
|
||||
### Available Scripts
|
||||
- `pnpm dev` - Start development servers
|
||||
- `pnpm build` - Build for production
|
||||
- `pnpm preview` - Preview production build
|
||||
- `pnpm lint` - Run ESLint
|
||||
|
||||
We welcome contributions that align with our mission of improving accessibility and empowerment for the Deaf community. Follow our established patterns for:
|
||||
## 🔒 Security
|
||||
|
||||
- Component architecture
|
||||
- Styling conventions (Tailwind CSS)
|
||||
- Accessibility standards (WCAG 2.1)
|
||||
- TypeScript typing
|
||||
- Git commit messages
|
||||
- All dependencies updated to latest secure versions
|
||||
- Environment variables properly excluded from git
|
||||
- Rate limiting on API endpoints
|
||||
- Input validation and sanitization
|
||||
- CORS configuration
|
||||
|
||||
## 📄 License
|
||||
## 📝 Environment Variables
|
||||
|
||||
Proprietary - DeafGain LLC
|
||||
Required for production deployment:
|
||||
|
||||
## 📞 Contact
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `NODE_ENV` | Environment mode | `production` |
|
||||
| `GMAIL_USER` | Gmail SMTP username | `user@gmail.com` |
|
||||
| `GMAIL_PASS` | Gmail app password | `your-app-password` |
|
||||
| `REDIS_URL` | Redis connection URL | `redis://localhost:6379` |
|
||||
| `REDIS_TOKEN` | Redis auth token | `your-redis-token` |
|
||||
| `API_URL` | Backend API URL | `http://localhost:804` |
|
||||
|
||||
For questions or support, reach out through our [contact form](https://deafgain.com/contact).
|
||||
## 🎯 Post-Deployment
|
||||
|
||||
1. **Add Video Files:** Upload `.mp4` files to `public/videos/` directory
|
||||
2. **Test Functionality:** Verify all features work correctly
|
||||
3. **Monitor Logs:** Check container logs for any issues
|
||||
4. **SSL Certificate:** Configure HTTPS if needed
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For technical support or questions about deployment, refer to the project documentation or contact the development team.
|
||||
|
||||
---
|
||||
|
||||
> This website represents DeafGain LLC's commitment to creating accessible, user-friendly digital experiences while maintaining high technical standards and professional quality.
|
||||
**Version:** Production Ready
|
||||
**Last Updated:** January 2025
|
||||
**Security Status:** ✅ All vulnerabilities resolved
|
||||
|
|
|
|||
49
Tech_Stack
49
Tech_Stack
|
|
@ -1,27 +1,34 @@
|
|||
## Tech Stack
|
||||
|
||||
### Frontend
|
||||
- React 18
|
||||
- TypeScript 4.9
|
||||
- React Router DOM 6
|
||||
- Tailwind CSS 3.3
|
||||
- Framer Motion 10
|
||||
- React Icons 4.11
|
||||
- React Simple Maps 3.0
|
||||
- React Intersection Observer 9.5
|
||||
- React 19
|
||||
- TypeScript 5.7
|
||||
- React Router DOM 7
|
||||
- Tailwind CSS 3.4
|
||||
- Framer Motion 11
|
||||
- React Icons 5.4
|
||||
- React Simple Maps 4.0 (beta)
|
||||
- React Intersection Observer 9.13
|
||||
- Next.js 14.1
|
||||
|
||||
### Backend
|
||||
- Node.js with Express
|
||||
- Node.js with Express 4.21
|
||||
- TypeScript
|
||||
- Nodemailer for email handling
|
||||
- Nodemailer 6.9 for email handling
|
||||
- CORS for cross-origin resource sharing
|
||||
- dotenv for environment variable management
|
||||
- dotenv 16.4 for environment variable management
|
||||
- Upstash Redis 1.28 for caching/rate limiting
|
||||
|
||||
### Development & Build Tools
|
||||
- Docker & Docker Compose
|
||||
- PostCSS
|
||||
- Autoprefixer
|
||||
- npm
|
||||
- PostCSS 8.4
|
||||
- Autoprefixer 10.4
|
||||
- pnpm 9.14 (package manager)
|
||||
- Vite 6.0
|
||||
- ESLint 9
|
||||
- Concurrently for running multiple processes
|
||||
- tsx/ts-node for TypeScript execution
|
||||
- Hot module replacement (HMR)
|
||||
|
||||
### Production
|
||||
- Nginx (Production server for frontend)
|
||||
|
|
@ -33,25 +40,37 @@
|
|||
- Containerized development and production environments
|
||||
- REST API for contact form handling
|
||||
- Gmail SMTP for email delivery
|
||||
- Rate limiting implementation
|
||||
- Context-based state management
|
||||
- Redis-backed caching layer
|
||||
|
||||
### Key Features
|
||||
- Responsive design
|
||||
- Animated UI components
|
||||
- Animated UI components with Framer Motion
|
||||
- Interactive USA region map
|
||||
- Contact form with email notifications
|
||||
- Timeline visualization
|
||||
- Service showcase
|
||||
- Biography section
|
||||
- Testimonials section
|
||||
- Video playback with WebVTT subtitles
|
||||
- Transcription support
|
||||
- Toast notification system
|
||||
- Snow peaks animation effect
|
||||
- Custom video player component
|
||||
|
||||
### Development Environment
|
||||
- Multi-container setup with Docker Compose
|
||||
- Hot-reloading enabled
|
||||
- Separate development and production builds
|
||||
- Environment variable management for sensitive data
|
||||
- TypeScript configuration for both client and server
|
||||
- Concurrent development server (frontend + backend)
|
||||
|
||||
### Production Environment
|
||||
- Nginx serving static frontend files
|
||||
- Node.js backend API
|
||||
- Containerized deployment
|
||||
- Secure email handling with Google App credentials
|
||||
- Multi-stage Docker builds
|
||||
- Production-optimized builds with tree-shaking
|
||||
|
|
|
|||
362
cline_docs/activeContext.md
Normal file
362
cline_docs/activeContext.md
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
# Active Context - Current Work Status
|
||||
|
||||
## Production Deployment Success 🚀
|
||||
**Date: 5/27/2025 - FINAL STATUS**
|
||||
|
||||
### ✅ FULL PRODUCTION DEPLOYMENT - COMPLETED
|
||||
Successfully deployed complete DeafGain website to production server with all functionality working:
|
||||
|
||||
**Production Environment:**
|
||||
- **URL**: https://deafgain.org/ ✅ LIVE
|
||||
- **Server**: 10.4.0.206 behind Caddy reverse proxy
|
||||
- **All 6 videos displaying correctly** ✅
|
||||
- **Email subscription system working** ✅
|
||||
- **Logging enabled for statistics** ✅
|
||||
|
||||
**Final Deployment Process:**
|
||||
1. **Docker Cache Issues Resolved**: Forced fresh rebuild by removing cached images
|
||||
2. **Missing Content Fixed**: All governance documents assets now included
|
||||
3. **Email Configuration Fixed**: Environment variables properly mapped to .env file
|
||||
4. **Production Verified**: Website fully functional at https://deafgain.org/
|
||||
|
||||
**Email System Status:**
|
||||
- **Sender**: system@deafgain.org
|
||||
- **Recipient**: eliza@deafgain.org
|
||||
- **SMTP**: Gmail (smtp.gmail.com:587) with app password authentication
|
||||
- **Test Result**: ✅ Subscription notifications working correctly
|
||||
|
||||
**Current Production Status**: 🟢 **FULLY OPERATIONAL**
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 5/27/2025**
|
||||
|
||||
### ✅ Docker Production Configuration - COMPLETED
|
||||
Successfully configured Docker setup for production deployment behind Caddy reverse proxy:
|
||||
|
||||
**Key Changes Made:**
|
||||
- **Server Analysis**: SSH'd to production server (10.4.0.206) to verify compatibility
|
||||
- **Network Integration**: Configured containers for `caddy_network` (172.22.0.0/16)
|
||||
- **Container Naming**: Set proper container names to match Caddy expectations:
|
||||
- `deafgain-website-web-1` (port 804)
|
||||
- `deafgain-website-api-1` (port 3000)
|
||||
- `deafgain-website-redis-1`
|
||||
- **Volume Mounting**: Added video content volume mapping `/docker/websites/deafgain/video_content:/usr/share/nginx/html/videos`
|
||||
- **API Routing**: Updated nginx config for `/api/contact` and `/api/subscribe` endpoints
|
||||
- **Frontend Updates**: Modified Resources.tsx to use `/api/subscribe` endpoint
|
||||
|
||||
**Files Updated:**
|
||||
- `Dockerfile` - Cleaned up and optimized for production
|
||||
- `docker-compose.yml` - Added external caddy_network, proper container names, volume mounts
|
||||
- `default.conf` - Fixed nginx routing for API and video serving
|
||||
- `src/pages/Resources.tsx` - Updated API endpoints
|
||||
|
||||
**Production Ready Status:** ✅ All configurations verified compatible with existing server infrastructure
|
||||
|
||||
### ✅ Governance Documents Video Integration - COMPLETED
|
||||
Successfully added the new "Governance Documents: Understanding the Hierarchy" video to the DeafGain website:
|
||||
|
||||
**Files Created/Updated:**
|
||||
- `public/subtitles/governance-documents.vtt` - WebVTT subtitle file with proper timing codes
|
||||
- `public/transcriptions/governance-documents.txt` - Full transcription with scene descriptions
|
||||
- `src/pages/Resources.tsx` - Updated to include the new video as first in the Professional Development & Governance category
|
||||
|
||||
**Video Content:**
|
||||
- Source: Converted from `new.ass` file (ASS subtitle format)
|
||||
- Features Eliza Kragh and Mark Apodaca discussing governance document hierarchy
|
||||
- Topics: State laws, Articles of Incorporation, Bylaws, Special Rules of Order, Standing Rules, Customs
|
||||
- Duration: ~61 minutes
|
||||
- All dialogue preserved without modifications
|
||||
- Added appropriate scene descriptions for context
|
||||
|
||||
**Integration Details:**
|
||||
- Video ID: `governance-documents`
|
||||
- Title: "Governance Documents: Understanding the Hierarchy"
|
||||
- Description: Expert explanation of complete governance document hierarchy for non-profit organizations
|
||||
- Thumbnail: `/images/thumbnails/governance-documents.png` (user added)
|
||||
- Video file location: `/videos/governance-documents.mp4` (user will add)
|
||||
- Full feature parity with existing videos (subtitles, transcripts, sharing, modal display)
|
||||
|
||||
## Current System Status
|
||||
|
||||
### ✅ Production Docker Configuration - COMPLETED
|
||||
All Docker production files are configured and ready:
|
||||
- **Nginx (`default.conf`)**: Routes `/subscribe` and `/contact` endpoints to API server
|
||||
- **API Dockerfile (`Dockerfile.api`)**: Includes TypeScript declarations, properly configured
|
||||
- **Main Docker Compose**: Multi-container setup with Redis, email, rate limiting
|
||||
- **Environment Variables**: Gmail SMTP configured, all secrets properly handled
|
||||
|
||||
### ✅ Subscription System - COMPLETED
|
||||
Email subscription system fully operational:
|
||||
- **Frontend**: Resources page subscription form (updated to use `/subscribe` endpoint)
|
||||
- **Backend**: Node.js API with rate limiting, email validation, Redis storage
|
||||
- **Email**: Gmail SMTP integration with professional templates
|
||||
- **Security**: Rate limiting, input validation, error handling
|
||||
|
||||
### ✅ Video Resource Library - EXPANDED
|
||||
Currently contains 6 videos in Professional Development & Governance category:
|
||||
1. **governance-documents** - NEW - Understanding governance document hierarchy
|
||||
2. secretary-role-responsibilities - Secretary duties and responsibilities
|
||||
3. minutes-approval-procedure - Meeting minutes approval process
|
||||
4. board-participation-guidelines - Board meeting participation protocols
|
||||
5. board-member-reprimands - Due process for board member issues
|
||||
6. meeting-minutes-access-rights - Member rights to access meeting minutes
|
||||
|
||||
All videos have:
|
||||
- WebVTT subtitles for accessibility
|
||||
- Full transcriptions with scene descriptions
|
||||
- Thumbnail images
|
||||
- Modal video player
|
||||
- Share/copy link functionality
|
||||
- Responsive design for mobile/tablet/desktop
|
||||
|
||||
## File Structure Status
|
||||
|
||||
### ✅ Public Assets Complete
|
||||
```
|
||||
public/
|
||||
├── images/thumbnails/
|
||||
│ ├── governance-documents.png ✅
|
||||
│ ├── secretary-role-responsibilities.png ✅
|
||||
│ ├── minutes-approval-procedure.png ✅
|
||||
│ ├── board-participation-guidelines.png ✅
|
||||
│ ├── board-member-reprimands.png ✅
|
||||
│ └── meeting-minutes-access-rights.png ✅
|
||||
├── subtitles/
|
||||
│ ├── governance-documents.vtt ✅ NEW
|
||||
│ ├── secretary-role-responsibilities.vtt ✅
|
||||
│ ├── minutes-approval-procedure.vtt ✅
|
||||
│ ├── board-participation-guidelines.vtt ✅
|
||||
│ ├── board-member-reprimands.vtt ✅
|
||||
│ └── meeting-minutes-access-rights.vtt ✅
|
||||
├── transcriptions/
|
||||
│ ├── governance-documents.txt ✅ NEW
|
||||
│ ├── secretary-role-responsibilities.txt ✅
|
||||
│ ├── minutes-approval-procedure.txt ✅
|
||||
│ ├── board-participation-guidelines.txt ✅
|
||||
│ ├── board-member-reprimands.txt ✅
|
||||
│ └── meeting-minutes-access-rights.txt ✅
|
||||
└── videos/
|
||||
├── governance-documents.mp4 ⏳ (USER TO ADD)
|
||||
└── [other video files exist]
|
||||
```
|
||||
|
||||
## Recently Completed Security Updates
|
||||
**Date: 5/27/2025**
|
||||
|
||||
### ✅ Critical Security Vulnerabilities Fixed - COMPLETED
|
||||
Successfully resolved all 14 security vulnerabilities found during audit:
|
||||
|
||||
**Major Updates:**
|
||||
- **Next.js**: 14.2.20 → 15.3.2 (Fixed critical authorization bypass)
|
||||
- **React Router**: 7.0.2 → 7.6.1 (Fixed high severity data spoofing)
|
||||
- **Vite**: 6.0.3 → 6.3.5 (Fixed multiple moderate server.fs.deny bypasses)
|
||||
- **@vitejs/plugin-react**: 4.3.4 → 4.5.0
|
||||
- **tsx**: 4.19.2 → 4.19.4 (Fixed esbuild vulnerability)
|
||||
- **concurrently**: 8.2.2 → 9.1.2 (Fixed Babel runtime issues)
|
||||
|
||||
**Security Status:** ✅ **ALL VULNERABILITIES RESOLVED** - `pnpm audit` shows "No known vulnerabilities found"
|
||||
|
||||
### ✅ Thumbnail Issue Fixed - COMPLETED
|
||||
Fixed governance documents thumbnail loading by correcting filename:
|
||||
- Renamed `goverance-documents.png` → `governance-documents.png`
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 6/15/2025**
|
||||
|
||||
### ✅ Interactive Map Updates - COMPLETED
|
||||
Successfully updated the Services page interactive map with 7 new 2025 conference locations:
|
||||
|
||||
**New Locations Added:**
|
||||
1. **Phoenix, AZ** - DANE (Deaf Abuse Network) + NAP (National Association of Parliamentarians)
|
||||
2. **Missoula, MT** - AIP (American Institute of Parliamentarians) Online ⬅️ **UPDATED 6/15/2025 Evening**
|
||||
3. **Wichita, KS** - KAD (Kansas Association of the Deaf)
|
||||
4. **Baton Rouge, LA** - LAD (Louisiana Association of the Deaf)
|
||||
5. **Washington, DC** - ASLTA (American Sign Language Teachers Association)
|
||||
6. **Kansas City, MO** - NBDA (National Black Deaf Advocates)
|
||||
|
||||
**Map Status:** Expanded from 10 to 16 total locations nationwide
|
||||
|
||||
**Files Updated:**
|
||||
- `src/pages/Services.tsx` - Added 6 new RelationshipPoint entries with precise coordinates
|
||||
- All new 2025 events display as green markers (upcoming events)
|
||||
- Maintained full interactive functionality with popup details
|
||||
|
||||
### ✅ Docker Deployment Fix - COMPLETED
|
||||
Resolved Portainer cache issue preventing map updates from deploying:
|
||||
|
||||
**Docker Configuration Updates:**
|
||||
- Added `no_cache: true` to both web and api services in `docker-compose.yml`
|
||||
- Added `pull_policy: build` to force fresh builds
|
||||
- Eliminates Docker layer caching that was preventing code updates
|
||||
|
||||
**Deployment Status:** ✅ **VERIFIED SUCCESSFUL** - Updated map now live at https://deafgain.org/services
|
||||
|
||||
### ✅ Map Location Update - COMPLETED
|
||||
**Date: 6/15/2025 - Evening**
|
||||
|
||||
Updated Montana event location per user request:
|
||||
- **Changed**: Helena, MT → Missoula, MT
|
||||
- **Event**: AIP (American Institute of Parliamentarians) - 2025 Spring Practicum (Online)
|
||||
- **Coordinates Updated**: [-112.0362, 46.5958] → [-113.9940, 46.8721]
|
||||
- **File Modified**: `src/pages/Services.tsx`
|
||||
- **Reason**: User feedback to use Missoula instead of Helena for Montana event
|
||||
|
||||
## Next Steps / Pending Tasks
|
||||
|
||||
### 🔄 Ready for Production Build
|
||||
1. **Docker production build** - All security issues resolved, ready to build
|
||||
2. **User needs to add `governance-documents.mp4`** to `public/videos/` directory (optional for production build)
|
||||
|
||||
### 📋 No Outstanding Technical Work
|
||||
All requested features are complete and secure:
|
||||
- ✅ Docker production deployment ready
|
||||
- ✅ Email subscription system operational
|
||||
- ✅ Video resource library with 6 videos
|
||||
- ✅ WebVTT subtitle conversion completed
|
||||
- ✅ Full transcription with scene descriptions
|
||||
- ✅ Resource page integration complete
|
||||
- ✅ All security vulnerabilities resolved
|
||||
- ✅ Interactive map expanded and location corrected
|
||||
|
||||
### 🎯 System Ready For
|
||||
- Production deployment via `docker-compose up --build -d`
|
||||
- Adding additional videos (same pattern established)
|
||||
- Content management and updates
|
||||
- User testing and feedback collection
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### 🏗️ Current Stack
|
||||
- **Frontend**: React + TypeScript + Vite + Tailwind CSS
|
||||
- **Backend**: Node.js + Express + TypeScript
|
||||
- **Database**: Redis for caching and rate limiting
|
||||
- **Email**: Gmail SMTP with Nodemailer
|
||||
- **Deployment**: Docker multi-container setup
|
||||
- **Web Server**: Nginx reverse proxy
|
||||
- **Video**: HTML5 video player with WebVTT subtitles
|
||||
|
||||
### 📁 Key Configuration Files
|
||||
- `docker-compose.yml` - Production orchestration with forced rebuild flags
|
||||
- `default.conf` - Nginx proxy configuration
|
||||
- `src/pages/Resources.tsx` - Video library interface
|
||||
- `src/pages/Services.tsx` - Interactive map with 18 locations
|
||||
- `src/api/subscribe.ts` - Email subscription API
|
||||
- `server.ts` - Main API server
|
||||
- `.env` - Environment variables (Gmail credentials)
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 8/13/2025**
|
||||
|
||||
### ✅ Interactive Map Updates - COMPLETED
|
||||
Successfully added two new 2025 conference locations to the Services page interactive map:
|
||||
|
||||
**New Locations Added:**
|
||||
1. **New Orleans, LA** - LAD (Louisiana Association of the Deaf) - Board Training (September 26-28, 2025)
|
||||
2. **Austin, TX** - NLTC (National Leadership Training Conference) - NAD conference (October 29 - November 1, 2025)
|
||||
|
||||
**Map Status:** Expanded from 16 to 18 total locations nationwide
|
||||
|
||||
**Files Updated:**
|
||||
- `src/pages/Services.tsx` - Added 2 new RelationshipPoint entries with precise coordinates
|
||||
- All new 2025 events display as green markers (upcoming events)
|
||||
- Maintained full interactive functionality with popup details
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 8/28/2025**
|
||||
|
||||
### ✅ Services Page Update - COMPLETED
|
||||
Successfully removed ADA-Compliant Transcripts service from the Services page per user request:
|
||||
|
||||
**Changes Made:**
|
||||
- Removed "ADA-Compliant Transcripts" service entry from services array
|
||||
- Cleaned up unused `FaFileAlt` import
|
||||
- Services page now displays 3 services instead of 4:
|
||||
1. Universal Website Design
|
||||
2. Board Communication
|
||||
3. Training & Workshops
|
||||
|
||||
**Reason for Change:** ADA is no longer offering transcribing services
|
||||
|
||||
**Files Updated:**
|
||||
- `src/pages/Services.tsx` - Removed ADA transcribing service and cleaned imports
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 1/9/2026**
|
||||
|
||||
### ✅ Email Configuration Fix - COMPLETED
|
||||
Successfully resolved Gmail authentication issue and updated email configuration:
|
||||
|
||||
**Root Cause Identified:**
|
||||
- Google password was changed on January 8, 2026
|
||||
- This automatically revoked all existing app passwords for security
|
||||
- Old app password (ojvlysraxwjriwzy) was no longer valid
|
||||
|
||||
**Changes Made:**
|
||||
- **SMTP Port Corrected**: Changed from 2525 to 587 (standard Gmail SMTP port with STARTTLS)
|
||||
- **App Password Updated**: Generated new Gmail app password (idjcxxwzvloorfuk)
|
||||
- **Configuration Tested**: Verified with test email to chris@sigd.net
|
||||
|
||||
**Current Email Configuration:**
|
||||
- **SMTP Host**: smtp.gmail.com
|
||||
- **SMTP Port**: 587 (standard port with STARTTLS)
|
||||
- **SMTP Secure**: false (uses STARTTLS)
|
||||
- **Sender**: system@deafgain.org
|
||||
- **Recipient**: eliza@deafgain.org
|
||||
- **App Password**: idjcxxwzvloorfuk (generated 1/9/2026)
|
||||
- **Status**: ✅ **FULLY OPERATIONAL** - Test email sent successfully
|
||||
|
||||
**Test Results:**
|
||||
```
|
||||
✅ SMTP connection verified successfully!
|
||||
✅ Test email sent successfully!
|
||||
Message ID: <99a88457-6604-ca19-a17c-60daa7a317ae@deafgain.org>
|
||||
Response: 250 2.0.0 OK 1767992239 41be03b00d2f7-c4cc96ca7a9sm11346262a12.25 - gsmtp
|
||||
```
|
||||
|
||||
**Rate Limiting Status:**
|
||||
- **Redis**: ⚠️ Temporarily disabled (commented out in .env)
|
||||
- **Fallback**: Rate limiting code allows all requests when Redis unavailable
|
||||
- **Impact**: Contact and subscription forms will work without rate limiting protection
|
||||
|
||||
**Files Modified:**
|
||||
- `.env` - Updated SMTP_PORT=587, updated GOOGLE_APP_PASSWORD, Redis still commented out
|
||||
|
||||
## Recently Completed Work
|
||||
**Date: 1/20/2026**
|
||||
|
||||
### ✅ Anti-Spam Implementation - COMPLETED
|
||||
Successfully implemented honeypot field and updated service dropdown to reduce contact form spam:
|
||||
|
||||
**Changes Made:**
|
||||
1. **Honeypot Field Added** (Layer 4):
|
||||
- Added hidden "website" field to contact form
|
||||
- Field positioned off-screen with CSS (absolute left-[-5000px], opacity-0)
|
||||
- Backend validation: silently rejects submissions if honeypot is filled
|
||||
- Logs bot attempts to console for monitoring
|
||||
- Zero impact on legitimate users
|
||||
|
||||
2. **Service Dropdown Updated** (Layer 6):
|
||||
- Removed "ADA-Compliant Transcripts" option (service discontinued)
|
||||
- Updated to match current Services page offerings:
|
||||
- Universal Website Design
|
||||
- Board Communication
|
||||
- Training & Workshops
|
||||
|
||||
**Files Modified:**
|
||||
- `src/pages/Contact.tsx` - Added honeypot field, removed outdated service option
|
||||
- `src/api/contact.ts` - Added honeypot validation logic
|
||||
|
||||
**Expected Results:**
|
||||
- 60-80% reduction in bot spam submissions
|
||||
- Bots that auto-fill forms will be caught by honeypot
|
||||
- Legitimate users unaffected (field is invisible and inaccessible)
|
||||
- Bot submissions logged but not emailed
|
||||
|
||||
**Anti-Spam Strategy:**
|
||||
- Honeypot catches simple bots that auto-fill all form fields
|
||||
- Returns "success" to bots to avoid detection
|
||||
- Actual email only sent for legitimate submissions
|
||||
- Console logging allows monitoring of blocked attempts
|
||||
|
||||
## Status Summary
|
||||
🟢 **SYSTEM FULLY OPERATIONAL** - Email configuration fixed with standard SMTP port 587 and new Gmail app password (generated 1/9/2026). Email system tested and working correctly. Redis temporarily disabled (rate limiting inactive). Anti-spam honeypot field implemented to reduce bot submissions. Services page updated to reflect current service offerings. Interactive map successfully expanded with additional 2025 conference locations in New Orleans and Austin. Production website live at https://deafgain.org/ and ready for continued use.
|
||||
497
cline_docs/gitWorkflow.md
Normal file
497
cline_docs/gitWorkflow.md
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
# Git Workflow & Memory Bank
|
||||
|
||||
## Git Configuration
|
||||
|
||||
### Repository Information
|
||||
- **Repository URL**: `https://gitea.sigd.net/chaulmark/deafgain-website`
|
||||
- **Gitea Server**: gitea.sigd.net (private Gitea instance)
|
||||
- **Username**: `eliza`
|
||||
- **Authentication**: Personal Access Token
|
||||
- **Current Token**: `3dba996c9eca5b6267f7cd1b0996a94aac1ac0d5` (created 3/5/2026)
|
||||
- **Local Path**: `/Users/Eliza/DeafGain-website`
|
||||
- **Main Branch**: `main`
|
||||
- **Latest Commit**: `78aade0c94a4bad05e5b8ff95d025e0b68e17336`
|
||||
|
||||
### Token Permissions
|
||||
The access token has been configured with:
|
||||
- ✅ Read repository access
|
||||
- ✅ Write repository access (for pushing changes)
|
||||
|
||||
### Git Remote Configuration
|
||||
```bash
|
||||
# Remote is already configured:
|
||||
origin: https://eliza:3dba996c9eca5b6267f7cd1b0996a94aac1ac0d5@gitea.sigd.net/chaulmark/deafgain-website.git
|
||||
```
|
||||
|
||||
## Common Git Workflows
|
||||
|
||||
### 1. Checking Repository Status
|
||||
```bash
|
||||
# Check current status
|
||||
git status
|
||||
|
||||
# View current branch
|
||||
git branch
|
||||
|
||||
# View remote branches
|
||||
git branch -r
|
||||
```
|
||||
|
||||
### 2. Pulling Latest Changes
|
||||
```bash
|
||||
# Pull from main branch (recommended before starting work)
|
||||
git pull origin main
|
||||
|
||||
# Or simply
|
||||
git pull
|
||||
```
|
||||
|
||||
### 3. Viewing Changes History
|
||||
```bash
|
||||
# View recent commits (short format)
|
||||
git log --oneline --graph --all -15
|
||||
|
||||
# View specific commit details
|
||||
git show HEAD
|
||||
git show <commit-hash>
|
||||
|
||||
# View changes between commits
|
||||
git diff HEAD~1 HEAD
|
||||
git diff <commit-hash1> <commit-hash2>
|
||||
|
||||
# View files changed in a commit
|
||||
git show --stat HEAD
|
||||
```
|
||||
|
||||
### 4. Making Changes
|
||||
```bash
|
||||
# After editing files, check what changed
|
||||
git status
|
||||
git diff
|
||||
|
||||
# Stage all changes
|
||||
git add .
|
||||
|
||||
# Or stage specific files
|
||||
git add src/pages/Services.tsx
|
||||
|
||||
# Commit changes with meaningful message
|
||||
git commit -m "Add new feature: Interactive map updates"
|
||||
|
||||
# Push to remote repository
|
||||
git push origin main
|
||||
# Or simply
|
||||
git push
|
||||
```
|
||||
|
||||
### 5. Viewing File History
|
||||
```bash
|
||||
# View commit history for specific file
|
||||
git log --oneline src/pages/Services.tsx
|
||||
|
||||
# View changes to a file over time
|
||||
git log -p src/pages/Services.tsx
|
||||
|
||||
# Who changed what in a file
|
||||
git blame src/pages/Services.tsx
|
||||
```
|
||||
|
||||
### 6. Undoing Changes
|
||||
```bash
|
||||
# Discard changes to a file (before staging)
|
||||
git checkout -- src/pages/Services.tsx
|
||||
|
||||
# Unstage a file (keep changes)
|
||||
git reset HEAD src/pages/Services.tsx
|
||||
|
||||
# Undo last commit (keep changes)
|
||||
git reset --soft HEAD~1
|
||||
|
||||
# Undo last commit (discard changes - DANGEROUS)
|
||||
git reset --hard HEAD~1
|
||||
```
|
||||
|
||||
### 7. Working with Branches
|
||||
```bash
|
||||
# Create new branch
|
||||
git checkout -b feature/new-feature
|
||||
|
||||
# Switch branches
|
||||
git checkout main
|
||||
|
||||
# List all branches
|
||||
git branch -a
|
||||
|
||||
# Delete branch
|
||||
git branch -d feature/old-feature
|
||||
|
||||
# Push branch to remote
|
||||
git push origin feature/new-feature
|
||||
```
|
||||
|
||||
## Recent Commit History
|
||||
|
||||
### Latest Changes (as of 3/5/2026)
|
||||
```
|
||||
78aade0 (HEAD -> main) - Update interactive map: Add RID conference to Missoula MT and update ASLTA description
|
||||
4755cd3 - Consolidate services into single DeafGain Consultants offering
|
||||
a565932 - Remove service selection field from contact form
|
||||
ae7c52d - Add anti-spam honeypot field and update contact form service options
|
||||
a0cf3e9 - Update memory bank status summary with current system state
|
||||
df6c696 - Fix email configuration: Update SMTP port to 587 and regenerate Gmail app password
|
||||
379c4e1 - Add Parliamentarian service and update Austin TX event
|
||||
8b2b421 - Update email configuration: change SMTP port to 2525
|
||||
5d49c9e - Remove ADA-compliant transcripts service
|
||||
67c775a - Add two locations
|
||||
86096d9 - Switch to Missoula from Helena
|
||||
b1e7d29 - Add rebuild flags
|
||||
b214427 - Added geographic locations for upcoming events
|
||||
```
|
||||
|
||||
## Git Authentication Troubleshooting
|
||||
|
||||
### If Access Token Expires or Needs Regeneration
|
||||
|
||||
1. **Generate New Token in Gitea:**
|
||||
- Log into https://gitea.sigd.net
|
||||
- Settings → Applications → Access Tokens
|
||||
- Delete old token
|
||||
- Generate new token with repository permissions
|
||||
- Copy the new token immediately
|
||||
|
||||
2. **Update Git Remote:**
|
||||
```bash
|
||||
# Remove old remote
|
||||
git remote remove origin
|
||||
|
||||
# Add new remote with new token
|
||||
git remote add origin https://eliza:NEW_TOKEN_HERE@gitea.sigd.net/chaulmark/deafgain-website.git
|
||||
|
||||
# Verify remote
|
||||
git remote -v
|
||||
```
|
||||
|
||||
3. **Test Connection:**
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
### Common Authentication Issues
|
||||
|
||||
**Issue**: 403 Forbidden error
|
||||
- **Cause**: Token doesn't have proper permissions or has expired
|
||||
- **Solution**: Generate new token with `read:repository` and `write:repository` permissions
|
||||
|
||||
**Issue**: Username incorrect
|
||||
- **Cause**: Using wrong Gitea username
|
||||
- **Solution**: Username is `eliza` (not email address `eliza@deafgain.org`)
|
||||
|
||||
**Issue**: Token doesn't work
|
||||
- **Cause**: Token permissions not properly configured
|
||||
- **Solution**: Ensure both read and write repository permissions are enabled
|
||||
|
||||
## DeafGain Website Codebase Overview
|
||||
|
||||
### Project Architecture
|
||||
|
||||
```
|
||||
DeafGain-website/
|
||||
├── Frontend (React + TypeScript)
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # Main page components
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── api/ # API integration layer
|
||||
│ │ ├── context/ # React context providers
|
||||
│ │ └── lib/ # Utility functions
|
||||
│ └── public/ # Static assets
|
||||
│ ├── videos/ # Video files (.mp4)
|
||||
│ ├── subtitles/ # WebVTT subtitle files
|
||||
│ ├── transcriptions/ # Text transcripts
|
||||
│ └── images/ # Images and thumbnails
|
||||
│
|
||||
├── Backend (Node.js + Express)
|
||||
│ └── server.ts # API server
|
||||
│
|
||||
├── Docker Configuration
|
||||
│ ├── docker-compose.yml # Production orchestration
|
||||
│ ├── docker-compose.dev.yml # Development environment
|
||||
│ ├── Dockerfile # Frontend container
|
||||
│ ├── Dockerfile.api # Backend container
|
||||
│ ├── Dockerfile.dev # Dev frontend container
|
||||
│ └── Dockerfile.dev.api # Dev backend container
|
||||
│
|
||||
└── Configuration Files
|
||||
├── vite.config.ts # Vite build configuration
|
||||
├── tailwind.config.js # Tailwind CSS config
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── nginx.conf # Nginx configuration
|
||||
├── default.conf # Nginx reverse proxy
|
||||
└── .env # Environment variables
|
||||
```
|
||||
|
||||
### Key Files and Their Purposes
|
||||
|
||||
#### Frontend Pages (`src/pages/`)
|
||||
- **Home.tsx**: Landing page with hero section and overview
|
||||
- **About.tsx**: Information about Eliza and DeafGain
|
||||
- **Services.tsx**: Service offerings + interactive US map with conference locations
|
||||
- **Resources.tsx**: Video library with 6 governance training videos
|
||||
- **Contact.tsx**: Contact form with email notifications
|
||||
|
||||
#### Components (`src/components/`)
|
||||
- **Layout/**: Navbar, Footer, Layout wrapper
|
||||
- **VideoPlayer.tsx**: Custom video player with WebVTT subtitle support
|
||||
- **SnowPeaks.tsx**: Animated background graphics
|
||||
- **Toast.tsx**: Notification component
|
||||
|
||||
#### Backend (`server.ts` & `src/api/`)
|
||||
- **server.ts**: Main Express API server
|
||||
- **api/contact.ts**: Contact form endpoint with email sending
|
||||
- **api/subscribe.ts**: Newsletter subscription endpoint
|
||||
- **lib/email.ts**: Email service integration (Gmail SMTP)
|
||||
- **lib/rate-limit.ts**: Rate limiting logic
|
||||
|
||||
### Important Patterns
|
||||
|
||||
#### 1. Video Resource Structure
|
||||
Each video requires 4 files:
|
||||
```
|
||||
public/videos/video-name.mp4 # Video file
|
||||
public/subtitles/video-name.vtt # WebVTT subtitles
|
||||
public/transcriptions/video-name.txt # Full text transcript
|
||||
public/images/thumbnails/video-name.png # Thumbnail image
|
||||
```
|
||||
|
||||
#### 2. Interactive Map Data
|
||||
Location data structure in `Services.tsx`:
|
||||
```typescript
|
||||
const relationshipPoints: RelationshipPoint[] = [
|
||||
{
|
||||
coordinates: [-longitude, latitude],
|
||||
city: "City Name",
|
||||
state: "State",
|
||||
events: [{
|
||||
name: "Organization Acronym",
|
||||
description: "Full event description",
|
||||
startDate: "Month DD, YYYY",
|
||||
endDate: "Month DD, YYYY"
|
||||
}]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 3. Environment Variables
|
||||
Critical `.env` variables:
|
||||
```env
|
||||
NODE_ENV=production
|
||||
GMAIL_USER=system@deafgain.org
|
||||
GMAIL_PASS=gmail_app_password
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
REDIS_URL=redis://localhost:6379
|
||||
API_URL=http://localhost:804
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
#### Making Changes to the Website
|
||||
|
||||
1. **Pull latest changes**
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
2. **Make your changes**
|
||||
- Edit files as needed
|
||||
- Test locally with `pnpm dev`
|
||||
|
||||
3. **Check what changed**
|
||||
```bash
|
||||
git status
|
||||
git diff
|
||||
```
|
||||
|
||||
4. **Stage and commit**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Descriptive message about changes"
|
||||
```
|
||||
|
||||
5. **Push to Gitea**
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
6. **Deploy to production**
|
||||
- Changes are pulled to production server
|
||||
- Docker containers rebuilt with `docker-compose up --build -d`
|
||||
|
||||
#### Adding New Videos
|
||||
|
||||
1. **Prepare files:**
|
||||
- Video: `public/videos/video-name.mp4`
|
||||
- Subtitles: `public/subtitles/video-name.vtt`
|
||||
- Transcript: `public/transcriptions/video-name.txt`
|
||||
- Thumbnail: `public/images/thumbnails/video-name.png`
|
||||
|
||||
2. **Update Resources.tsx:**
|
||||
```typescript
|
||||
{
|
||||
id: 'video-name',
|
||||
title: 'Video Title',
|
||||
thumbnail: '/images/thumbnails/video-name.png',
|
||||
description: 'Description',
|
||||
category: 'Professional Development & Governance'
|
||||
}
|
||||
```
|
||||
|
||||
3. **Commit and push:**
|
||||
```bash
|
||||
git add public/videos/ public/subtitles/ public/transcriptions/ public/images/thumbnails/ src/pages/Resources.tsx
|
||||
git commit -m "Add new video: Video Title"
|
||||
git push
|
||||
```
|
||||
|
||||
#### Updating Interactive Map
|
||||
|
||||
1. **Edit Services.tsx**
|
||||
2. **Add new location to relationshipPoints array**
|
||||
3. **Use exact coordinates from Google Maps**
|
||||
4. **Commit and push changes**
|
||||
5. **Docker must rebuild with `no_cache: true` flag**
|
||||
|
||||
### Production Deployment
|
||||
|
||||
#### Current Production Setup
|
||||
- **URL**: https://deafgain.org/
|
||||
- **Server**: 10.4.0.206 (behind Caddy reverse proxy)
|
||||
- **Network**: caddy_network (172.22.0.0/16)
|
||||
- **Containers**:
|
||||
- deafgain-website-web-1 (port 804)
|
||||
- deafgain-website-api-1 (port 3000)
|
||||
- deafgain-website-redis-1
|
||||
|
||||
#### Deployment Process
|
||||
```bash
|
||||
# On production server
|
||||
cd /docker/websites/deafgain/
|
||||
git pull
|
||||
docker-compose up --build -d
|
||||
docker-compose logs -f # Monitor logs
|
||||
```
|
||||
|
||||
### Common Tasks Reference
|
||||
|
||||
#### View Recent Changes
|
||||
```bash
|
||||
git log --oneline --graph --all -15
|
||||
```
|
||||
|
||||
#### See What Files Changed in Last Commit
|
||||
```bash
|
||||
git show --stat HEAD
|
||||
```
|
||||
|
||||
#### See Full Changes in Last Commit
|
||||
```bash
|
||||
git show HEAD
|
||||
```
|
||||
|
||||
#### Compare Two Commits
|
||||
```bash
|
||||
git diff abc123 def456
|
||||
```
|
||||
|
||||
#### Search Commit History
|
||||
```bash
|
||||
# Find commits by message
|
||||
git log --grep="map"
|
||||
|
||||
# Find commits that changed a specific file
|
||||
git log -- src/pages/Services.tsx
|
||||
|
||||
# Find commits by author
|
||||
git log --author="eliza"
|
||||
```
|
||||
|
||||
#### Undo Last Commit (Keep Changes)
|
||||
```bash
|
||||
git reset --soft HEAD~1
|
||||
```
|
||||
|
||||
#### Discard All Local Changes
|
||||
```bash
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Commit Messages
|
||||
- Use descriptive, clear messages
|
||||
- Start with a verb (Add, Update, Fix, Remove)
|
||||
- Be specific about what changed
|
||||
- Examples:
|
||||
- ✅ "Add RID conference to Missoula MT on interactive map"
|
||||
- ✅ "Fix email SMTP configuration and update app password"
|
||||
- ❌ "Updates"
|
||||
- ❌ "Fixed stuff"
|
||||
|
||||
### Before Making Changes
|
||||
1. Always `git pull` first
|
||||
2. Check current status with `git status`
|
||||
3. Review what you're about to commit with `git diff`
|
||||
|
||||
### After Making Changes
|
||||
1. Test locally if possible
|
||||
2. Review changes with `git status` and `git diff`
|
||||
3. Commit with clear message
|
||||
4. Push to remote
|
||||
5. Verify on production if critical
|
||||
|
||||
### Working with Production
|
||||
- Test changes locally before pushing
|
||||
- Be cautious with Docker configuration changes
|
||||
- Monitor logs after deployment
|
||||
- Keep `.env` file secure (never commit)
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never commit `.env` file** - contains sensitive credentials
|
||||
- **Keep access token secure** - treat like a password
|
||||
- **Regenerate token if exposed** - create new one immediately
|
||||
- **Use app-specific passwords** - for Gmail integration
|
||||
- **Review changes before pushing** - especially configuration files
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Pull latest changes | `git pull` |
|
||||
| Check status | `git status` |
|
||||
| View recent commits | `git log --oneline -15` |
|
||||
| View last commit | `git show HEAD` |
|
||||
| Stage all changes | `git add .` |
|
||||
| Commit changes | `git commit -m "message"` |
|
||||
| Push to remote | `git push` |
|
||||
| Undo last commit (keep changes) | `git reset --soft HEAD~1` |
|
||||
| Discard local changes | `git reset --hard HEAD` |
|
||||
| View file history | `git log -- path/to/file` |
|
||||
| Search commits | `git log --grep="keyword"` |
|
||||
|
||||
## Useful Git Aliases
|
||||
|
||||
Add these to your `.gitconfig` for shortcuts:
|
||||
```bash
|
||||
git config --global alias.st status
|
||||
git config --global alias.co checkout
|
||||
git config --global alias.br branch
|
||||
git config --global alias.ci commit
|
||||
git config --global alias.lg "log --oneline --graph --all"
|
||||
git config --global alias.last "show HEAD"
|
||||
```
|
||||
|
||||
Then use:
|
||||
- `git st` instead of `git status`
|
||||
- `git lg` instead of `git log --oneline --graph --all`
|
||||
- `git last` instead of `git show HEAD`
|
||||
422
cline_docs/memoryBank.md
Normal file
422
cline_docs/memoryBank.md
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
|
||||
# DeafGain Website - AI Assistant Memory Bank
|
||||
|
||||
## IMPORTANT: User Profile & Expectations
|
||||
|
||||
**USER: Eliza Kragh**
|
||||
- **Experience Level**: Novice web developer, NOT interested in learning web development
|
||||
- **Role**: Project owner and content provider
|
||||
- **Expectation**: AI assistant (Cline) handles ALL technical work
|
||||
- **User Interaction**: Provides content, approves changes, makes high-level requests only
|
||||
- **NO USER EDUCATION**: Do not teach or explain technical concepts unless specifically asked
|
||||
|
||||
## AI Assistant Responsibilities
|
||||
|
||||
### What I (AI) Do Autonomously:
|
||||
1. ✅ **Edit/Create/Delete Files**: All code changes without user intervention
|
||||
2. ✅ **Git Operations**: Pull, commit, push using Eliza's credentials
|
||||
3. ✅ **Local Testing**: Test changes before deployment
|
||||
4. ✅ **Remote Deployment**: SSH to production server and deploy via Docker
|
||||
5. ✅ **Troubleshooting**: Debug and fix issues independently
|
||||
6. ✅ **Documentation**: Update memory bank after significant changes
|
||||
|
||||
### What User Does:
|
||||
1. 📝 **Content Requests**: "Add this video", "Update map with this location"
|
||||
2. ✅ **Approve Actions**: Review and approve proposed changes
|
||||
3. 📦 **Provide Assets**: Supply video files, images, text when needed
|
||||
4. 🎯 **Strategic Decisions**: Final say on design/content direction
|
||||
|
||||
## Critical Credentials & Access
|
||||
|
||||
### Git Access (Eliza's Account)
|
||||
- **Repository**: https://gitea.sigd.net/chaulmark/deafgain-website
|
||||
- **Username**: `eliza` (NOT eliza@deafgain.org)
|
||||
- **Token**: `3dba996c9eca5b6267f7cd1b0996a94aac1ac0d5` (created 3/5/2026)
|
||||
- **Local Path**: `/Users/Eliza/DeafGain-website`
|
||||
- **Remote**: Already configured in repository
|
||||
|
||||
### SSH Production Access (Chaulmark's Account)
|
||||
- **Server**: 10.4.0.205
|
||||
- **SSH User**: `chaulmark`
|
||||
- **Project Path**: `/home/chaulmark/websites/deafgain.org`
|
||||
- **Purpose**: Deploy Docker containers on production server
|
||||
- **VPN Required**: Yes - Automatically handled by `deploy.sh` script
|
||||
|
||||
### Production Environment
|
||||
- **URL**: https://deafgain.org/
|
||||
- **Behind**: Caddy reverse proxy
|
||||
- **Network**: caddy_network (172.22.0.0/16)
|
||||
- **Containers**: web (port 804), api (port 3000), redis
|
||||
|
||||
## AI Workflow for Changes
|
||||
|
||||
### Standard Change Process (I Handle Everything)
|
||||
1. **User Request**: User says "Add location to map" or "Fix email"
|
||||
2. **Pull Latest**: `git pull` to ensure working with latest code
|
||||
3. **Make Changes**: Edit necessary files (no user involvement)
|
||||
4. **Test Locally**: Run `pnpm dev` to verify changes work
|
||||
5. **Commit & Push**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Descriptive message"
|
||||
git push
|
||||
```
|
||||
6. **Deploy to Production** (Automated via deploy.sh):
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
This script automatically:
|
||||
- Connects to VPN via WireGuard
|
||||
- Tests server connectivity
|
||||
- SSHs to production server
|
||||
- Pulls latest changes from Git
|
||||
- Rebuilds and restarts Docker containers
|
||||
7. **Verify**: Check https://deafgain.org/ to confirm changes live
|
||||
8. **Report**: Tell user "Done! Changes are live at deafgain.org"
|
||||
|
||||
### Git Commands I Use (User Never Runs These)
|
||||
```bash
|
||||
# Start every task
|
||||
git pull
|
||||
|
||||
# After making changes
|
||||
git add .
|
||||
git commit -m "Clear description of what changed"
|
||||
git push
|
||||
|
||||
# Check what changed (for my reference)
|
||||
git status
|
||||
git diff
|
||||
git log --oneline -15
|
||||
```
|
||||
|
||||
### Automated Production Deployment
|
||||
The production server is on a private network (10.4.0.0/24) requiring VPN access. All deployment is now automated via `deploy.sh` script.
|
||||
|
||||
**Deployment Script**: `deploy.sh` in project root
|
||||
- Automatically connects VPN via WireGuard
|
||||
- Tests server connectivity
|
||||
- SSHs to production and deploys
|
||||
|
||||
**What deploy.sh does**:
|
||||
1. Checks if VPN is connected, connects if needed
|
||||
2. Tests connectivity to 10.4.0.205
|
||||
3. SSHs to chaulmark@10.4.0.205
|
||||
4. Changes to `/home/chaulmark/websites/deafgain.org`
|
||||
5. Runs `git pull` to get latest code
|
||||
6. Runs `docker compose up --build -d` to rebuild containers
|
||||
7. Reports success
|
||||
|
||||
**When I Need to Deploy**:
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
That's it! The script handles VPN, SSH, Git pull, and Docker rebuild automatically.
|
||||
|
||||
**Manual VPN Tool** (Optional): `vpn-manager.command`
|
||||
- Interactive menu-driven script for manual VPN management
|
||||
- Can connect/disconnect VPN manually
|
||||
- Tests connectivity to production server
|
||||
|
||||
## Token Management (If Needed)
|
||||
|
||||
### When Token Expires:
|
||||
**What I Tell User**: "Your Git access token has expired. Please generate a new one:"
|
||||
|
||||
**User Steps** (only these 3):
|
||||
1. Go to https://gitea.sigd.net
|
||||
2. Settings → Applications → Access Tokens → Generate New Token
|
||||
3. Copy the new token and give it to me
|
||||
|
||||
**What I Do**: Update the git remote configuration with new token
|
||||
```bash
|
||||
git remote remove origin
|
||||
git remote add origin https://eliza:NEW_TOKEN@gitea.sigd.net/chaulmark/deafgain-website.git
|
||||
git pull
|
||||
```
|
||||
|
||||
## Website Components (What I Manage)
|
||||
|
||||
### Pages (src/pages/)
|
||||
1. **Home.tsx**: Landing page
|
||||
2. **About.tsx**: About Eliza
|
||||
3. **Services.tsx**: Has the interactive US map with conference locations
|
||||
4. **Resources.tsx**: Video library (6 governance training videos)
|
||||
5. **Contact.tsx**: Contact form
|
||||
|
||||
### Key Features I Maintain
|
||||
|
||||
#### Interactive Map (Services.tsx)
|
||||
- 23 locations across US showing conferences/events
|
||||
- Green = upcoming, Gray = past events
|
||||
- File: `src/pages/Services.tsx`
|
||||
- Array: `relationshipPoints`
|
||||
|
||||
**When User Says**: "Add [Event] to [City, State] on [dates]"
|
||||
|
||||
**What I Do**:
|
||||
1. Get coordinates from Google Maps for the city
|
||||
2. Open `src/pages/Services.tsx`
|
||||
3. Add to `relationshipPoints` array:
|
||||
```typescript
|
||||
{
|
||||
coordinates: [-longitude, latitude],
|
||||
city: "City",
|
||||
state: "State",
|
||||
events: [{
|
||||
name: "ORG",
|
||||
description: "Full event description",
|
||||
startDate: "Month DD, YYYY",
|
||||
endDate: "Month DD, YYYY"
|
||||
}]
|
||||
}
|
||||
```
|
||||
4. Commit: "Add [Event] to [City, ST] on interactive map"
|
||||
5. Push and deploy
|
||||
|
||||
#### Video Library (Resources.tsx)
|
||||
- 6 governance training videos with subtitles/transcripts
|
||||
- File: `src/pages/Resources.tsx`
|
||||
- Each video needs 4 files (user provides, I integrate)
|
||||
|
||||
**When User Says**: "Add new video [title]"
|
||||
|
||||
**User Provides**:
|
||||
- Video file (.mp4)
|
||||
- Subtitle file (.vtt) OR raw subtitle text
|
||||
- Transcript text
|
||||
- Thumbnail image (.png)
|
||||
|
||||
**What I Do**:
|
||||
1. Save files to correct locations:
|
||||
- `public/videos/video-name.mp4`
|
||||
- `public/subtitles/video-name.vtt`
|
||||
- `public/transcriptions/video-name.txt`
|
||||
- `public/images/thumbnails/video-name.png`
|
||||
2. Add entry to `src/pages/Resources.tsx` videos array
|
||||
3. Commit: "Add new video: [Title]"
|
||||
4. Push and deploy
|
||||
|
||||
**Current Videos**:
|
||||
1. governance-documents
|
||||
2. secretary-role-responsibilities
|
||||
3. minutes-approval-procedure
|
||||
4. board-participation-guidelines
|
||||
5. board-member-reprimands
|
||||
6. meeting-minutes-access-rights
|
||||
|
||||
#### Email System
|
||||
- Contact form → eliza@deafgain.org
|
||||
- Subscription form → welcome email
|
||||
- Config in `.env` file (never commit this!)
|
||||
- Current Gmail app password: `idjcxxwzvloorfuk` (generated 1/9/2026)
|
||||
- SMTP: smtp.gmail.com:587
|
||||
|
||||
**When User Says**: "Email not working"
|
||||
|
||||
**What I Do**:
|
||||
1. Check `.env` file for correct settings
|
||||
2. Test with execute_command
|
||||
3. If password expired, ask user to regenerate Gmail app password
|
||||
4. Update `.env` and redeploy
|
||||
|
||||
## Task Execution Patterns (AI Internal Process)
|
||||
|
||||
### Task: "Add Location to Map"
|
||||
```
|
||||
1. User Request: "Add RID conference in Missoula, MT on Jan 10, 2026"
|
||||
2. I pull latest: git pull
|
||||
3. I lookup coordinates: [-113.9940, 46.8721]
|
||||
4. I edit Services.tsx, add to relationshipPoints array
|
||||
5. I commit: "Add RID conference to Missoula MT on interactive map"
|
||||
6. I push: git push
|
||||
7. I deploy: ./deploy.sh (handles VPN, SSH, git pull, docker rebuild automatically)
|
||||
8. I verify: Check https://deafgain.org/services
|
||||
9. I report: "Done! RID conference added to map at deafgain.org"
|
||||
```
|
||||
|
||||
### Task: "Add New Video"
|
||||
```
|
||||
1. User Request: "Add this video [title]" + provides files
|
||||
2. User provides: .mp4, .vtt (or subtitle text), transcript text, .png
|
||||
3. I pull latest: git pull
|
||||
4. I create/save files:
|
||||
- public/videos/video-name.mp4
|
||||
- public/subtitles/video-name.vtt (convert if needed)
|
||||
- public/transcriptions/video-name.txt
|
||||
- public/images/thumbnails/video-name.png
|
||||
5. I edit Resources.tsx, add video to array
|
||||
6. I test locally: pnpm dev (verify video plays)
|
||||
7. I commit: "Add new video: [Title]"
|
||||
8. I push: git push
|
||||
9. I deploy: ./deploy.sh (handles VPN, SSH, git pull, docker rebuild automatically)
|
||||
10. I verify: Check https://deafgain.org/resources
|
||||
11. I report: "Done! New video '[Title]' is live on resources page"
|
||||
```
|
||||
|
||||
### Task: "Update Services/Content"
|
||||
```
|
||||
1. User Request: "Change [content] on [page]"
|
||||
2. I pull latest: git pull
|
||||
3. I identify correct file (Home.tsx, About.tsx, Services.tsx, etc.)
|
||||
4. I make changes
|
||||
5. I commit: "Update [page]: [description]"
|
||||
6. I push: git push
|
||||
7. I deploy: ./deploy.sh (handles VPN, SSH, git pull, docker rebuild automatically)
|
||||
8. I report: "Done! Changes live at deafgain.org"
|
||||
```
|
||||
|
||||
### Task: "Fix Bug/Issue"
|
||||
```
|
||||
1. User reports: "Email form not working" or "Video won't play"
|
||||
2. I pull latest: git pull
|
||||
3. I diagnose issue (check logs, test locally, review code)
|
||||
4. I fix the problem in relevant files
|
||||
5. I test fix locally: pnpm dev
|
||||
6. I commit: "Fix [issue description]"
|
||||
7. I push: git push
|
||||
8. I deploy: ./deploy.sh (handles VPN, SSH, git pull, docker rebuild automatically)
|
||||
9. I verify fix: Test on live site
|
||||
10. I report: "Fixed! [Explanation of what was wrong and what I did]"
|
||||
```
|
||||
|
||||
## Critical Files I Work With
|
||||
|
||||
### Main Code Files
|
||||
- `src/pages/Services.tsx` - Interactive map (relationshipPoints array)
|
||||
- `src/pages/Resources.tsx` - Video library (videos array)
|
||||
- `src/pages/Home.tsx` - Landing page content
|
||||
- `src/pages/About.tsx` - About page content
|
||||
- `src/pages/Contact.tsx` - Contact form
|
||||
- `src/api/contact.ts` - Contact form backend
|
||||
- `src/api/subscribe.ts` - Subscription backend
|
||||
- `.env` - Environment variables (SECRETS - never commit!)
|
||||
|
||||
### Asset Directories
|
||||
- `public/videos/` - Video files (.mp4)
|
||||
- `public/subtitles/` - WebVTT subtitle files (.vtt)
|
||||
- `public/transcriptions/` - Text transcripts (.txt)
|
||||
- `public/images/thumbnails/` - Video thumbnails (.png)
|
||||
|
||||
### Configuration Files
|
||||
- `docker-compose.yml` - Production container setup
|
||||
- `vite.config.ts` - Build configuration
|
||||
- `tailwind.config.js` - Styling configuration
|
||||
- `package.json` - Dependencies
|
||||
|
||||
## Environment Variables (.env)
|
||||
```env
|
||||
NODE_ENV=production
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
GMAIL_USER=system@deafgain.org
|
||||
GMAIL_PASS=idjcxxwzvloorfuk
|
||||
SMTP_FROM=system@deafgain.org
|
||||
SMTP_TO=eliza@deafgain.org
|
||||
API_URL=http://localhost:804
|
||||
# REDIS_URL=redis://localhost:6379 (disabled)
|
||||
```
|
||||
|
||||
## Recent History (Reference Only)
|
||||
- **April 6, 2026**:
|
||||
- Updated Eliza's photo on About page
|
||||
- Copied IMG_8508.jpeg from Desktop (new photo with dog and Montana mountains)
|
||||
- Renamed to `eliza-2026.jpg` to bypass CDN/Cloudflare cache (old photo was cached for 30 days)
|
||||
- Updated About.tsx to reference new filename
|
||||
- Successfully deployed, new photo now visible to all visitors
|
||||
- Added American Institute of Parliamentarians membership to Certifications section
|
||||
- Now reads: "Certified ADA Coordinator, Insured Registered Parliamentarian (RP) of the National Association of Parliamentarians, Member of the American Institute of Parliamentarians"
|
||||
- Installed node_modules locally on Mac to clear VS Code editor errors
|
||||
- Ran `npm install --legacy-peer-deps` to install 410 packages
|
||||
- Cleared 56 TypeScript/linter errors that were showing in VS Code
|
||||
- This only affects local editor display, does not change live website or Git repository
|
||||
- node_modules folder is ignored by Git (.gitignore)
|
||||
- **April 4, 2026**:
|
||||
- Added 4 new network relationships to interactive map:
|
||||
- MCDi Board Training in Missoula, MT (April 25, 2026)
|
||||
- NAD Conference in San Francisco, CA (June 30 - July 5, 2026)
|
||||
- NAP National Training Conference in Birmingham, AL (August 26-30, 2026)
|
||||
- AIP Annual Session in San Diego, CA (July 29 - August 1, 2026)
|
||||
- Map now has 23 total locations (up from 20)
|
||||
- **March 13, 2026**:
|
||||
- Updated About page: Added "Insured" prefix to Registered Parliamentarian certification
|
||||
- Now reads: "Insured Registered Parliamentarian (RP) of the National Association of Parliamentarians"
|
||||
- **March 5, 2026**:
|
||||
- Cloned repo, created memory bank, established AI assistant role
|
||||
- Created automated deployment script (deploy.sh) with VPN and Docker rebuild
|
||||
- Installed WireGuard for VPN connectivity
|
||||
- Fixed server path to /home/chaulmark/websites/deafgain.org
|
||||
- Successfully tested full automated deployment workflow
|
||||
- Added MOAD Board Training event to Jefferson City, MO on interactive map (now 20 total locations)
|
||||
- Updated About page:
|
||||
- Changed NAP certification to "Registered Parliamentarian (RP) of the National Association of Parliamentarians"
|
||||
- Updated Leadership to "Founding Former President of Missoula Council of the Deaf (MCDi), Owner of DeafGain LLC"
|
||||
- Added Certificate in Deaf Studies and Social Justice from University of Kansas to Education
|
||||
- Removed comma from advocacy quote
|
||||
- **January 26, 2026**: Added RID conference to Missoula MT
|
||||
- **January 20, 2026**: Added anti-spam honeypot
|
||||
- **January 9, 2026**: Fixed email (SMTP 587, new Gmail password)
|
||||
- **May 27, 2025**: Fixed security vulnerabilities, deployed to production
|
||||
|
||||
## Common Issues & AI Solutions
|
||||
|
||||
### Issue: "Email not working"
|
||||
**What I Do**: Check `.env` Gmail settings, test email, regenerate app password if needed, redeploy
|
||||
|
||||
### Issue: "Video won't play"
|
||||
**What I Do**: Verify all 4 files exist (.mp4, .vtt, .txt, .png), check file paths, test locally, fix and redeploy
|
||||
|
||||
### Issue: "Map not showing location"
|
||||
**What I Do**: Check coordinates format `[-long, lat]`, verify array syntax, test locally, fix and redeploy
|
||||
|
||||
### Issue: "Changes not live on website"
|
||||
**What I Do**: Run `./deploy.sh` to automatically connect VPN, SSH to production, pull latest, rebuild Docker containers
|
||||
|
||||
### Issue: "Git token expired"
|
||||
**What I Tell User**: "Please generate new token at gitea.sigd.net → Settings → Applications → Access Tokens"
|
||||
**What I Do**: Update git remote with new token when provided
|
||||
|
||||
## Communication Style
|
||||
|
||||
### Good Responses (User-Friendly):
|
||||
✅ "Done! I've added the RID conference to Missoula, MT on the map. Changes are live at deafgain.org/services"
|
||||
✅ "Fixed the email issue - it was using the wrong port. Everything works now!"
|
||||
✅ "I've added your new video to the Resources page. You can see it at deafgain.org/resources"
|
||||
|
||||
### Bad Responses (Too Technical):
|
||||
❌ "I updated the relationshipPoints array in Services.tsx with the new coordinates..."
|
||||
❌ "The SMTP configuration in the .env file had the wrong port value..."
|
||||
❌ "I modified the videos array and committed to the main branch..."
|
||||
|
||||
**Rule**: Report WHAT was done and RESULTS, not HOW it was done technically
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### User Says → AI Does
|
||||
- "Add [Event] to map" → Get coordinates, edit Services.tsx, commit, push, run deploy.sh
|
||||
- "Add video [Title]" → Save 4 files, edit Resources.tsx, commit, push, run deploy.sh
|
||||
- "Update [Page] content" → Edit page file, commit, push, run deploy.sh
|
||||
- "Email not working" → Diagnose, fix .env or code, commit, push, run deploy.sh
|
||||
- "Fix [problem]" → Diagnose, fix code, test locally, commit, push, run deploy.sh
|
||||
|
||||
### AI Never Asks User To:
|
||||
- ❌ Run git commands
|
||||
- ❌ Edit code files
|
||||
- ❌ SSH to server
|
||||
- ❌ Run Docker commands
|
||||
- ❌ Install dependencies
|
||||
- ❌ Debug code
|
||||
|
||||
### AI Always:
|
||||
- ✅ Pull before starting work
|
||||
- ✅ Test locally when possible
|
||||
- ✅ Commit with clear messages
|
||||
- ✅ Push to Git repository
|
||||
- ✅ Deploy to production via `./deploy.sh` (handles VPN and everything automatically)
|
||||
- ✅ Verify changes are live
|
||||
- ✅ Report completion simply
|
||||
|
||||
## Status
|
||||
🟢 **System Operational** - All components working, production live at https://deafgain.org/
|
||||
🤖 **AI Assistant Ready** - Waiting for user requests to make changes, add content, or fix issues
|
||||
57
cline_docs/productContext.md
Normal file
57
cline_docs/productContext.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Product Context
|
||||
|
||||
## Purpose
|
||||
DeafGain LLC Website serves as a digital platform dedicated to empowering the Deaf community through innovative communication solutions, training, and accessibility services.
|
||||
|
||||
## Problems Solved
|
||||
1. Digital Accessibility
|
||||
- Provides ADA-compliant transcripts
|
||||
- Implements universal website design principles
|
||||
- Ensures content accessibility through WebVTT subtitles and transcriptions
|
||||
|
||||
2. Communication Barriers
|
||||
- Facilitates board communication protocols
|
||||
- Offers leadership training
|
||||
- Bridges communication gaps between deaf and hearing communities
|
||||
|
||||
3. Service Visibility
|
||||
- Showcases comprehensive service offerings
|
||||
- Provides easy access to resources
|
||||
- Demonstrates commitment to professional quality
|
||||
|
||||
## Intended Functionality
|
||||
|
||||
### Core Features
|
||||
1. Interactive Components
|
||||
- USA region map visualization
|
||||
- Custom video player with accessibility features
|
||||
- Contact form with email notifications
|
||||
- Snow peaks animation effect
|
||||
|
||||
2. Content Sections
|
||||
- Service showcase
|
||||
- Biography section
|
||||
- Testimonials
|
||||
- Timeline visualization
|
||||
- Resources library
|
||||
|
||||
3. Accessibility Features
|
||||
- WebVTT subtitles
|
||||
- Text transcriptions
|
||||
- Universal design principles
|
||||
- Responsive layout
|
||||
|
||||
### User Experience
|
||||
- Modern, responsive design
|
||||
- Smooth animations and transitions
|
||||
- Clear navigation structure
|
||||
- Accessible content presentation
|
||||
- Professional and engaging interface
|
||||
|
||||
### Technical Requirements
|
||||
- Fast loading times
|
||||
- Mobile responsiveness
|
||||
- Email notification system
|
||||
- Rate limiting for security
|
||||
- Caching for performance
|
||||
- Secure contact form handling
|
||||
99
cline_docs/progress.md
Normal file
99
cline_docs/progress.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Project Progress
|
||||
|
||||
## Implemented Features
|
||||
|
||||
### Core Components
|
||||
✅ Project Structure
|
||||
- Layout components (Navbar, Footer)
|
||||
- Page components (Home, About, Contact, Resources, Services)
|
||||
- Shared components (Toast notifications)
|
||||
|
||||
✅ Video Features
|
||||
- Custom VideoPlayer component
|
||||
- WebVTT subtitle support
|
||||
- Transcription integration
|
||||
|
||||
✅ Interactive Elements
|
||||
- SnowPeaks animation
|
||||
- Contact form with email integration
|
||||
- Toast notification system
|
||||
- Interactive map with 18 conference locations (latest additions: New Orleans, LA and Austin, TX)
|
||||
|
||||
### Content
|
||||
✅ Media Assets
|
||||
- Video content (5 videos total)
|
||||
- Latest addition: "Secretary Role & Responsibilities" (March 2025)
|
||||
- Subtitle files (.vtt)
|
||||
- Transcription files (.txt)
|
||||
- Images and thumbnails
|
||||
|
||||
✅ Page Content
|
||||
- Navigation structure
|
||||
- Service information
|
||||
- Resource materials
|
||||
- Contact functionality
|
||||
|
||||
### Infrastructure
|
||||
✅ Development Environment
|
||||
- Docker configuration
|
||||
- TypeScript setup
|
||||
- ESLint configuration
|
||||
- Build system (Vite)
|
||||
|
||||
✅ Backend Services
|
||||
- Express server
|
||||
- Email handling
|
||||
- Rate limiting
|
||||
- Redis integration
|
||||
|
||||
## In Progress Features
|
||||
🔄 None currently identified - all requested features implemented
|
||||
|
||||
## Planned Features
|
||||
📋 To be determined based on roadmap and requirements
|
||||
|
||||
## Known Issues
|
||||
- None documented yet - need to review issue tracker
|
||||
|
||||
## Next Steps
|
||||
1. Verify new video content displays and functions correctly
|
||||
2. Review and verify all implemented features
|
||||
3. Test accessibility compliance
|
||||
4. Perform performance optimization
|
||||
5. Conduct security audit
|
||||
6. Update documentation as needed
|
||||
|
||||
## Testing Status
|
||||
### Completed
|
||||
- Basic component rendering
|
||||
- Route navigation
|
||||
- Form submission
|
||||
- Video playback
|
||||
|
||||
### Pending
|
||||
- End-to-end testing
|
||||
- Accessibility testing
|
||||
- Performance testing
|
||||
- Security testing
|
||||
|
||||
## Deployment Status
|
||||
- Development environment: Configured
|
||||
- Production environment: Configured
|
||||
- Docker containers: Set up
|
||||
- Nginx configuration: Complete
|
||||
- Redis integration: Complete
|
||||
|
||||
## Documentation Status
|
||||
✅ README
|
||||
✅ Technical documentation
|
||||
✅ API documentation
|
||||
✅ Deployment guides
|
||||
|
||||
## Notes
|
||||
- Project appears to be in a mature state with core features implemented
|
||||
- Focus should be on testing, optimization, and maintenance
|
||||
- Regular updates to documentation needed as features evolve
|
||||
- Video content management process established:
|
||||
- Consistent file naming convention: [descriptive-id].mp4, .vtt, .txt
|
||||
- Thumbnails stored in /images/thumbnails/
|
||||
- Videos added to appropriate category in Resources.tsx
|
||||
119
cline_docs/systemPatterns.md
Normal file
119
cline_docs/systemPatterns.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# System Patterns
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Frontend Architecture
|
||||
1. React Component Structure
|
||||
- Layout components for consistent structure
|
||||
- Shared components for reusability
|
||||
- Page-specific components for unique features
|
||||
- Context providers for state management
|
||||
|
||||
2. Routing System
|
||||
- React Router v7 for navigation
|
||||
- Client-side routing
|
||||
- Protected routes where necessary
|
||||
|
||||
3. State Management
|
||||
- React Context for global state
|
||||
- Local state for component-specific data
|
||||
- Notification context for toast messages
|
||||
|
||||
### Backend Architecture
|
||||
1. API Structure
|
||||
- Express.js server
|
||||
- RESTful endpoints
|
||||
- Rate-limited routes
|
||||
- Email service integration
|
||||
|
||||
2. Caching Layer
|
||||
- Redis-backed caching
|
||||
- Session management
|
||||
- Rate limiting implementation
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
### Development Environment
|
||||
1. Build System
|
||||
- Vite for fast development
|
||||
- Hot Module Replacement
|
||||
- TypeScript compilation
|
||||
- Concurrent frontend/backend development
|
||||
|
||||
2. Docker Configuration
|
||||
- Multi-container setup
|
||||
- Development and production configurations
|
||||
- Volume mounting for persistent data
|
||||
- Bridge network setup
|
||||
|
||||
### Production Setup
|
||||
1. Deployment Strategy
|
||||
- Multi-stage Docker builds
|
||||
- Nginx for static file serving
|
||||
- Node.js API server
|
||||
- Automatic container restart
|
||||
|
||||
2. Performance Optimizations
|
||||
- Tree shaking
|
||||
- Code splitting
|
||||
- Redis caching
|
||||
- Static file optimization
|
||||
|
||||
### Security Patterns
|
||||
1. API Security
|
||||
- Rate limiting
|
||||
- CORS configuration
|
||||
- Environment variable protection
|
||||
- Secure email handling
|
||||
|
||||
2. Data Protection
|
||||
- Form validation
|
||||
- Input sanitization
|
||||
- Redis session management
|
||||
- Secure credential storage
|
||||
|
||||
## Directory Structure
|
||||
```
|
||||
src/
|
||||
├── api/ # Backend API routes
|
||||
├── components/ # UI components
|
||||
│ ├── Layout/ # Layout structure
|
||||
│ └── shared/ # Reusable components
|
||||
├── context/ # React Context
|
||||
├── lib/ # Utility functions
|
||||
├── pages/ # Route pages
|
||||
└── assets/ # Static assets
|
||||
|
||||
public/
|
||||
├── images/ # Image assets
|
||||
├── subtitles/ # WebVTT files
|
||||
├── transcriptions/# Text files
|
||||
└── videos/ # Video content
|
||||
```
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### Component Patterns
|
||||
- Functional components with hooks
|
||||
- TypeScript interfaces for props
|
||||
- Separation of concerns
|
||||
- Reusable styling with Tailwind
|
||||
|
||||
### Animation Patterns
|
||||
- Framer Motion for complex animations
|
||||
- CSS transitions for simple effects
|
||||
- Intersection Observer for scroll animations
|
||||
- Performance-optimized animations
|
||||
|
||||
### Accessibility Patterns
|
||||
- ARIA labels
|
||||
- Semantic HTML
|
||||
- Keyboard navigation
|
||||
- Screen reader support
|
||||
- Color contrast compliance
|
||||
|
||||
### Testing Strategy
|
||||
- Component testing
|
||||
- API endpoint testing
|
||||
- Integration testing
|
||||
- Accessibility testing
|
||||
113
cline_docs/techContext.md
Normal file
113
cline_docs/techContext.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Technical Context
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Frontend Technologies
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| React | 19.0.0 | Core UI Framework |
|
||||
| TypeScript | 5.7.2 | Type Safety |
|
||||
| React Router | 7.0.2 | Routing |
|
||||
| Tailwind CSS | 3.4.16 | Styling |
|
||||
| Framer Motion | 11.13.5 | Animations |
|
||||
| React Simple Maps | 4.0.0-beta.6 | Geographic Visualization |
|
||||
| Next.js | 14.1.0 | Framework Features |
|
||||
| React Icons | 5.4.0 | Icon Library |
|
||||
| React Intersection Observer | 9.13.1 | Scroll Detection |
|
||||
|
||||
### Backend Technologies
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Express | 4.21.2 | API Server |
|
||||
| Node.js | Latest LTS | Runtime Environment |
|
||||
| Nodemailer | 6.9.9 | Email Service |
|
||||
| Upstash Redis | 1.28.4 | Caching/Rate Limiting |
|
||||
| TypeScript | 5.7.2 | Type Safety |
|
||||
| dotenv | 16.4.7 | Environment Management |
|
||||
|
||||
### Development Tools
|
||||
| Tool | Version | Purpose |
|
||||
|------|---------|---------|
|
||||
| Vite | 6.0.3 | Build System |
|
||||
| pnpm | 9.14.2 | Package Manager |
|
||||
| ESLint | 9.15.0 | Code Linting |
|
||||
| Docker | Latest | Containerization |
|
||||
| PostCSS | 8.4.49 | CSS Processing |
|
||||
| Autoprefixer | 10.4.20 | CSS Compatibility |
|
||||
| Concurrently | 8.2.2 | Process Management |
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js LTS version
|
||||
- pnpm 9.14.2
|
||||
- Docker and Docker Compose
|
||||
- Git
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start development servers
|
||||
pnpm dev # Runs frontend (3000) and backend (4000)
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Preview production build
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
Required variables:
|
||||
- `GMAIL_USER`: Email service username
|
||||
- `GMAIL_PASS`: Email service password
|
||||
- `REDIS_URL`: Upstash Redis connection URL
|
||||
- `REDIS_TOKEN`: Upstash Redis auth token
|
||||
- `API_URL`: Backend API URL
|
||||
- `NODE_ENV`: Environment (development/production)
|
||||
|
||||
### Docker Development
|
||||
```bash
|
||||
# Development environment
|
||||
docker-compose -f docker-compose.dev.yml up
|
||||
|
||||
# Production build
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
### Performance Requirements
|
||||
- First Contentful Paint < 1.5s
|
||||
- Time to Interactive < 3.5s
|
||||
- Lighthouse Performance Score > 90
|
||||
- Mobile-responsive breakpoints
|
||||
|
||||
### Browser Support
|
||||
- Modern browsers (last 2 versions)
|
||||
- Chrome, Firefox, Safari, Edge
|
||||
- Mobile browsers (iOS Safari, Chrome Android)
|
||||
|
||||
### Accessibility Requirements
|
||||
- WCAG 2.1 Level AA compliance
|
||||
- Screen reader compatibility
|
||||
- Keyboard navigation support
|
||||
- Color contrast ratios
|
||||
- Subtitle and transcript support
|
||||
|
||||
### Security Requirements
|
||||
- Rate limiting on API endpoints
|
||||
- CORS policy configuration
|
||||
- Secure email handling
|
||||
- Environment variable protection
|
||||
- Input validation and sanitization
|
||||
|
||||
### Infrastructure Requirements
|
||||
- Docker container deployment
|
||||
- Nginx reverse proxy
|
||||
- Redis caching layer
|
||||
- Email service integration
|
||||
- Static file serving
|
||||
- API server configuration
|
||||
110
create-readme.sh
110
create-readme.sh
|
|
@ -1,110 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cat > README.md << 'EOL'
|
||||
# DeafGain LLC Website 🌐
|
||||
|
||||
[](https://pnpm.io/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://reactjs.org/)
|
||||
|
||||
## Overview
|
||||
|
||||
A dynamic web platform dedicated to empowering the Deaf community through innovative communication solutions, training, and accessibility services. This website serves as the digital presence for DeafGain LLC, showcasing our comprehensive services including ADA-compliant transcripts, universal website design, board communication protocols, and leadership training.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
\`\`\`
|
||||
|
||||
Visit \`localhost:3000\` in your browser.
|
||||
|
||||
## 🛠 Technical Stack
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| React | 18.2.0 | Core Framework |
|
||||
| TypeScript | 4.9.5 | Type Safety |
|
||||
| Tailwind CSS | 3.x | Styling |
|
||||
| Framer Motion | 10.x | Animations |
|
||||
| React Simple Maps | 3.0.0 | Geographic Visualization |
|
||||
| Vite | 4.x | Build System |
|
||||
| Docker | - | Containerization |
|
||||
| Nginx | - | Web Server |
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
\`\`\`
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── layout/ # Layout components
|
||||
│ ├── ui/ # UI elements
|
||||
│ └── maps/ # Map visualization components
|
||||
├── pages/ # Main application pages
|
||||
├── hooks/ # Custom React hooks
|
||||
├── utils/ # Utility functions
|
||||
├── types/ # TypeScript definitions
|
||||
├── assets/ # Static assets
|
||||
└── styles/ # Global styles
|
||||
\`\`\`
|
||||
|
||||
## 🎯 Core Features
|
||||
|
||||
- ⚡️ Intuitive user interface with responsive design
|
||||
- 🗺️ Interactive USA region map displaying network and relationships
|
||||
- 🎥 Video resources with accessible playback controls and captions
|
||||
- 📱 Mobile-first responsive design
|
||||
- 🔒 Secure contact system
|
||||
- 🌐 Key sections: Home, Services, About, Resources, and Contact
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Environment Setup
|
||||
- Development: Vite dev server with hot reload
|
||||
- Production: Nginx configuration with optimized cache settings
|
||||
- Docker containers for consistent deployment
|
||||
|
||||
### Scripts
|
||||
\`\`\`bash
|
||||
pnpm dev # Start development server
|
||||
pnpm build # Build for production
|
||||
pnpm preview # Preview production build
|
||||
pnpm lint # Run ESLint
|
||||
pnpm test # Run tests
|
||||
\`\`\`
|
||||
|
||||
## 🚢 Production Deployment
|
||||
|
||||
\`\`\`bash
|
||||
docker-compose up --build
|
||||
\`\`\`
|
||||
|
||||
Application will be served on port 80.
|
||||
|
||||
## <20><> Contributing
|
||||
|
||||
We welcome contributions that align with our mission of improving accessibility and empowerment for the Deaf community. Follow our established patterns for:
|
||||
|
||||
- Component architecture
|
||||
- Styling conventions (Tailwind CSS)
|
||||
- Accessibility standards (WCAG 2.1)
|
||||
- TypeScript typing
|
||||
- Git commit messages
|
||||
|
||||
## 📄 License
|
||||
|
||||
Proprietary - DeafGain LLC
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
For questions or support, reach out through our [contact form](https://deafgain.com/contact).
|
||||
|
||||
---
|
||||
|
||||
> This website represents DeafGain LLC's commitment to creating accessible, user-friendly digital experiences while maintaining high technical standards and professional quality.
|
||||
EOL
|
||||
|
||||
chmod +x create-readme.sh
|
||||
echo "README.md has been created successfully!"
|
||||
|
||||
94
default.conf
Normal file
94
default.conf
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
server {
|
||||
listen 804;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Logging configuration for statistics
|
||||
access_log /var/log/nginx/deafgain-website/access.log combined;
|
||||
error_log /var/log/nginx/deafgain-website/error.log error;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
# API endpoints
|
||||
location /api/contact {
|
||||
proxy_pass http://deafgain-website-api-1:3000/contact;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
|
||||
# Handle OPTIONS method
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
location /api/subscribe {
|
||||
proxy_pass http://deafgain-website-api-1:3000/subscribe;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
|
||||
# Handle OPTIONS method
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# React app - serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Video files handling - serve from mounted directory
|
||||
location /videos/ {
|
||||
alias /usr/share/nginx/html/videos/;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
add_header Accept-Ranges bytes;
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
}
|
||||
|
||||
# Static asset caching
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|vtt|txt)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
}
|
||||
120
deploy.sh
Executable file
120
deploy.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/bin/bash
|
||||
|
||||
# DeafGain Website - Automated Deployment Script
|
||||
# Handles VPN connection and Docker deployment to production server
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
VPN_CONFIG="$HOME/wg1.conf"
|
||||
PRODUCTION_SERVER="10.4.0.205"
|
||||
SSH_USER="chaulmark"
|
||||
PROJECT_PATH="/home/chaulmark/websites/deafgain.org"
|
||||
|
||||
echo -e "${BLUE}=================================================${NC}"
|
||||
echo -e "${BLUE}🚀 DeafGain Website Deployment${NC}"
|
||||
echo -e "${BLUE}=================================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to check if VPN is connected
|
||||
is_vpn_connected() {
|
||||
sudo wg show interfaces 2>/dev/null | grep -q "wg1"
|
||||
}
|
||||
|
||||
# Function to connect VPN
|
||||
connect_vpn() {
|
||||
echo -e "${YELLOW}🔄 Connecting to VPN...${NC}"
|
||||
|
||||
if ! [ -f "$VPN_CONFIG" ]; then
|
||||
echo -e "${RED}❌ VPN config not found: $VPN_CONFIG${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run wg-quick and capture output
|
||||
result=$(sudo wg-quick up "$VPN_CONFIG" 2>&1) || true
|
||||
|
||||
# Check if already exists (already connected)
|
||||
if echo "$result" | grep -q "already exists"; then
|
||||
echo -e "${GREEN}✅ VPN already connected${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if connection was successful (look for success indicators)
|
||||
if echo "$result" | grep -q "Backgrounding route monitor" || is_vpn_connected; then
|
||||
echo -e "${GREEN}✅ VPN connected successfully${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}❌ Failed to connect VPN:${NC}"
|
||||
echo "$result"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test connectivity
|
||||
test_connectivity() {
|
||||
echo -e "${YELLOW}🔍 Testing connectivity to production server...${NC}"
|
||||
|
||||
if ping -c 1 -W 2 $PRODUCTION_SERVER >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Production server ($PRODUCTION_SERVER) is reachable${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}❌ Cannot reach production server ($PRODUCTION_SERVER)${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to deploy
|
||||
deploy_to_production() {
|
||||
echo -e "${YELLOW}🚀 Deploying to production server...${NC}"
|
||||
echo ""
|
||||
|
||||
ssh $SSH_USER@$PRODUCTION_SERVER "cd $PROJECT_PATH && echo '📥 Pulling latest changes...' && git pull && echo '' && echo '🔨 Rebuilding and restarting Docker containers...' && docker compose up --build -d && echo '' && echo '✅ Deployment complete!'"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=================================================${NC}"
|
||||
echo -e "${GREEN}✅ Deployment Successful!${NC}"
|
||||
echo -e "${GREEN}=================================================${NC}"
|
||||
echo -e "${GREEN}🌐 Website: https://deafgain.org/${NC}"
|
||||
return 0
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}=================================================${NC}"
|
||||
echo -e "${RED}❌ Deployment Failed${NC}"
|
||||
echo -e "${RED}=================================================${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main deployment flow
|
||||
main() {
|
||||
# Step 1: Check/Connect VPN
|
||||
if is_vpn_connected; then
|
||||
echo -e "${GREEN}✅ VPN already connected${NC}"
|
||||
else
|
||||
connect_vpn
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 2: Test connectivity
|
||||
if ! test_connectivity; then
|
||||
echo -e "${RED}❌ Cannot proceed with deployment - server unreachable${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 3: Deploy
|
||||
deploy_to_production
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis-dev:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_dev_data:/data
|
||||
|
||||
web-dev:
|
||||
platform: linux/arm64
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
|
|
@ -10,8 +18,37 @@ services:
|
|||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- ./public:/app/public:ro # Mount public directory as read-only
|
||||
- ./public:/app/public
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_HOST=0.0.0.0
|
||||
command: pnpm dev --host 0.0.0.0
|
||||
- VITE_API_URL=http://localhost:3000
|
||||
command: pnpm dev
|
||||
depends_on:
|
||||
- api-dev
|
||||
|
||||
api-dev:
|
||||
platform: linux/arm64
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev.api
|
||||
ports:
|
||||
- "804:804"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=804
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- RECIPIENT_EMAIL=${RECIPIENT_EMAIL}
|
||||
- GOOGLE_EMAIL=${GOOGLE_EMAIL}
|
||||
- GOOGLE_APP_PASSWORD=${GOOGLE_APP_PASSWORD}
|
||||
- REDIS_URL=redis://redis-dev:6379
|
||||
command: pnpm dev
|
||||
depends_on:
|
||||
- redis-dev
|
||||
|
||||
volumes:
|
||||
redis_dev_data:
|
||||
|
|
|
|||
|
|
@ -1,25 +1,61 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
no_cache: true
|
||||
pull_policy: build
|
||||
container_name: deafgain-website-web-1
|
||||
ports:
|
||||
- "804:804"
|
||||
networks:
|
||||
- caddy_network
|
||||
volumes:
|
||||
- video_content:/app/public/videos
|
||||
depends_on:
|
||||
- api
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=804
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /docker/websites/deafgain/video_content:/usr/share/nginx/html/videos
|
||||
- /docker/websites/deafgain/logs:/var/log/nginx/deafgain-website
|
||||
networks:
|
||||
- caddy_network
|
||||
- default
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
no_cache: true
|
||||
pull_policy: build
|
||||
container_name: deafgain-website-api-1
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- GOOGLE_EMAIL=${GOOGLE_EMAIL}
|
||||
- GOOGLE_APP_PASSWORD=${GOOGLE_APP_PASSWORD}
|
||||
- RECIPIENT_EMAIL=${RECIPIENT_EMAIL}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_TOKEN=${REDIS_TOKEN}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: deafgain-website-redis-1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
video_content:
|
||||
external: true
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
caddy_network:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="preload" href="https://unpkg.com/react-icons/fa/index.js" as="script" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DeafGain LLC</title>
|
||||
</head>
|
||||
|
|
|
|||
50
nginx.conf
50
nginx.conf
|
|
@ -1,29 +1,25 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;";
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Static asset caching
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Define log formats first
|
||||
include /etc/nginx/conf.d/matomo_log_format.conf;
|
||||
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Include remaining server configurations
|
||||
include /etc/nginx/conf.d/default.conf;
|
||||
}
|
||||
|
|
|
|||
45
package.json
45
package.json
|
|
@ -4,38 +4,49 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "concurrently \"vite\" \"tsx watch server.ts\"",
|
||||
"build": "ROLLUP_SKIP_NODE_RESOLVE=true tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@upstash/redis": "^1.28.4",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"framer-motion": "^11.11.17",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"framer-motion": "^11.13.5",
|
||||
"next": "^15.3.2",
|
||||
"nodemailer": "^6.9.9",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"vite": "^5.4.11"
|
||||
"react-router-dom": "^7.6.1",
|
||||
"react-simple-maps": "4.0.0-beta.6",
|
||||
"redis": "^5.1.0",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.12.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.13.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
"typescript-eslint": "^8.18.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.14.2"
|
||||
}
|
||||
|
|
|
|||
2790
pnpm-lock.yaml
generated
2790
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
0
public/.gitkeep
Normal file
0
public/.gitkeep
Normal file
BIN
public/images/eliza-2026.jpg
Normal file
BIN
public/images/eliza-2026.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 364 KiB |
BIN
public/images/thumbnails/governance-documents.png
Normal file
BIN
public/images/thumbnails/governance-documents.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 529 KiB |
BIN
public/images/thumbnails/secretary-role-responsibilities.png
Normal file
BIN
public/images/thumbnails/secretary-role-responsibilities.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 692 KiB |
718
public/subtitles/governance-documents.vtt
Normal file
718
public/subtitles/governance-documents.vtt
Normal file
|
|
@ -0,0 +1,718 @@
|
|||
WEBVTT
|
||||
|
||||
00:00:01.780 --> 00:00:14.860
|
||||
Hello, this is Eliza Kragh here today with Mark Apodaca, who is an expert Professional Registered Parliamentarian.
|
||||
|
||||
00:00:15.130 --> 00:00:23.040
|
||||
We have an exciting topic to discuss today and will cover governance documents.
|
||||
|
||||
00:00:24.230 --> 00:00:34.440
|
||||
Hi Mark, thank you for your time. Please share about governance documents today.
|
||||
|
||||
00:00:39.140 --> 00:00:51.090
|
||||
I will be showing a triangle and will name each part of it, then take it down and discuss it in detail.
|
||||
|
||||
00:00:51.760 --> 00:00:52.480
|
||||
Beautiful!
|
||||
|
||||
00:01:18.890 --> 00:01:38.190
|
||||
These documents apply to corporations, non-profits, partnership businesses, and some government entities and universities.
|
||||
|
||||
00:01:38.360 --> 00:01:43.380
|
||||
They all have governance documents.
|
||||
|
||||
00:01:44.730 --> 00:01:50.580
|
||||
For today, I am focusing specifically on non-profit organizations.
|
||||
|
||||
00:01:52.560 --> 00:01:56.410
|
||||
First, you can see the top of the pyramid.
|
||||
|
||||
00:02:04.740 --> 00:02:11.210
|
||||
Laws or statutes - state level.
|
||||
|
||||
00:02:12.760 --> 00:02:16.580
|
||||
Next is Articles of Incorporation,
|
||||
|
||||
00:02:17.090 --> 00:02:19.630
|
||||
then Bylaws,
|
||||
|
||||
00:02:20.030 --> 00:02:34.110
|
||||
Special Rules of Order, Rules of Order, Standing Rules, and Policies and Procedures,
|
||||
|
||||
00:02:34.780 --> 00:02:38.360
|
||||
and last one on the bottom are Customs.
|
||||
|
||||
00:02:40.930 --> 00:02:47.590
|
||||
Laws at the top are the most important and have the highest authority.
|
||||
|
||||
00:02:48.780 --> 00:02:50.280
|
||||
I'm going to stop sharing now.
|
||||
|
||||
00:02:50.910 --> 00:02:51.410
|
||||
Okay.
|
||||
|
||||
00:03:02.380 --> 00:03:07.980
|
||||
Every state has its own non-profit laws.
|
||||
|
||||
00:03:09.210 --> 00:03:14.180
|
||||
They are not the same and they're all different.
|
||||
|
||||
00:03:15.710 --> 00:03:26.580
|
||||
What is New Mexico state law for non-profits is not the same as MN or MT or KS.
|
||||
|
||||
00:03:27.160 --> 00:03:33.140
|
||||
Each has their own with their own legislative passing laws.
|
||||
|
||||
00:03:33.930 --> 00:03:36.460
|
||||
These are the most authoritative laws.
|
||||
|
||||
00:03:38.280 --> 00:03:43.280
|
||||
The second one is the Articles of Incorporation.
|
||||
|
||||
00:03:47.210 --> 00:04:01.460
|
||||
If you want to set up a 501(c)(3) non-profit organization, you must file Articles of Incorporation with the Secretary of State.
|
||||
|
||||
00:04:02.110 --> 00:04:10.880
|
||||
Most states around the country require it to be filed with the Secretary of State, but some have exceptions.
|
||||
|
||||
00:04:13.880 --> 00:04:33.090
|
||||
In the Articles of Incorporation, you identify the name of the organization, the mission and purpose, and who the directors of the organization are.
|
||||
|
||||
00:04:33.260 --> 00:04:36.940
|
||||
Most of the time you will name three directors.
|
||||
|
||||
00:04:38.440 --> 00:04:42.790
|
||||
Directors do not have to be president or vice president.
|
||||
|
||||
00:04:44.060 --> 00:04:45.110
|
||||
Directors can be anyone.
|
||||
|
||||
00:04:48.290 --> 00:05:02.740
|
||||
An important point when filing Articles of Incorporation: you must have a paragraph with specific IRS clauses.
|
||||
|
||||
00:05:03.110 --> 00:05:18.560
|
||||
The reason is when you file a 501(c)(3) application, the IRS requires a copy of the AOI and bylaws.
|
||||
|
||||
00:05:19.590 --> 00:05:32.440
|
||||
If it does not show two important clauses, then the IRS will not accept your application for 501(c)(3).
|
||||
|
||||
00:05:34.130 --> 00:05:37.130
|
||||
So make sure to include these clauses.
|
||||
|
||||
00:05:37.490 --> 00:05:48.530
|
||||
Often times, the state will give a template to fill out, but that is not good enough
|
||||
|
||||
00:05:48.790 --> 00:05:58.080
|
||||
because many people use that template, submit it, and get denied because it doesn't include what the IRS requires.
|
||||
|
||||
00:05:59.190 --> 00:06:07.210
|
||||
So when you file AOI, make sure those clauses are included.
|
||||
|
||||
00:06:09.080 --> 00:06:27.090
|
||||
With the AOI, you send it to the Secretary of State who will check to see if the AOI and state law are in compliance with no contradictions between them.
|
||||
|
||||
00:06:28.030 --> 00:06:36.610
|
||||
If the Secretary of State finds that a clause conflicts with state law,
|
||||
|
||||
00:06:37.190 --> 00:06:46.210
|
||||
they will deny and send it back saying they do not accept it because the state law says otherwise.
|
||||
|
||||
00:06:46.830 --> 00:06:56.790
|
||||
This means you need to change that and ensure nothing conflicts with state law to get approval.
|
||||
|
||||
00:06:57.310 --> 00:07:05.730
|
||||
So AOI is the second highest authority under state law.
|
||||
|
||||
00:07:08.010 --> 00:07:11.440
|
||||
The third one is Bylaws.
|
||||
|
||||
00:07:12.930 --> 00:07:22.740
|
||||
The Bylaws cannot conflict with the AOI and cannot conflict with the state law either.
|
||||
|
||||
00:07:25.180 --> 00:07:29.660
|
||||
The fourth one is Special Rules of Order.
|
||||
|
||||
00:07:31.390 --> 00:07:33.410
|
||||
I will give you one good example.
|
||||
|
||||
00:07:36.080 --> 00:07:48.590
|
||||
You go to a business meeting as a member and someone makes a motion, it's seconded, and debate is open.
|
||||
|
||||
00:07:50.360 --> 00:07:54.480
|
||||
Each person can debate for 10 minutes.
|
||||
|
||||
00:07:57.660 --> 00:08:01.310
|
||||
10 minutes, two times.
|
||||
|
||||
00:08:02.360 --> 00:08:08.890
|
||||
That means I can debate in support, then someone debates against, and each time is 10 minutes.
|
||||
|
||||
00:08:09.680 --> 00:08:19.260
|
||||
If no one else new wants to speak in support, then the same person can speak again a second time for 10 minutes.
|
||||
|
||||
00:08:20.140 --> 00:08:26.910
|
||||
Suppose you had 200 members in a meeting and each has 10 minutes...
|
||||
|
||||
00:08:27.210 --> 00:08:28.960
|
||||
Meeting drags on.
|
||||
|
||||
00:08:29.030 --> 00:08:32.690
|
||||
It drags on all night and all day.
|
||||
|
||||
00:08:33.880 --> 00:08:42.640
|
||||
So Special Rules of Order must be passed by the members.
|
||||
|
||||
00:08:43.030 --> 00:08:58.590
|
||||
If you want to limit debate to 3 minutes per person, one time each for support and against, alternating, and then call for a vote...
|
||||
|
||||
00:08:58.930 --> 00:09:06.930
|
||||
That is called Special Rules because it will override Robert's Rules of Order.
|
||||
|
||||
00:09:08.140 --> 00:09:12.510
|
||||
If you do not have that, then you follow RONR.
|
||||
|
||||
00:09:14.780 --> 00:09:18.830
|
||||
Then the next authority we have is Rules of Order.
|
||||
|
||||
00:09:19.690 --> 00:09:24.710
|
||||
That can be Robert's Rules of Order, Newly Revised.
|
||||
|
||||
00:09:26.440 --> 00:09:29.380
|
||||
12th edition.
|
||||
|
||||
00:09:28.930 --> 00:09:30.460
|
||||
12th edition,
|
||||
|
||||
00:09:32.180 --> 00:09:33.590
|
||||
which is this book.
|
||||
|
||||
00:09:35.340 --> 00:09:44.230
|
||||
If your bylaws state that RONR is your parliamentary authority, then you need to follow the book.
|
||||
|
||||
00:09:44.460 --> 00:09:48.490
|
||||
Some organizations, though not many, use a different book.
|
||||
|
||||
00:09:50.810 --> 00:09:53.390
|
||||
Standard Code of Parliamentary Procedure.
|
||||
|
||||
00:09:53.530 --> 00:09:55.960
|
||||
Other organizations use different parliamentary authorities.
|
||||
|
||||
00:09:56.690 --> 00:10:01.860
|
||||
Yes, this is from the American Institute of Parliamentarians.
|
||||
|
||||
00:10:04.910 --> 00:10:07.360
|
||||
That is different from the National Association of Parliamentarians.
|
||||
|
||||
00:10:10.280 --> 00:10:13.580
|
||||
Many parliamentarians know both books.
|
||||
|
||||
00:10:13.690 --> 00:10:15.490
|
||||
I am a member of both organizations.
|
||||
|
||||
00:10:16.080 --> 00:10:21.010
|
||||
Some parliamentarians are certified in both.
|
||||
|
||||
00:10:22.260 --> 00:10:30.530
|
||||
Again, the parliamentarian gets a copy of the bylaws and reads it,
|
||||
|
||||
00:10:30.790 --> 00:10:33.910
|
||||
and if it states RONR, they follow it.
|
||||
|
||||
00:10:34.540 --> 00:10:42.730
|
||||
If it states AIP with the Standard Code of Parliamentary Procedure, then you follow that.
|
||||
|
||||
00:10:47.490 --> 00:11:05.280
|
||||
Sometimes organizations will use the Standard Code for Rules of Order that will supersede RONR, which can happen.
|
||||
|
||||
00:11:08.410 --> 00:11:13.040
|
||||
The next one is Standing Rules, Policies, and Procedures,
|
||||
|
||||
00:11:15.510 --> 00:11:18.910
|
||||
investment policies, finance policy, etc.
|
||||
|
||||
00:11:20.760 --> 00:11:29.290
|
||||
At that level, do not put them in the bylaws.
|
||||
|
||||
00:11:30.010 --> 00:11:36.830
|
||||
The bylaws explain the structure of the organization.
|
||||
|
||||
00:11:39.940 --> 00:11:45.980
|
||||
Policies and procedures need to be separate documents.
|
||||
|
||||
00:11:47.480 --> 00:11:50.180
|
||||
The last one is Customs.
|
||||
|
||||
00:11:51.460 --> 00:11:55.280
|
||||
It is something that has been used for many years.
|
||||
|
||||
00:11:56.530 --> 00:12:09.680
|
||||
But again, if you go to a business meeting and find a custom that conflicts with RONR,
|
||||
|
||||
00:12:10.290 --> 00:12:15.540
|
||||
RONR beats customs and you must follow RONR,
|
||||
|
||||
00:12:15.930 --> 00:12:24.080
|
||||
except sometimes people will elevate customs into Special Rules of Order.
|
||||
|
||||
00:12:25.740 --> 00:12:26.230
|
||||
It happens.
|
||||
|
||||
00:12:31.830 --> 00:12:33.390
|
||||
I want to clarify.
|
||||
|
||||
00:12:34.940 --> 00:12:41.580
|
||||
Your bylaws are a legal document - a legal document.
|
||||
|
||||
00:12:41.690 --> 00:12:51.780
|
||||
I've been in court where a judge asks me to be an expert witness.
|
||||
|
||||
00:12:51.930 --> 00:13:02.940
|
||||
I have to explain what the bylaws say and interpret them so the judge can see all perspectives because it is a legal document.
|
||||
|
||||
00:13:05.810 --> 00:13:10.440
|
||||
Another good example related to the triangle:
|
||||
|
||||
00:13:11.890 --> 00:13:20.930
|
||||
One organization established in their AOI that they would have members,
|
||||
|
||||
00:13:21.230 --> 00:13:34.360
|
||||
then when they created their bylaws, they also included members, just like the AOI.
|
||||
|
||||
00:13:35.230 --> 00:13:43.190
|
||||
Years later, the board decided they did not want members anymore.
|
||||
|
||||
00:13:43.330 --> 00:13:57.740
|
||||
So they amended the bylaws and removed members, but the AOI was never amended.
|
||||
|
||||
00:13:57.840 --> 00:13:58.840
|
||||
So they were in conflict.
|
||||
|
||||
00:13:59.490 --> 00:14:01.790
|
||||
That caused a conflict.
|
||||
|
||||
00:14:03.360 --> 00:14:12.830
|
||||
Some members notified me that this couldn't be done because they never voted on removing members.
|
||||
|
||||
00:14:15.090 --> 00:14:19.080
|
||||
I asked to see the minutes.
|
||||
|
||||
00:14:19.290 --> 00:14:25.880
|
||||
That is why minutes are very important because they are a legal document too.
|
||||
|
||||
00:14:27.390 --> 00:14:33.710
|
||||
They could not provide me with any minutes.
|
||||
|
||||
00:14:34.060 --> 00:14:42.830
|
||||
There was no record of members voting and approving the removal of members.
|
||||
|
||||
00:14:44.310 --> 00:14:51.410
|
||||
That means the AOI was more authoritative than the bylaws,
|
||||
|
||||
00:14:51.580 --> 00:15:02.090
|
||||
which means the organization had to revert to having members because there was no record in the minutes that members voted and approved the change.
|
||||
|
||||
00:15:02.990 --> 00:15:07.930
|
||||
Again, if it is not in the meeting minutes, then it never happened.
|
||||
|
||||
00:15:08.580 --> 00:15:09.060
|
||||
Right.
|
||||
|
||||
00:15:10.480 --> 00:15:12.290
|
||||
That is the bottom line.
|
||||
|
||||
00:15:14.510 --> 00:15:25.490
|
||||
It is important that members of any organization, whether a state agency or an affiliate,
|
||||
|
||||
00:15:26.430 --> 00:15:31.630
|
||||
understand these important governance documents.
|
||||
|
||||
00:15:34.680 --> 00:15:43.890
|
||||
Another interesting example is when COVID happened, which had a significant impact.
|
||||
|
||||
00:15:47.080 --> 00:16:03.810
|
||||
I went to an annual meeting for an organization and was approached about whether their Zoom meeting was valid.
|
||||
|
||||
00:16:05.610 --> 00:16:06.430
|
||||
Why?
|
||||
|
||||
00:16:06.430 --> 00:16:15.480
|
||||
Because their bylaws were silent about having meetings through video conferencing.
|
||||
|
||||
00:16:16.630 --> 00:16:26.280
|
||||
But their state law already allowed it.
|
||||
|
||||
00:16:26.660 --> 00:16:36.390
|
||||
They had included this provision before COVID, so the meeting was legally permitted.
|
||||
|
||||
00:16:38.560 --> 00:16:42.190
|
||||
Another state did not have it in their laws.
|
||||
|
||||
00:16:43.740 --> 00:16:53.440
|
||||
Robert's Rules of Order states that if video conferencing is not in the bylaws, you cannot have one.
|
||||
|
||||
00:16:54.530 --> 00:17:08.280
|
||||
It must be in the bylaws, so many legislators across the US started adding amendments to their state laws to include video conferencing.
|
||||
|
||||
00:17:08.510 --> 00:17:10.430
|
||||
So they could proceed with their meetings.
|
||||
|
||||
00:17:10.960 --> 00:17:16.280
|
||||
But that one state already had it in there before COVID.
|
||||
|
||||
00:17:20.590 --> 00:17:27.660
|
||||
There are variations in how to approach this. When I'm asked to serve as parliamentarian,
|
||||
|
||||
00:17:27.980 --> 00:17:35.160
|
||||
I have to find out which state the organization is incorporated in,
|
||||
|
||||
00:17:35.510 --> 00:17:46.160
|
||||
and if they provide that information, I will look up the non-profit laws in that state to read them.
|
||||
|
||||
00:17:46.810 --> 00:17:54.780
|
||||
I will ask for a copy of the AOI, though many organizations don't keep a copy.
|
||||
|
||||
00:17:56.090 --> 00:17:58.360
|
||||
That is an important document.
|
||||
|
||||
00:18:00.530 --> 00:18:07.190
|
||||
If I receive these documents, I will review them to see if there are any conflicts.
|
||||
|
||||
00:18:08.960 --> 00:18:15.340
|
||||
I review the bylaws, review any other rules, standing rules, etc.
|
||||
|
||||
00:18:15.690 --> 00:18:21.810
|
||||
I must see the whole picture before my assignment to work as a parliamentarian.
|
||||
|
||||
00:18:22.060 --> 00:18:27.810
|
||||
All governance documents should be there, minutes too.
|
||||
|
||||
00:18:28.010 --> 00:18:40.510
|
||||
I make sure everything is available and check if there's anything from previous business meetings that must be added to the bylaws.
|
||||
|
||||
00:18:42.590 --> 00:18:51.560
|
||||
I've encountered one or two organizations that did not keep good meeting minutes.
|
||||
|
||||
00:18:51.960 --> 00:19:04.380
|
||||
There were many amendments to the bylaws going back 10 or 12 years, but no record in the minutes.
|
||||
|
||||
00:19:04.710 --> 00:19:19.490
|
||||
That means the current bylaws are void because there is no record of the amendments in the minutes.
|
||||
|
||||
00:19:19.730 --> 00:19:26.660
|
||||
You have to go back to the bylaws from 12 years ago and follow those.
|
||||
|
||||
00:19:29.060 --> 00:19:39.440
|
||||
I have seen many organizations that don't realize the importance of these valuable documents.
|
||||
|
||||
00:19:43.830 --> 00:19:47.660
|
||||
So that is my message about these documents.
|
||||
|
||||
00:19:53.780 --> 00:20:06.810
|
||||
I would appreciate some clarification. Now that you've mentioned each state needs to review their state laws,
|
||||
|
||||
00:20:07.380 --> 00:20:18.630
|
||||
one good example would be that each state determines how often a non-profit must host a business meeting.
|
||||
|
||||
00:20:18.810 --> 00:20:24.380
|
||||
Some require meetings every year, some every two years.
|
||||
|
||||
00:20:24.540 --> 00:20:29.380
|
||||
Is that why it's important to review the state laws?
|
||||
|
||||
00:20:29.540 --> 00:20:36.690
|
||||
Yes, if your state law says you must have an annual meeting, that means every year.
|
||||
|
||||
00:20:37.260 --> 00:20:40.690
|
||||
If you do not follow that, you are breaking the law.
|
||||
|
||||
00:20:41.740 --> 00:20:42.340
|
||||
Okay.
|
||||
|
||||
00:20:45.080 --> 00:20:54.660
|
||||
But if your state law says it's left up to the organization to decide depending on their bylaws,
|
||||
|
||||
00:20:55.860 --> 00:21:00.530
|
||||
then the bylaws can state every two years and that's acceptable.
|
||||
|
||||
00:21:00.640 --> 00:21:09.940
|
||||
But if state law clearly states that an annual meeting means every year, you need to call for a meeting annually.
|
||||
|
||||
00:21:10.890 --> 00:21:14.630
|
||||
Okay. So again, that's why it's important to review the laws.
|
||||
|
||||
00:21:15.340 --> 00:21:28.330
|
||||
Now with AOI, you mentioned that it's really important to include the mission of the organization,
|
||||
|
||||
00:21:28.860 --> 00:21:32.610
|
||||
IRS information...
|
||||
|
||||
00:21:32.680 --> 00:21:33.040
|
||||
Clauses.
|
||||
|
||||
00:21:33.240 --> 00:21:34.660
|
||||
Clauses must be in there.
|
||||
|
||||
00:21:34.680 --> 00:21:37.660
|
||||
The specific code.
|
||||
|
||||
00:21:36.730 --> 00:21:45.190
|
||||
And must have a board of directors. All that information needs to be included in the AOI. Is that correct?
|
||||
|
||||
00:21:48.160 --> 00:21:55.290
|
||||
In the past, they would name the positions of board members.
|
||||
|
||||
00:21:55.530 --> 00:22:00.080
|
||||
Now that's not the case anymore; it's in the bylaws.
|
||||
|
||||
00:22:00.640 --> 00:22:11.910
|
||||
So if your state law says you must have the president, vice president, secretary, and treasurer, then it's required.
|
||||
|
||||
00:22:12.190 --> 00:22:22.110
|
||||
If your bylaws have president, vice president, and secretary/treasurer combined,
|
||||
|
||||
00:22:22.990 --> 00:22:24.380
|
||||
the AOI and bylaws are in conflict.
|
||||
|
||||
00:22:24.380 --> 00:22:24.940
|
||||
Conflict.
|
||||
|
||||
00:22:25.030 --> 00:22:31.210
|
||||
because there are two separate positions in AOI and it's one position in the bylaws.
|
||||
|
||||
00:22:31.930 --> 00:22:37.330
|
||||
Today's AOI would not typically include the positions.
|
||||
|
||||
00:22:37.540 --> 00:22:42.910
|
||||
In the past they did, but not anymore,
|
||||
|
||||
00:22:43.160 --> 00:22:47.730
|
||||
at least in the states I've worked with.
|
||||
|
||||
00:22:49.680 --> 00:22:54.260
|
||||
But it is important to include the IRS clauses.
|
||||
|
||||
00:22:55.010 --> 00:23:05.460
|
||||
I used a template the first time, sent it to the Secretary of State, and it was approved with no problems.
|
||||
|
||||
00:23:06.130 --> 00:23:14.490
|
||||
I got copies of the bylaws and AOI, sent them to the IRS for their review and approval for 501(c)(3).
|
||||
|
||||
00:23:14.790 --> 00:23:26.230
|
||||
They did not accept it and said I must insert specific clauses, so I added them to the AOI.
|
||||
|
||||
00:23:26.440 --> 00:23:28.910
|
||||
I sent it back and it was approved.
|
||||
|
||||
00:23:31.090 --> 00:23:31.410
|
||||
Okay.
|
||||
|
||||
00:23:32.490 --> 00:23:35.710
|
||||
Just put in the IRS code.
|
||||
|
||||
00:23:37.030 --> 00:23:47.810
|
||||
When an organization wants to approve AOI, what kind of vote is required for that to happen?
|
||||
|
||||
00:23:49.630 --> 00:23:50.780
|
||||
AOI?
|
||||
|
||||
00:23:51.010 --> 00:23:52.280
|
||||
(Nods in affirmation)
|
||||
|
||||
00:23:57.840 --> 00:24:02.240
|
||||
I have experienced two different approaches.
|
||||
|
||||
00:24:03.160 --> 00:24:10.830
|
||||
One way is a group gets together and they want to establish a non-profit organization.
|
||||
|
||||
00:24:11.110 --> 00:24:20.480
|
||||
They vote as a group, of course, by majority.
|
||||
|
||||
00:24:22.010 --> 00:24:26.180
|
||||
Normally all agree that they want the organization.
|
||||
|
||||
00:24:27.430 --> 00:24:37.410
|
||||
Then once they all agree, vote, and pass, they fill out the AOI application to submit it.
|
||||
|
||||
00:24:38.360 --> 00:24:43.730
|
||||
The other approach that I have seen is when an organization
|
||||
|
||||
00:24:45.590 --> 00:24:50.080
|
||||
finds three people that want to establish a non-profit.
|
||||
|
||||
00:24:51.580 --> 00:24:56.940
|
||||
Those three, since state law requires three.
|
||||
|
||||
00:24:57.310 --> 00:25:16.140
|
||||
So they apply for AOI with their three names, get approved, and announce to the community that they've established a new non-profit and will have a general meeting.
|
||||
|
||||
00:25:16.390 --> 00:25:24.130
|
||||
The purpose is to develop the mission, vision, values, and bylaws together.
|
||||
|
||||
00:25:24.260 --> 00:25:39.510
|
||||
People that want to join become members, pay dues, and start forward progress. Then that group develops everything with the bylaws.
|
||||
|
||||
00:25:40.430 --> 00:25:45.860
|
||||
Then you have everything you need to send to the IRS to submit for 501(c)(3).
|
||||
|
||||
00:25:46.790 --> 00:25:48.980
|
||||
So I have seen both approaches.
|
||||
|
||||
00:25:49.040 --> 00:25:50.060
|
||||
Either way.
|
||||
|
||||
00:25:50.130 --> 00:25:57.190
|
||||
What if the organization wants to amend their AOI? What kind of vote is required for that process?
|
||||
|
||||
00:25:57.460 --> 00:26:00.440
|
||||
Two-thirds (2/3).
|
||||
|
||||
00:26:02.290 --> 00:26:06.480
|
||||
With previous notice?
|
||||
|
||||
00:26:06.860 --> 00:26:15.960
|
||||
Previous notice with 2/3 or majority of the whole membership.
|
||||
|
||||
00:26:17.410 --> 00:26:18.240
|
||||
Either one of those.
|
||||
|
||||
00:26:20.340 --> 00:26:26.390
|
||||
If you amend the AOI, you have to amend the bylaws if necessary.
|
||||
|
||||
00:26:27.660 --> 00:26:29.590
|
||||
Because they cannot conflict.
|
||||
|
||||
00:26:28.380 --> 00:26:29.480
|
||||
Both.
|
||||
|
||||
00:26:31.790 --> 00:26:38.380
|
||||
Okay, so now the next document are the bylaws.
|
||||
|
||||
00:26:38.640 --> 00:26:50.460
|
||||
The bylaws will explain the structure of the organization, and expand on the responsibilities and duties of each board position.
|
||||
|
||||
00:26:55.090 --> 00:27:03.510
|
||||
For the bylaws, what is the voting process to accept them?
|
||||
|
||||
00:27:07.180 --> 00:27:09.360
|
||||
For the first time or to amend?
|
||||
|
||||
00:27:09.680 --> 00:27:12.830
|
||||
For the first time and then to amend.
|
||||
|
||||
00:27:14.380 --> 00:27:19.440
|
||||
For the first time, it will be majority of the votes.
|
||||
|
||||
00:27:19.730 --> 00:27:25.310
|
||||
Then to amend is two-thirds unless your bylaws say otherwise.
|
||||
|
||||
00:27:25.880 --> 00:27:32.540
|
||||
One organization required eighty (80) percent approval to amend.
|
||||
|
||||
00:27:32.990 --> 00:27:40.260
|
||||
Another organization required one hundred (100) percent, which means if one person voted against it, the whole proposal fails.
|
||||
|
||||
00:27:41.580 --> 00:27:47.180
|
||||
Normally two-thirds (2/3) according to Robert's Rules of Order is required to amend.
|
||||
|
||||
00:27:47.180 --> 00:28:02.990
|
||||
At the beginning, the members review it which requires majority, and then later change it to two-thirds (2/3).
|
||||
|
||||
00:28:04.830 --> 00:28:12.740
|
||||
You're right that the bylaws explain the structure, the responsibilities of the board and officers.
|
||||
|
||||
00:28:14.760 --> 00:28:24.410
|
||||
Some bylaws, for example, will list the specific duties for the role of Secretary and that's it.
|
||||
|
||||
00:28:24.840 --> 00:28:35.380
|
||||
Some organizations have a list in bylaws but need to go to the policies for a detailed list of responsibilities.
|
||||
|
||||
00:28:35.880 --> 00:28:39.160
|
||||
So the policy is separate from the bylaws.
|
||||
|
||||
00:28:41.010 --> 00:28:49.180
|
||||
The function and responsibilities of the Secretary in the bylaws might state to pass out the minutes,
|
||||
|
||||
00:28:49.180 --> 00:28:53.580
|
||||
keep a record of the names of all the members,
|
||||
|
||||
00:28:54.580 --> 00:29:04.790
|
||||
and to make sure that the minutes be posted on the web or distributed to all members.
|
||||
|
||||
00:29:05.280 --> 00:29:12.090
|
||||
If your bylaws say the minutes need to be sent by mail to all members,
|
||||
|
||||
00:29:12.660 --> 00:29:17.290
|
||||
then if you post on the web and not send it out, you violate the bylaws.
|
||||
|
||||
00:29:19.060 --> 00:29:33.190
|
||||
So the clause in the bylaws must be specific, like "the minutes will be posted on the web within 10 days after the board meeting."
|
||||
|
||||
00:29:34.630 --> 00:29:37.640
|
||||
It must be specific and not broad.
|
||||
|
||||
00:29:37.890 --> 00:29:48.460
|
||||
I have seen many bylaws that are very vague and they cause a lot of problems and mistakes in interpretation.
|
||||
|
||||
00:29:51.030 --> 00:30:01.280
|
||||
I have seen some bylaws with 30 to 40 pages with many of them mixing policy and procedures together.
|
||||
|
||||
00:30:01.540 --> 00:30:10.330
|
||||
You want to separate them, as shown in the triangle with the bylaws, and move policies down to their proper level.
|
||||
|
||||
00:30:14.330 --> 00:30:22.790
|
||||
Another important thing to include in the bylaws would be the membership dues - how much it costs?
|
||||
|
||||
00:30:23.080 --> 00:30:31.830
|
||||
And also, if the members want to amend the membership dues, that requires a two-thirds (2/3) vote?
|
||||
|
||||
00:30:31.990 --> 00:30:39.410
|
||||
Bylaws, again, define the structure, with different categories:
|
||||
|
||||
00:30:39.780 --> 00:30:50.680
|
||||
regular members, senior citizens, hearing members, and so on - all those categories.
|
||||
|
||||
00:30:51.230 --> 00:30:56.330
|
||||
The fees and dues are in the standing rules.
|
||||
|
||||
00:30:56.380 --> 00:30:59.260
|
||||
In the standing rules and not in the bylaws?
|
||||
|
||||
00:30:59.330 --> 00:31:04.060
|
||||
No, the standing rules will list them.
|
||||
|
||||
00:31:05.290 --> 00:31:13.110
|
||||
I look at the National Association of Parliamentarians and their bylaws are silent on dues.
|
||||
|
||||
00:31:13.640 --> 00:31:24.190
|
||||
The standing rules will have it since the board themselves can adjust the dues.
|
||||
|
||||
00:31:26.080 --> 00:31:39.340
|
||||
So if during a business meeting, a motion to increase the dues by $5 is passed, it will go into the standing rules and modify it.
|
||||
|
||||
00:31:39.780 --> 00:31:42.960
|
||||
The bylaws stay the same.
|
||||
|
||||
00:31:43.860 --> 00:31:55.040
|
||||
That is a very common thing that I've noticed in various state associations - their bylaws include their member dues.
|
||||
|
||||
00:31:55.290 --> 00:31:56.790
|
||||
So that was good to clarify.
|
||||
|
||||
00:31:56.060 --> 00:32:00.990
|
||||
It says they will pay their member dues, but how much is in the standing rules.
|
||||
|
||||
00:32:03.440 --> 00:32:04.630
|
||||
Thank you for the clarification.
|
||||
|
||||
00:32:07.090 --> 00:32:17.660
|
||||
You also mentioned that if you want to vote electronically, you must include that in the bylaws itself.
|
||||
|
||||
00:32:18.980 --> 00:32:21.080
|
||||
If it is not in the bylaws, then you cannot.
|
||||
|
||||
00:32:22.710 --> 00:32:32.410
|
||||
Paper voting by ballot ensures privacy so people will not know who voted for what.
|
||||
|
||||
00:32:33.230 --> 00:32:38.630
|
||||
If the bylaws state you must use ballots, you
|
||||
583
public/subtitles/secretary-role-responsibilities.vtt
Normal file
583
public/subtitles/secretary-role-responsibilities.vtt
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
WEBVTT
|
||||
|
||||
00:00:02.310 --> 00:00:13.680
|
||||
Hello, this is Eliza Kragh and we're here today with Mark Apodaca who is an expert in parliamentary procedure.
|
||||
|
||||
00:00:13.880 --> 00:00:18.780
|
||||
Today we will be discussing The Role of a Secretary.
|
||||
|
||||
00:00:19.980 --> 00:00:24.360
|
||||
Mark, please share with us about the role of a secretary.
|
||||
|
||||
00:00:26.330 --> 00:00:27.690
|
||||
Okay.
|
||||
|
||||
00:00:30.680 --> 00:00:45.940
|
||||
Often the bylaws and policy procedures will list the responsibilities of a secretary.
|
||||
|
||||
00:00:49.040 --> 00:00:59.190
|
||||
Some bylaws will have a long list of responsibilities and some will have three listed.
|
||||
|
||||
00:01:01.530 --> 00:01:18.530
|
||||
I have seen both where either bylaws or policy procedures will list the responsibilities.
|
||||
|
||||
00:01:21.060 --> 00:01:38.860
|
||||
Robert's Rules of Order, itself, has a list of some of the responsibilities, but it is important to know what your governance documents mention about the role itself.
|
||||
|
||||
00:01:41.590 --> 00:01:46.410
|
||||
It also includes what a secretary can do and cannot do.
|
||||
|
||||
00:01:48.710 --> 00:02:00.440
|
||||
One good example is when going to a business meeting where the secretary shows up but the president or vice president has not,
|
||||
|
||||
00:02:00.860 --> 00:02:12.760
|
||||
the secretary can start the meeting until the president or vice president shows up to take over, which can happen.
|
||||
|
||||
00:02:13.690 --> 00:02:27.140
|
||||
Another thing is it's important during the meeting to always have both president and secretary there, as it is required in RONR.
|
||||
|
||||
00:02:30.740 --> 00:02:40.060
|
||||
I have seen some secretaries write a 35 page paper of minutes.
|
||||
|
||||
00:02:41.590 --> 00:02:57.590
|
||||
It is not necessary to make a story of what happened during the meeting about they said this, they said that and so on.
|
||||
|
||||
00:02:57.590 --> 00:03:00.430
|
||||
That is not required for minutes.
|
||||
|
||||
00:03:02.040 --> 00:03:16.330
|
||||
The result, the motion, seconded - for seconded, you don't need the name of the person, but do need the name of who made the motions.
|
||||
|
||||
00:03:16.330 --> 00:03:21.130
|
||||
Do not include the debate of who said what.
|
||||
|
||||
00:03:21.880 --> 00:03:38.430
|
||||
Then vote - the vote must show the numbers to support or against / passed or failed.
|
||||
|
||||
00:03:38.610 --> 00:03:52.990
|
||||
All motions or action items that were proposed must be in the minutes so we will not forget that they happened.
|
||||
|
||||
00:03:54.830 --> 00:03:56.680
|
||||
That is the basic role.
|
||||
|
||||
00:03:59.060 --> 00:04:06.480
|
||||
I am going to pull up a powerpoint to show some rules from RONR.
|
||||
|
||||
00:04:42.090 --> 00:04:52.490
|
||||
The responsibilities of a secretary in RONR 47:33 - that is where you will find it.
|
||||
|
||||
00:04:56.060 --> 00:05:06.160
|
||||
Number 1 is the most important one and that is to keep a record of the minutes.
|
||||
|
||||
00:05:09.230 --> 00:05:28.640
|
||||
What the secretary puts down and then when it is approved or accepted as is or with corrections, the minutes become a legal document.
|
||||
|
||||
00:05:29.830 --> 00:05:45.990
|
||||
Often in court, the judge will ask to see the minutes and if you do not provide the minutes or if it is not in the minutes, the judge will deny.
|
||||
|
||||
00:05:47.860 --> 00:05:52.710
|
||||
The second thing is to keep a file of all committee reports.
|
||||
|
||||
00:05:56.330 --> 00:06:16.460
|
||||
It is important that the organization has committees and most popular is finance, governance, or strategy plan of the members, and fundraising.
|
||||
|
||||
00:06:16.610 --> 00:06:33.590
|
||||
All those committees are very important and they meet in between board meetings: board meeting, committee meeting, and then board meeting.
|
||||
|
||||
00:06:33.980 --> 00:06:44.060
|
||||
Each of those committees will give the secretary the minutes, reports from their meeting.
|
||||
|
||||
00:06:44.530 --> 00:06:55.280
|
||||
The secretary is responsible to review them and make sure that they are in order to put them in files.
|
||||
|
||||
00:06:57.890 --> 00:07:12.960
|
||||
The reason is that, for example, you are on the fundraising committee and I'm on the finance committee - two different committees,
|
||||
|
||||
00:07:13.860 --> 00:07:27.010
|
||||
now I want to know as a board what is up with the other committee that I'm not involved with when the next board meeting comes up?
|
||||
|
||||
00:07:27.990 --> 00:07:38.090
|
||||
The board will get a copy of the committee minutes and report to review.
|
||||
|
||||
00:07:38.240 --> 00:07:51.310
|
||||
I may have some questions about the fundraising committee that you as chair of the finance committee can answer.
|
||||
|
||||
00:07:51.310 --> 00:08:06.890
|
||||
While reading the reports and there are no questions, then the president or the chair will state "any questions about the minutes from the fundraising committee?"
|
||||
|
||||
00:08:07.280 --> 00:08:18.440
|
||||
If there are no questions, then file it and move on since reading it is not required.
|
||||
|
||||
00:08:19.140 --> 00:08:22.560
|
||||
So that is another important role of the secretary.
|
||||
|
||||
00:08:23.910 --> 00:08:35.410
|
||||
The fourth one is to keep a list of all the members.
|
||||
|
||||
00:08:40.930 --> 00:09:04.030
|
||||
For example, suppose that an organization has 500 members and the secretary must notify all 500 members about the upcoming annual meeting.
|
||||
|
||||
00:09:07.160 --> 00:09:16.030
|
||||
If the secretary sends it out and misses 10 people, you cannot have a meeting.
|
||||
|
||||
00:09:17.560 --> 00:09:19.390
|
||||
Oh, really?
|
||||
|
||||
00:09:19.440 --> 00:09:30.330
|
||||
Oh no! Every member on record must get notice that there is a meeting.
|
||||
|
||||
00:09:31.640 --> 00:09:49.460
|
||||
If you miss one member, you cannot have a meeting because they object "they had a meeting and voted on it but I was not there because I did not get notice."
|
||||
|
||||
00:09:50.290 --> 00:09:53.630
|
||||
It could become a legal issue.
|
||||
|
||||
00:09:56.440 --> 00:10:15.990
|
||||
I remember one organization where a group of members came together and they invited officers but the officers refused to come
|
||||
|
||||
00:10:17.210 --> 00:10:40.090
|
||||
and what they decided was that the officers refused to follow, so they saw a lawyer and the lawyer asked the first question if they kept a list of all the members' names.
|
||||
|
||||
00:10:41.410 --> 00:10:46.490
|
||||
Did you contact all those members - ALL the members?
|
||||
|
||||
00:10:48.780 --> 00:10:59.530
|
||||
The person who set the meeting admitted no, so the lawyer could not do anything.
|
||||
|
||||
00:10:59.680 --> 00:11:07.960
|
||||
Your meeting is null and void because not all the members were aware of the meeting.
|
||||
|
||||
00:11:09.930 --> 00:11:20.260
|
||||
When you pay dues to be a member, you have every right to know when the meeting is happening.
|
||||
|
||||
00:11:20.260 --> 00:11:30.230
|
||||
That is an important role of the secretary to make sure that the list of members is always updated.
|
||||
|
||||
00:11:32.380 --> 00:11:47.540
|
||||
The fourth one is the minutes must be available to those that ask for them and you cannot abuse it.
|
||||
|
||||
00:11:48.690 --> 00:12:16.680
|
||||
I have seen minutes that are required by RONR to be shown, especially if special rules of order state otherwise, unless the special rules of order are silent on it,
|
||||
|
||||
00:12:16.990 --> 00:12:31.030
|
||||
then RONR states that you must provide them if I approach the secretary of that organization to see the meeting minutes, which you have to show.
|
||||
|
||||
00:12:31.030 --> 00:12:41.680
|
||||
It is nicer and saves time and headache if the minutes are put on the organization's website and are right there to read
|
||||
|
||||
00:12:41.890 --> 00:12:49.160
|
||||
and then if you have questions, go to the next meeting and bring it up.
|
||||
|
||||
00:12:49.160 --> 00:12:49.890
|
||||
The next one will be:
|
||||
|
||||
00:12:58.960 --> 00:13:11.680
|
||||
Notify the officers and member delegates of their election appointment, give the documents to the committee,
|
||||
|
||||
00:13:14.680 --> 00:13:26.590
|
||||
require a performance of jobs, and have everything ready to hand over.
|
||||
|
||||
00:13:26.590 --> 00:13:49.040
|
||||
Number six is credentials - go to the organization line and sign delegates' credentials and give to someone with a badge or delegate to go in and sign.
|
||||
|
||||
00:13:52.230 --> 00:13:58.780
|
||||
Number seven, sign all certified copies of actions of the organization.
|
||||
|
||||
00:13:59.140 --> 00:14:17.130
|
||||
What they vote on and motion that was passed, the action taken must be certified by documenting it in the minutes to certify that action.
|
||||
|
||||
00:14:19.430 --> 00:14:30.640
|
||||
Keep good records, books, bylaws, special rules and all those document amendments with proper records.
|
||||
|
||||
00:14:34.090 --> 00:14:46.840
|
||||
Number nine is to send members notice of the next meeting.
|
||||
|
||||
00:14:48.160 --> 00:14:57.780
|
||||
If you miss one, then you cannot really have the meeting because all must receive notice.
|
||||
|
||||
00:14:58.560 --> 00:15:09.760
|
||||
Number ten is to prepare, really work with the president or whoever is running the meeting to develop the agenda, order of business.
|
||||
|
||||
00:15:12.980 --> 00:15:20.480
|
||||
Just sit down with the managing officer to help develop those and plan for the next meeting.
|
||||
|
||||
00:15:22.860 --> 00:15:26.610
|
||||
The last one is when you have an absent president and vice president,
|
||||
|
||||
00:15:26.910 --> 00:15:45.640
|
||||
the secretary can call the meeting to order until someone is elected to chair the meeting - pro temp.
|
||||
|
||||
00:15:47.660 --> 00:16:00.110
|
||||
Those are some of the responsibilities from RONR that are not all in the bylaws.
|
||||
|
||||
00:16:02.660 --> 00:16:15.190
|
||||
minutes, giving members notice, and lastly keeping an updated members list.
|
||||
|
||||
00:16:15.190 --> 00:16:24.330
|
||||
Those are the important functions and to gather reports to send out for the board to review.
|
||||
|
||||
00:16:27.260 --> 00:16:34.510
|
||||
Those all are basic responsibilities of the secretary.
|
||||
|
||||
00:16:36.630 --> 00:16:50.730
|
||||
It is worth keeping documents of any bylaw amendments because if you do not document them in the minutes, it means they never happened.
|
||||
|
||||
00:16:52.760 --> 00:16:59.680
|
||||
unless you make corrections to the minutes.
|
||||
|
||||
00:16:59.680 --> 00:17:15.090
|
||||
If there are no corrections and the minutes are accepted, then that means you can go back and make corrections in the minutes back in time.
|
||||
|
||||
00:17:15.710 --> 00:17:18.410
|
||||
It does not stop there.
|
||||
|
||||
00:17:18.410 --> 00:17:20.530
|
||||
Is there a time limit?
|
||||
|
||||
00:17:20.660 --> 00:17:21.830
|
||||
No, there is no time limit.
|
||||
|
||||
00:17:25.080 --> 00:17:43.230
|
||||
Again, just one organization with 10-12 years amendment after amendment every two years and you're talking about 5 business meetings,
|
||||
|
||||
00:17:43.340 --> 00:17:53.880
|
||||
records amended in minutes but you have nothing, which means it did not happen.
|
||||
|
||||
00:17:54.030 --> 00:18:04.430
|
||||
It means you have to go back to the beginning and follow the original bylaws.
|
||||
|
||||
00:18:05.130 --> 00:18:09.440
|
||||
Some organization secretaries do not keep good records.
|
||||
|
||||
00:18:10.180 --> 00:18:21.590
|
||||
I am a member of an organization and I'm never informed of when my membership is due.
|
||||
|
||||
00:18:22.480 --> 00:18:32.480
|
||||
I mean, they inform me that they have a business meeting coming, but my membership dues expired two years ago and I've been waiting for it.
|
||||
|
||||
00:18:33.410 --> 00:18:41.560
|
||||
So if I come to the meeting, technically I am not a member.
|
||||
|
||||
00:18:41.810 --> 00:18:56.080
|
||||
One organization has "lifetime members" and they kept records of those members who paid up for lifetime members.
|
||||
|
||||
00:18:57.430 --> 00:19:09.540
|
||||
For those who did not want lifetime membership and prefer yearly renewals, there are no records of those members.
|
||||
|
||||
00:19:15.040 --> 00:19:26.340
|
||||
I ask you to call a meeting and you only list the lifetime members—what about all the rest of the members?
|
||||
|
||||
00:19:27.290 --> 00:19:35.890
|
||||
They are members and no notice, no records, nothing?
|
||||
|
||||
00:19:35.890 --> 00:19:37.790
|
||||
The secretary did not do their job.
|
||||
|
||||
00:19:42.360 --> 00:19:52.210
|
||||
Yeah, I have seen some meetings where the secretary used their phone and took notes.
|
||||
|
||||
00:19:52.580 --> 00:19:53.440
|
||||
Their phone?
|
||||
|
||||
00:19:56.280 --> 00:20:00.510
|
||||
I have a notepad so I fill it in properly.
|
||||
|
||||
00:20:06.740 --> 00:20:23.830
|
||||
I would encourage using a laptop during board meetings or annual meetings to finish it and send it to those board members.
|
||||
|
||||
00:20:24.140 --> 00:20:32.410
|
||||
You know that many organizations will establish a committee with three people to review the minutes,
|
||||
|
||||
00:20:34.080 --> 00:20:47.330
|
||||
because if the conference or annual meeting is one year from now, it means the secretary will type it all up and hold it for one year?
|
||||
|
||||
00:20:47.880 --> 00:20:51.380
|
||||
You think people will remember what happened one year ago?
|
||||
|
||||
00:20:53.490 --> 00:21:03.230
|
||||
So set up a committee, ad hoc committee, and they will receive the minutes from the secretary and
|
||||
|
||||
00:21:03.390 --> 00:21:12.480
|
||||
they will review and approve it and will inform the secretary and board president that they are accepted
|
||||
|
||||
00:21:12.480 --> 00:21:22.930
|
||||
Then at the next board meeting, the board approves the minutes that were reviewed for the next annual meeting,
|
||||
|
||||
00:21:23.990 --> 00:21:29.910
|
||||
During the next annual meeting, members do not need to approve the minutes
|
||||
|
||||
00:21:30.960 --> 00:21:39.510
|
||||
because the committee already did the work and will let you know that the committee finished.
|
||||
|
||||
00:21:39.630 --> 00:21:46.310
|
||||
You picked the people last year to review everything, all good, then finished and passed by the board.
|
||||
|
||||
00:21:47.160 --> 00:21:52.190
|
||||
That will be in the procedure policy guidelines.
|
||||
|
||||
00:21:58.590 --> 00:22:03.160
|
||||
Again, it is very important to keep a copy of the minutes.
|
||||
|
||||
00:22:05.440 --> 00:22:15.880
|
||||
I have seen some secretaries with two years of no minutes who then decide not to run again
|
||||
|
||||
00:22:18.760 --> 00:22:21.790
|
||||
and they do not give the minutes to the next secretary.
|
||||
|
||||
00:22:25.830 --> 00:22:27.940
|
||||
They need the minutes!
|
||||
|
||||
00:22:28.010 --> 00:22:30.090
|
||||
It is supposed to be passed on.
|
||||
|
||||
00:22:30.230 --> 00:22:32.210
|
||||
Some organizations forget that.
|
||||
|
||||
00:22:34.890 --> 00:22:44.590
|
||||
It is not good and organizations should have a centralized place with all governance documents and all the minutes and everything.
|
||||
|
||||
00:22:44.860 --> 00:22:58.560
|
||||
Have a safe place for it and you can use it in the future if something legal comes up and you need to review past events, then it is ready right there.
|
||||
|
||||
00:22:59.430 --> 00:23:08.510
|
||||
But some organizations are not very careful and they lose it in the cracks.
|
||||
|
||||
00:23:12.380 --> 00:23:13.230
|
||||
It is not good.
|
||||
|
||||
00:23:14.730 --> 00:23:18.360
|
||||
What happens if you lose the meeting minutes?
|
||||
|
||||
00:23:20.630 --> 00:23:23.360
|
||||
It can mean the meeting never happened.
|
||||
|
||||
00:23:27.830 --> 00:23:30.930
|
||||
Very important to have the minutes.
|
||||
|
||||
00:23:28.330 --> 00:23:31.440
|
||||
Really, you had a meeting and where are the minutes?
|
||||
|
||||
00:23:32.760 --> 00:23:43.490
|
||||
Where are the minutes? Minutes should open with the name of the organization, call to order, time, date, and so forth,
|
||||
|
||||
00:23:44.940 --> 00:24:00.780
|
||||
to meet quorum, reports, motions, new business, adjournment—simply all there.
|
||||
|
||||
00:24:02.090 --> 00:24:21.380
|
||||
No minutes? Hello, where's the proof? You have quorum, the minutes show if you meet quorum, motions passed,
|
||||
|
||||
00:24:22.890 --> 00:24:26.810
|
||||
If we do not have the minutes, technically the meeting never happened.
|
||||
|
||||
00:24:29.580 --> 00:24:35.040
|
||||
The court will say nothing, which means the courts will not waste their time.
|
||||
|
||||
00:24:38.140 --> 00:24:41.460
|
||||
If you do not list it, then it never happened.
|
||||
|
||||
00:24:46.310 --> 00:24:50.590
|
||||
That is the basic role of the secretary.
|
||||
|
||||
00:24:51.280 --> 00:25:01.540
|
||||
I have been secretary for one parliamentary organization and I had the role of the secretary and treasurer
|
||||
|
||||
00:25:03.180 --> 00:25:14.890
|
||||
so I had to oversee and document everything about parliament, president, members, and I had to write everything depending on interpreters.
|
||||
|
||||
00:25:15.340 --> 00:25:21.210
|
||||
I would list motions, seconds, votes passed and so forth until it is finished,
|
||||
|
||||
00:25:22.040 --> 00:25:34.790
|
||||
and then present it to members for review and acceptance, make corrections if needed, and then sign and file it.
|
||||
|
||||
00:25:34.790 --> 00:25:41.090
|
||||
So if the national organization wants to see a copy of the minutes, then you must have them ready.
|
||||
|
||||
00:25:43.880 --> 00:25:51.640
|
||||
That parliamentary organization is very specific and detailed in making sure everything is there.
|
||||
|
||||
00:25:54.240 --> 00:26:02.690
|
||||
If the bylaws are there, then it is worry free.
|
||||
|
||||
00:26:04.930 --> 00:26:08.840
|
||||
If you do not have it, then oh boy.
|
||||
|
||||
00:26:11.260 --> 00:26:14.960
|
||||
That is basically about the role of secretary, which is important.
|
||||
|
||||
00:26:16.140 --> 00:26:34.230
|
||||
It is nice if the secretary has some writing skills, maybe take a class at college or an extended program on how to write better minutes
|
||||
|
||||
00:26:35.180 --> 00:26:41.010
|
||||
and it is not about storytelling skills.
|
||||
|
||||
00:26:43.410 --> 00:26:51.180
|
||||
Many come in with no idea what their role is and what they should fill out.
|
||||
|
||||
00:26:52.780 --> 00:26:57.740
|
||||
It is often that I will nudge the secretary that they do not need a story,
|
||||
|
||||
00:26:57.740 --> 00:27:04.060
|
||||
just document specific things and follow RONR and that is it.
|
||||
|
||||
00:27:07.710 --> 00:27:14.160
|
||||
To basically sum it up, the role and responsibilities of a secretary is to
|
||||
|
||||
00:27:14.180 --> 00:27:20.940
|
||||
make sure that everything is documented, all actions that happened,
|
||||
|
||||
00:27:22.360 --> 00:27:24.980
|
||||
Vote passed or failed.
|
||||
|
||||
00:27:20.980 --> 00:27:23.540
|
||||
not a story.
|
||||
|
||||
00:27:25.380 --> 00:27:30.240
|
||||
Satisfy quorum, who is there,
|
||||
|
||||
00:27:31.980 --> 00:27:38.210
|
||||
If president and vice president are not there, the secretary can start the meeting,
|
||||
|
||||
00:27:39.060 --> 00:27:48.090
|
||||
can make sure to inform ALL members ahead of the meeting,
|
||||
|
||||
00:27:49.110 --> 00:28:00.880
|
||||
needs to make sure they keep records of all meeting minutes in files ready to provide to the board or members when requested
|
||||
|
||||
00:28:02.060 --> 00:28:09.210
|
||||
and have all committee reports in files too.
|
||||
|
||||
00:28:12.840 --> 00:28:18.210
|
||||
That basically covers the responsibilities of the secretary?
|
||||
|
||||
00:28:19.930 --> 00:28:26.530
|
||||
Yes, if it's too much, often they will have an assistant secretary.
|
||||
|
||||
00:28:27.290 --> 00:28:38.310
|
||||
not a board member, that can help the secretary work with duties if they are overwhelmed.
|
||||
|
||||
00:28:38.310 --> 00:28:52.160
|
||||
We've talked about small organizations, but if a huge organization with maybe 5000 members, then the secretary needs an assistant to help.
|
||||
|
||||
00:28:53.830 --> 00:28:56.690
|
||||
Again, it depends on the size, if large or small.
|
||||
|
||||
00:29:00.440 --> 00:29:07.580
|
||||
Because meeting minutes are a legal document, that is a very big responsibility for the secretary
|
||||
|
||||
00:29:08.180 --> 00:29:15.210
|
||||
and that is why it is so important for the secretary to know what needs to be involved in the meeting minutes.
|
||||
|
||||
00:29:15.210 --> 00:29:26.810
|
||||
For those secretaries that have no idea on how to develop minutes, they have several different options and can go online to learn more about templates.
|
||||
|
||||
00:29:31.490 --> 00:29:39.540
|
||||
NAP provides examples too, I believe, on what secretary meeting minutes can look like.
|
||||
|
||||
00:29:39.590 --> 00:29:44.710
|
||||
They pretty much look the same with AIP (American Institute of Parliamentarians)
|
||||
|
||||
00:29:50.110 --> 00:29:58.080
|
||||
which has a list of roles and responsibilities and it is the same as RONR
|
||||
|
||||
00:29:59.880 --> 00:30:00.280
|
||||
same
|
||||
|
||||
00:30:02.710 --> 00:30:15.590
|
||||
So I would strongly recommend for those who become secretary to take initiative to start researching themselves
|
||||
|
||||
00:30:15.660 --> 00:30:24.530
|
||||
so they can align with the mission of what the meeting minutes should look like
|
||||
|
||||
00:30:24.530 --> 00:30:40.290
|
||||
A person can go into NAP website, you can get 12th edition or this pamphlet.
|
||||
|
||||
00:30:40.760 --> 00:30:42.580
|
||||
Short and simple.
|
||||
|
||||
00:30:42.830 --> 00:30:46.280
|
||||
These focus on the secretary.
|
||||
|
||||
00:30:49.160 --> 00:30:58.610
|
||||
AIP has different books on how to write effective minutes.
|
||||
|
||||
00:31:00.590 --> 00:31:19.190
|
||||
AIP books can be purchased on Amazon since they do not sell books themselves and refer people to Amazon.
|
||||
|
||||
00:31:19.190 --> 00:31:23.960
|
||||
AIP also has many good resources.
|
||||
|
||||
00:31:24.340 --> 00:31:29.130
|
||||
I use them and I am a member of AIP.
|
||||
|
||||
00:31:31.180 --> 00:31:43.180
|
||||
NAP has a website that you can go into their book store and look for any information about secretary like this pamphlet.
|
||||
|
||||
00:31:44.660 --> 00:31:45.530
|
||||
Another one is:
|
||||
|
||||
00:32:01.890 --> 00:32:01.910
|
||||
which is also sold on Amazon.
|
||||
|
||||
00:32:06.980 --> 00:32:23.690
|
||||
The person who wrote this is certified by both NAP and AIP organizations, so they combined them into one and all the information should be in this.
|
||||
|
||||
00:32:24.560 --> 00:32:26.090
|
||||
It is easy to read and follow.
|
||||
|
||||
00:32:27.340 --> 00:32:40.440
|
||||
So I would encourage all the organization secretaries to get those books and become a great secretary.
|
||||
|
||||
00:32:40.440 --> 00:32:47.690
|
||||
So resources are available out there and people just have to take the initiative and have the passion for it.
|
||||
|
||||
00:32:48.430 --> 00:32:55.160
|
||||
I am always looking at different ways to improve
|
||||
|
||||
00:32:57.010 --> 00:33:05.730
|
||||
to improve my understanding in finances, in non-profits, with grants and multiple other things.
|
||||
|
||||
00:33:06.110 --> 00:33:15.960
|
||||
I research and find resources because I want to know what my role is and how to do it.
|
||||
|
||||
00:33:17.860 --> 00:33:22.510
|
||||
It is an investment into my education and I make sure to practice it.
|
||||
|
||||
00:33:23.160 --> 00:33:28.810
|
||||
It is worth it and you become an extremely valuable member of the community.
|
||||
|
||||
00:33:31.210 --> 00:33:37.130
|
||||
That would be very beneficial not just for the secretary but for the organization itself,
|
||||
|
||||
00:33:37.290 --> 00:33:44.890
|
||||
so if anything comes up, then you are ready to provide because the secretary does their job.
|
||||
|
||||
00:33:45.210 --> 00:33:49.790
|
||||
That is right. Do it right if you want to do it.
|
||||
|
||||
00:33:50.780 --> 00:33:55.710
|
||||
Right, and then not have to go back and fix everything.
|
||||
|
||||
00:33:57.640 --> 00:34:12.480
|
||||
Yes, I have often seen secretaries type and then learn that they do not need all those things and delete what is not required,
|
||||
|
||||
00:34:12.610 --> 00:34:15.630
|
||||
clean it up and then present it to the members for approval.
|
||||
|
||||
00:34:17.660 --> 00:34:22.830
|
||||
That is a great option to know that you can go back and clean up so you can go forward.
|
||||
|
||||
00:34:23.860 --> 00:34:25.140
|
||||
Yup, you can.
|
||||
|
||||
00:34:26.640 --> 00:34:33.710
|
||||
That very clearly explains the specifics of the role and responsibilities of the secretary.
|
||||
|
||||
00:34:34.180 --> 00:34:45.580
|
||||
I hope this will help improve meeting minutes out there to make it easier for all to understand exactly what happens.
|
||||
|
||||
00:34:46.990 --> 00:34:48.730
|
||||
Thank you, Mark, for the clarification.
|
||||
|
||||
00:34:51.310 --> 00:34:52.440
|
||||
Thank you.
|
||||
321
public/transcriptions/governance-documents.txt
Normal file
321
public/transcriptions/governance-documents.txt
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
Governance Documents: Understanding the Hierarchy
|
||||
|
||||
The frame is divided vertically into two equal sections, like a split screen. The left side displays Eliza Kragh, a woman with short brown hair wearing a dark top and a pendant necklace. Behind her is a light-colored, plain wall. On the right side, Mark Apodaca, a man with glasses and a gray beard is visible. He is wearing a dark sweatshirt over a red collared shirt. He is seated in a dark chair, and a portion of a wooden cabinet is visible behind him on the right. The wall behind him is also a light color, similar to the one on the left side.
|
||||
|
||||
Eliza: "Hello, this is Eliza Kragh here today with Mark Apodaca, who is an expert Professional Registered Parliamentarian. We have an exciting topic to discuss today and will cover governance documents."
|
||||
|
||||
Eliza: "Hi Mark, thank you for your time. Please share about governance documents today."
|
||||
|
||||
Mark: "I will be showing a triangle and will name each part of it, then take it down and discuss it in detail."
|
||||
|
||||
Eliza: "Beautiful!"
|
||||
|
||||
The scene shows a computer interface displaying a presentation slide. The slide shows a triangle diagram titled "Governance Documents" with different levels labeled from top to bottom. Mark is sharing his screen while both participants are visible in smaller video windows.
|
||||
|
||||
Mark: "These documents apply to corporations, non-profits, partnership businesses, and some government entities and universities. They all have governance documents. For today, I am focusing specifically on non-profit organizations."
|
||||
|
||||
Mark: "First, you can see the top of the pyramid. Laws or statutes - state level. Next is Articles of Incorporation, then Bylaws, Special Rules of Order, Rules of Order, Standing Rules, and Policies and Procedures, and last one on the bottom are Customs."
|
||||
|
||||
Mark: "Laws at the top are the most important and have the highest authority. I'm going to stop sharing now."
|
||||
|
||||
Eliza: "Okay."
|
||||
|
||||
The screen returns to the split view of Eliza and Mark.
|
||||
|
||||
Mark: "Every state has its own non-profit laws. They are not the same and they're all different. What is New Mexico state law for non-profits is not the same as MN or MT or KS. Each has their own with their own legislative passing laws. These are the most authoritative laws."
|
||||
|
||||
Mark: "The second one is the Articles of Incorporation. If you want to set up a 501(c)(3) non-profit organization, you must file Articles of Incorporation with the Secretary of State. Most states around the country require it to be filed with the Secretary of State, but some have exceptions."
|
||||
|
||||
Mark: "In the Articles of Incorporation, you identify the name of the organization, the mission and purpose, and who the directors of the organization are. Most of the time you will name three directors. Directors do not have to be president or vice president. Directors can be anyone."
|
||||
|
||||
Mark: "An important point when filing Articles of Incorporation: you must have a paragraph with specific IRS clauses. The reason is when you file a 501(c)(3) application, the IRS requires a copy of the AOI and bylaws. If it does not show two important clauses, then the IRS will not accept your application for 501(c)(3). So make sure to include these clauses."
|
||||
|
||||
Mark: "Often times, the state will give a template to fill out, but that is not good enough because many people use that template, submit it, and get denied because it doesn't include what the IRS requires. So when you file AOI, make sure those clauses are included."
|
||||
|
||||
Mark: "With the AOI, you send it to the Secretary of State who will check to see if the AOI and state law are in compliance with no contradictions between them. If the Secretary of State finds that a clause conflicts with state law, they will deny and send it back saying they do not accept it because the state law says otherwise. This means you need to change that and ensure nothing conflicts with state law to get approval. So AOI is the second highest authority under state law."
|
||||
|
||||
Mark: "The third one is Bylaws. The Bylaws cannot conflict with the AOI and cannot conflict with the state law either."
|
||||
|
||||
Mark: "The fourth one is Special Rules of Order. I will give you one good example. You go to a business meeting as a member and someone makes a motion, it's seconded, and debate is open. Each person can debate for 10 minutes. 10 minutes, two times. That means I can debate in support, then someone debates against, and each time is 10 minutes. If no one else new wants to speak in support, then the same person can speak again a second time for 10 minutes. Suppose you had 200 members in a meeting and each has 10 minutes..."
|
||||
|
||||
Eliza: "Meeting drags on."
|
||||
|
||||
Mark: "It drags on all night and all day. So Special Rules of Order must be passed by the members. If you want to limit debate to 3 minutes per person, one time each for support and against, alternating, and then call for a vote... That is called Special Rules because it will override Robert's Rules of Order. If you do not have that, then you follow RONR."
|
||||
|
||||
Mark: "Then the next authority we have is Rules of Order. That can be Robert's Rules of Order, Newly Revised."
|
||||
|
||||
Eliza: "12th edition."
|
||||
|
||||
Mark: "12th edition, which is this book. If your bylaws state that RONR is your parliamentary authority, then you need to follow the book. Some organizations, though not many, use a different book."
|
||||
|
||||
Mark: "Standard Code of Parliamentary Procedure."
|
||||
|
||||
Eliza: "Other organizations use different parliamentary authorities."
|
||||
|
||||
Mark: "Yes, this is from the American Institute of Parliamentarians. That is different from the National Association of Parliamentarians. Many parliamentarians know both books. I am a member of both organizations. Some parliamentarians are certified in both."
|
||||
|
||||
Mark: "Again, the parliamentarian gets a copy of the bylaws and reads it, and if it states RONR, they follow it. If it states AIP with the Standard Code of Parliamentary Procedure, then you follow that. Sometimes organizations will use the Standard Code for Rules of Order that will supersede RONR, which can happen."
|
||||
|
||||
Mark: "The next one is Standing Rules, Policies, and Procedures, investment policies, finance policy, etc. At that level, do not put them in the bylaws. The bylaws explain the structure of the organization. Policies and procedures need to be separate documents."
|
||||
|
||||
Mark: "The last one is Customs. It is something that has been used for many years. But again, if you go to a business meeting and find a custom that conflicts with RONR, RONR beats customs and you must follow RONR, except sometimes people will elevate customs into Special Rules of Order. It happens."
|
||||
|
||||
Mark: "I want to clarify. Your bylaws are a legal document - a legal document. I've been in court where a judge asks me to be an expert witness. I have to explain what the bylaws say and interpret them so the judge can see all perspectives because it is a legal document."
|
||||
|
||||
Mark: "Another good example related to the triangle: One organization established in their AOI that they would have members, then when they created their bylaws, they also included members, just like the AOI. Years later, the board decided they did not want members anymore. So they amended the bylaws and removed members, but the AOI was never amended."
|
||||
|
||||
Eliza: "So they were in conflict."
|
||||
|
||||
Mark: "That caused a conflict. Some members notified me that this couldn't be done because they never voted on removing members. I asked to see the minutes. That is why minutes are very important because they are a legal document too. They could not provide me with any minutes. There was no record of members voting and approving the removal of members."
|
||||
|
||||
Mark: "That means the AOI was more authoritative than the bylaws, which means the organization had to revert to having members because there was no record in the minutes that members voted and approved the change."
|
||||
|
||||
Eliza: "Again, if it is not in the meeting minutes, then it never happened."
|
||||
|
||||
Mark: "Right. That is the bottom line."
|
||||
|
||||
Mark: "It is important that members of any organization, whether a state agency or an affiliate, understand these important governance documents."
|
||||
|
||||
Mark: "Another interesting example is when COVID happened, which had a significant impact. I went to an annual meeting for an organization and was approached about whether their Zoom meeting was valid. Why? Because their bylaws were silent about having meetings through video conferencing. But their state law already allowed it. They had included this provision before COVID, so the meeting was legally permitted."
|
||||
|
||||
Mark: "Another state did not have it in their laws. Robert's Rules of Order states that if video conferencing is not in the bylaws, you cannot have one. It must be in the bylaws, so many legislators across the US started adding amendments to their state laws to include video conferencing. So they could proceed with their meetings. But that one state already had it in there before COVID."
|
||||
|
||||
Mark: "There are variations in how to approach this. When I'm asked to serve as parliamentarian, I have to find out which state the organization is incorporated in, and if they provide that information, I will look up the non-profit laws in that state to read them. I will ask for a copy of the AOI, though many organizations don't keep a copy. That is an important document."
|
||||
|
||||
Mark: "If I receive these documents, I will review them to see if there are any conflicts. I review the bylaws, review any other rules, standing rules, etc. I must see the whole picture before my assignment to work as a parliamentarian. All governance documents should be there, minutes too. I make sure everything is available and check if there's anything from previous business meetings that must be added to the bylaws."
|
||||
|
||||
Mark: "I've encountered one or two organizations that did not keep good meeting minutes. There were many amendments to the bylaws going back 10 or 12 years, but no record in the minutes. That means the current bylaws are void because there is no record of the amendments in the minutes. You have to go back to the bylaws from 12 years ago and follow those."
|
||||
|
||||
Mark: "I have seen many organizations that don't realize the importance of these valuable documents. So that is my message about these documents."
|
||||
|
||||
Eliza: "I would appreciate some clarification. Now that you've mentioned each state needs to review their state laws, one good example would be that each state determines how often a non-profit must host a business meeting. Some require meetings every year, some every two years. Is that why it's important to review the state laws?"
|
||||
|
||||
Mark: "Yes, if your state law says you must have an annual meeting, that means every year. If you do not follow that, you are breaking the law."
|
||||
|
||||
Eliza: "Okay."
|
||||
|
||||
Mark: "But if your state law says it's left up to the organization to decide depending on their bylaws, then the bylaws can state every two years and that's acceptable. But if state law clearly states that an annual meeting means every year, you need to call for a meeting annually."
|
||||
|
||||
Eliza: "Okay. So again, that's why it's important to review the laws. Now with AOI, you mentioned that it's really important to include the mission of the organization, IRS information..."
|
||||
|
||||
Mark: "Clauses."
|
||||
|
||||
Eliza: "Clauses must be in there."
|
||||
|
||||
Mark: "The specific code."
|
||||
|
||||
Eliza: "And must have a board of directors. All that information needs to be included in the AOI. Is that correct?"
|
||||
|
||||
Mark: "In the past, they would name the positions of board members. Now that's not the case anymore; it's in the bylaws. So if your state law says you must have the president, vice president, secretary, and treasurer, then it's required. If your bylaws have president, vice president, and secretary/treasurer combined, the AOI and bylaws are in conflict."
|
||||
|
||||
Eliza: "Conflict."
|
||||
|
||||
Mark: "because there are two separate positions in AOI and it's one position in the bylaws. Today's AOI would not typically include the positions. In the past they did, but not anymore, at least in the states I've worked with. But it is important to include the IRS clauses."
|
||||
|
||||
Mark: "I used a template the first time, sent it to the Secretary of State, and it was approved with no problems. I got copies of the bylaws and AOI, sent them to the IRS for their review and approval for 501(c)(3). They did not accept it and said I must insert specific clauses, so I added them to the AOI. I sent it back and it was approved."
|
||||
|
||||
Eliza: "Okay."
|
||||
|
||||
Mark: "Just put in the IRS code."
|
||||
|
||||
Eliza: "When an organization wants to approve AOI, what kind of vote is required for that to happen?"
|
||||
|
||||
Mark: "AOI?"
|
||||
|
||||
Eliza: "(Nods in affirmation)"
|
||||
|
||||
Mark: "I have experienced two different approaches. One way is a group gets together and they want to establish a non-profit organization. They vote as a group, of course, by majority. Normally all agree that they want the organization. Then once they all agree, vote, and pass, they fill out the AOI application to submit it."
|
||||
|
||||
Mark: "The other approach that I have seen is when an organization finds three people that want to establish a non-profit. Those three, since state law requires three. So they apply for AOI with their three names, get approved, and announce to the community that they've established a new non-profit and will have a general meeting. The purpose is to develop the mission, vision, values, and bylaws together. People that want to join become members, pay dues, and start forward progress. Then that group develops everything with the bylaws. Then you have everything you need to send to the IRS to submit for 501(c)(3). So I have seen both approaches."
|
||||
|
||||
Eliza: "Either way. What if the organization wants to amend their AOI? What kind of vote is required for that process?"
|
||||
|
||||
Mark: "Two-thirds (2/3)."
|
||||
|
||||
Eliza: "With previous notice?"
|
||||
|
||||
Mark: "Previous notice with 2/3 or majority of the whole membership. Either one of those. If you amend the AOI, you have to amend the bylaws if necessary."
|
||||
|
||||
Eliza: "Because they cannot conflict."
|
||||
|
||||
Mark: "Both."
|
||||
|
||||
Eliza: "Okay, so now the next document are the bylaws. The bylaws will explain the structure of the organization, and expand on the responsibilities and duties of each board position. For the bylaws, what is the voting process to accept them?"
|
||||
|
||||
Mark: "For the first time or to amend?"
|
||||
|
||||
Eliza: "For the first time and then to amend."
|
||||
|
||||
Mark: "For the first time, it will be majority of the votes. Then to amend is two-thirds unless your bylaws say otherwise. One organization required eighty (80) percent approval to amend. Another organization required one hundred (100) percent, which means if one person voted against it, the whole proposal fails. Normally two-thirds (2/3) according to Robert's Rules of Order is required to amend. At the beginning, the members review it which requires majority, and then later change it to two-thirds (2/3)."
|
||||
|
||||
Mark: "You're right that the bylaws explain the structure, the responsibilities of the board and officers. Some bylaws, for example, will list the specific duties for the role of Secretary and that's it. Some organizations have a list in bylaws but need to go to the policies for a detailed list of responsibilities. So the policy is separate from the bylaws."
|
||||
|
||||
Mark: "The function and responsibilities of the Secretary in the bylaws might state to pass out the minutes, keep a record of the names of all the members, and to make sure that the minutes be posted on the web or distributed to all members. If your bylaws say the minutes need to be sent by mail to all members, then if you post on the web and not send it out, you violate the bylaws."
|
||||
|
||||
Mark: "So the clause in the bylaws must be specific, like 'the minutes will be posted on the web within 10 days after the board meeting.' It must be specific and not broad. I have seen many bylaws that are very vague and they cause a lot of problems and mistakes in interpretation."
|
||||
|
||||
Mark: "I have seen some bylaws with 30 to 40 pages with many of them mixing policy and procedures together. You want to separate them, as shown in the triangle with the bylaws, and move policies down to their proper level."
|
||||
|
||||
Eliza: "Another important thing to include in the bylaws would be the membership dues - how much it costs? And also, if the members want to amend the membership dues, that requires a two-thirds (2/3) vote?"
|
||||
|
||||
Mark: "Bylaws, again, define the structure, with different categories: regular members, senior citizens, hearing members, and so on - all those categories. The fees and dues are in the standing rules."
|
||||
|
||||
Eliza: "In the standing rules and not in the bylaws?"
|
||||
|
||||
Mark: "No, the standing rules will list them. I look at the National Association of Parliamentarians and their bylaws are silent on dues. The standing rules will have it since the board themselves can adjust the dues. So if during a business meeting, a motion to increase the dues by $5 is passed, it will go into the standing rules and modify it. The bylaws stay the same."
|
||||
|
||||
Eliza: "That is a very common thing that I've noticed in various state associations - their bylaws include their member dues. So that was good to clarify."
|
||||
|
||||
Mark: "It says they will pay their member dues, but how much is in the standing rules."
|
||||
|
||||
Eliza: "Thank you for the clarification. You also mentioned that if you want to vote electronically, you must include that in the bylaws itself. If it is not in the bylaws, then you cannot."
|
||||
|
||||
Mark: "Paper voting by ballot ensures privacy so people will not know who voted for what. If the bylaws state you must use ballots, you must do so even with only one candidate."
|
||||
|
||||
Eliza: "You cannot use unanimous consent."
|
||||
|
||||
Mark: "The bylaws are clear. You must vote by ballot. You still have to vote and that is RONR rules. But if you put it in the special rules, the bylaws override special rules and RONR. If you do not want ballots, then amend the bylaws or put it in the special rules. Then if the special rules say if there is no challenge from any person, then you can have acclamation. Right with acclamation?"
|
||||
|
||||
Eliza: "Unanimous consent or acclamation."
|
||||
|
||||
Mark: "Yes, acclamation. Again, it is important to think about what you put in the bylaws. Some people just throw things in, but that will cause problems and conflicts that will pop up later. The bylaws must be thought through very carefully. Sometimes when a bylaws committee of an organization forms, they will have a parliamentarian there working with them."
|
||||
|
||||
Mark: "I have been involved with many bylaws committees to make sure that they think about various scenarios or questionable items. If you want to change the name of the organization, what do you think will happen when the members resist? First, you need to think about this."
|
||||
|
||||
Mark: "Sometimes an organization will develop new bylaws and submit them to a lawyer. The lawyer will review them to make sure they don't conflict with state law. But the organization will work with a parliamentarian to start the process with everything and then give it to the lawyer. Sometimes bylaws are very simple and easy to follow, then you do not need a lawyer. That is a decision the organization needs to make."
|
||||
|
||||
Eliza: "Okay, now the next document, Special Rules of Order. One example you used is that RONR itself states that in debate, each person can debate up to 2 times, 10 minutes each time."
|
||||
|
||||
Mark: "10, 10, 10."
|
||||
|
||||
Eliza: "Now if they have over 200 members, that would take a long time, so the Special Rules can limit debate time. What other topics are good to address in the Special Rules of Order?"
|
||||
|
||||
Mark: "I would recommend, for example, that members who make a motion and receive a second, the member who made the motion is the first person to discuss why they support it. That's one example. Your rules of order can have 3 in support and 3 against. Then after explaining why they support, it goes to explaining why against, and then back to support. If there's no one else to speak against, which means it's 2 to 1, then you stop there. It means stop debate."
|
||||
|
||||
Mark: "But you need to put this in the Special Rules to say that if there are 2 in support and 1 against, debate stops and you proceed to vote. When it happens, go ahead and follow that."
|
||||
|
||||
Eliza: "What is the voting process to accept Special Rules of Order?"
|
||||
|
||||
Mark: "To accept it at the beginning, it is majority and to amend it, it requires two-thirds (2/3)."
|
||||
|
||||
Eliza: "Do Special Rules of Order continue after the business meeting is finished?"
|
||||
|
||||
Mark: "They're dropped on the floor."
|
||||
|
||||
Eliza: "And then the next business meeting, you will have new Special Rules of Order again?"
|
||||
|
||||
Mark: "Members need to vote on them again."
|
||||
|
||||
Eliza: "So it's new every time."
|
||||
|
||||
Mark: "There is always something new that comes up based on what happened previously, so every year it changes. I have seen Special Rules for conferences from 1966 and compared them with recent ones, and they were very different. Things and times change."
|
||||
|
||||
Eliza: "True. Next is parliamentary authority. Most organizations currently follow Robert's Rules of Order, Newly Revised, which seems to be the most common one. How does an organization accept that parliamentary authority?"
|
||||
|
||||
Mark: "It is in the bylaws when they were approved the first time. If you want to change it to another authority, you must amend the bylaws."
|
||||
|
||||
Eliza: "That will require a two-thirds (2/3) vote?"
|
||||
|
||||
Mark: "Normally two-thirds (2/3), and the bylaws will state that if you want to amend, it must have two-thirds (2/3) or three-fourths (3/4) vote."
|
||||
|
||||
Eliza: "Next is Standing Rules. You mentioned that Standing Rules relate to financial issues or the administration of the organization. Can you please provide another example of what Standing Rules look like?"
|
||||
|
||||
Mark: "Sure, and I am going to show what Standing Rules of NAP look like."
|
||||
|
||||
The screen shows Mark sharing a document displaying the National Association of Parliamentarians' Standing Rules.
|
||||
|
||||
Mark: "This is an example of NAP's Standing Rules. It specifies titles and the Greek alphabetical names, and look at the registered mark symbol. When I make business cards as a member of NAP, I include that symbol. Next is the financial list with membership dues and the prices."
|
||||
|
||||
Eliza: "It separates the cost for each category."
|
||||
|
||||
Mark: "Yes, different groups. Now number 4 is about how I pay my dues to NAP. NAP will send $10 per member back to the state association. So if 50 members from New Mexico send dues to NAP, NAP will calculate 50 times $10 and send back $500 for the state membership. Members don't pay New Mexico state directly; instead, NAP distributes the funds. That is in their Standing Rules."
|
||||
|
||||
Mark: "Number 5 is referral procedure. Number six is public relations. It states that April is parliamentary law month which must be promoted and posted with all these rules every April. With number seven, when I ran for treasurer of NAP back in 2021, they put my picture from the bylaws in their newsletter."
|
||||
|
||||
Mark: "The next one is chartering. The next one covers conventions where non-voting attendees can come without voting rights. Next we have publications. Now we have professional responsibilities and members discipline committee. And the last one is reclassification. So those Standing Rules are not in the bylaws; instead, they are in the policies and procedures."
|
||||
|
||||
Eliza: "Because of the governance document structure, that means that bylaws themselves are above the Standing Rules?"
|
||||
|
||||
Mark: "Yes."
|
||||
|
||||
Eliza: "When an organization wants to accept Standing Rules, what vote is required for that?"
|
||||
|
||||
Mark: "Standing Rules at the beginning require a majority vote, but to change them requires two-thirds (2/3). It always requires two-thirds (2/3) when amending something, and that's normal."
|
||||
|
||||
Eliza: "When do Standing Rules expire?"
|
||||
|
||||
Mark: "There is no expiration."
|
||||
|
||||
Eliza: "None at all."
|
||||
|
||||
Mark: "If you want to change them, change them at a conference business meeting. During the conference, the members can review financial issues and make a motion to increase the dues by $10. Once passed, it goes into the Standing Rules for modification."
|
||||
|
||||
Eliza: "Okay, so now..."
|
||||
|
||||
Mark: "Things happen."
|
||||
|
||||
Eliza: "Then after Standing Rules we have Customs. One very common custom is that many people will make a motion to accept meeting minutes, and we've discussed that before. It is not necessary, but it is customary. Can you think of another common custom that does not need to happen?"
|
||||
|
||||
Mark: "Really, customs are dwindling because the other levels of authorities override customs frequently. I shared one example with you, but I cannot give you another one because they are fading away. That is good because it means the bylaws state that RONR is the authoritative parliamentary source, and that overrides customs. So bury it, drop it, and move on."
|
||||
|
||||
Eliza: "Yes, so that removes personal feelings and emotions and we must follow the law. That is a legal document."
|
||||
|
||||
Mark: "Right."
|
||||
|
||||
Eliza: "One last question now: Many state associations also have affiliates and local clubs with their own bylaws. Where (showing the governance document triangle) do the bylaws of the local club fit within that structure?"
|
||||
|
||||
Mark: "If the affiliate has their own 501(c)(3), then they just pay the membership dues as an affiliate and that's it. If the bylaws say that they can have affiliates, then they will list the requirements that must be met in order to pay the dues and that's it. The organization does not manage the affiliate organization; they are on their own."
|
||||
|
||||
Mark: "Now, if they are not an affiliate, do not have their own 501(c)(3), and federal ID, then they are a chapter. There must be a clause in the bylaws about having a chapter. With chapters, the members pay their membership dues to the chapter and can also fundraise. But their financial information must be part of the parent organization's finances when they do their taxes and financial reports."
|
||||
|
||||
Mark: "An affiliated 501(c)(3) has their own financial information that is not part of the parent organization. They are separate and different organizations with their own IDs and names. I am aware of a few organizations that feel chapters are not effective, so members are informed that they will no longer be a chapter at the end of the year. You can set up your own 501(c)(3) with your own name that is different from the parent organization, with your own Articles of Incorporation. Some will do that, and others will simply dissolve. But that is an option."
|
||||
|
||||
Eliza: "What is the benefit for a chapter to become its own 501(c)(3) and become an affiliate?"
|
||||
|
||||
Mark: "Suppose an organization is national with chapters in Texas, Iowa, and California that use their parent organization's name? They can stay as a chapter, or they can decide to go on their own to become a national organization themselves. They could do that, or if they want their state association to serve their state as a 501(c)(3). They have their own board and charge dues to earn money which is their own and don't have to share with a parent organization."
|
||||
|
||||
Mark: "If they are a chapter, the parent organization will have rules in their bylaws about what the chapter can and cannot do. They have control. If they're set up on their own, they control themselves."
|
||||
|
||||
Eliza: "There's very limited flexibility within a chapter's potential ability to manage themselves."
|
||||
|
||||
Mark: "Yes, but I am not seeing many organizations with chapters."
|
||||
|
||||
Eliza: "They are fading away."
|
||||
|
||||
Mark: "Yes, some have regionals and some have chapters. And with effectiveness - some are and some are not."
|
||||
|
||||
Eliza: "It's important to make sure that all the governance documents align with state law, that everything is legal, and that all those documents are actually legal documents. Is that correct?"
|
||||
|
||||
Mark: "AOI and bylaws are legal documents. Special Rules of Order and Rules of Order are more procedural. They are not legal documents; they are procedures on how to conduct a smooth meeting. But the other two are legal."
|
||||
|
||||
Eliza: "When a person becomes a member of a non-profit organization, what is considered best practice for providing members with documents?"
|
||||
|
||||
Mark: "Provide the bylaws and policy procedures as well. More and more organizations that I am seeing are posting bylaws and policy procedures on their website."
|
||||
|
||||
Eliza: "Transparency."
|
||||
|
||||
Mark: "Yes, and when they go to the business meeting, they have it right there. They know what they can do and what they cannot do, what they would like to amend, to make a motion, and so on. They don't go into a meeting only to be told something and then 'see you next year.' No, these are people who read the bylaws, notice something is wrong during the meeting, and raise their hands for a Point of Order. 'You decided these things but the policy says this.'"
|
||||
|
||||
Mark: "Where is the protection of the people? Members are protected with policy procedures and bylaws. If you withhold them and the board decides they can do whatever they want, that is not a member organization. To make it clear, members are above the board. You should be transparent and distribute documents to the people so they know what's happening. Maybe they will see something that needs work or is limited, so let members decide what to vote on. That is democratic procedure."
|
||||
|
||||
Eliza: "The more information each member has about their own organization, the more ability they have to influence and make decisions with proper procedure knowledge. Without that, they're overwhelmed and lost."
|
||||
|
||||
Mark: "Right."
|
||||
|
||||
Eliza: "The point of parliamentary procedure is to protect the majority and the minority members, not only the board."
|
||||
|
||||
Mark: "Right, and again, your bylaws make it very clear. Suppose the bylaws clearly say if a board member does not perform their duties, then only the board can remove that member. Members cannot remove a board member because that is the board's responsibility. If you have a board member who has a conflict of interest and the board turns a blind eye or does not care, you may want to add some teeth in the bylaws to give the members some power to remove a board member. It needs to be put in the bylaws. If it is not in there, then what can you do? You are stuck, so that is why it is important to be transparent with the bylaws."
|
||||
|
||||
Eliza: "That really clarifies each section of the governance documents. Again, that's why it's really important to consult with either a lawyer or an expert parliamentarian like yourself."
|
||||
|
||||
Mark: "Yes, because both lawyers and parliamentarians spend a lot of time going to workshops, going to training, building a library, writing opinions, researching, and numerous other things. We don't just tell you that it is how it is; we show what, where, and who stated it - the whole picture."
|
||||
|
||||
Eliza: "Facts."
|
||||
|
||||
Mark: "Facts and always provide facts, not our opinion. I have written opinions that could be 5 pages long to explain why you cannot do certain things based on state law, AOI, and bylaws. 'Why are you going off on a tangent?' I have written several."
|
||||
|
||||
Mark: "Also, I will tell you that parliamentarians are constantly pursuing education. You go to conferences, go to workshops, and build a library. I just ordered new books and should get them today for my parliamentary library. With expert witness cases, I read different cases on how to handle things to become more knowledgeable with different viewpoints when I provide expert testimony. Again, that requires a lot of time and is part of our responsibilities."
|
||||
|
||||
Eliza: "Parliamentary procedure itself is not simple and is a big responsibility. It is very important to continue with education on all the current laws, policies, and everything else. Thank you for sharing your knowledge and explaining more about each part, including the vote required for accepting and amending. I feel that clarifies our non-profit governance documents. Thank you, Mark!"
|
||||
|
||||
Mark: "You're welcome."
|
||||
115
public/transcriptions/secretary-role-responsibilities.txt
Normal file
115
public/transcriptions/secretary-role-responsibilities.txt
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
Secretary Role & Responsibilities: Comprehensive Guide
|
||||
|
||||
The frame is divided vertically into two equal sections, like a split screen. The left side displays Eliza Kragh, a woman with short brown hair wearing a dark top and a pendant necklace. Behind her is a light-colored, plain wall. On the right side, Mark Apodaca, a man with glasses and a gray beard is visible. He is wearing a dark sweatshirt over a red collared shirt. He is seated in a dark chair, and a portion of a wooden cabinet is visible behind him on the right. The wall behind him is also a light color, similar to the one on the left side.
|
||||
|
||||
Eliza: "Hello, this is Eliza Kragh and we're here today with Mark Apodaca who is an expert in parliamentary procedure. Today we will be discussing The Role of a Secretary."
|
||||
|
||||
Eliza: "Mark, please share with us about the role of a secretary."
|
||||
|
||||
Mark: "Okay. Often the bylaws and policy procedures will list the responsibilities of a secretary. Some bylaws will have a long list of responsibilities and some will have three listed. I have seen both where either bylaws or policy procedures will list the responsibilities. Robert's Rules of Order, itself, has a list of some of the responsibilities, but it is important to know what your governance documents mention about the role itself. It also includes what a secretary can do and cannot do."
|
||||
|
||||
Mark continues, explaining a key example: "One good example is when going to a business meeting where the secretary shows up but the president or vice president has not, the secretary can start the meeting until the president or vice president shows up to take over, which can happen. Another thing is it's important during the meeting to always have both president and secretary there, as it is required in RONR."
|
||||
|
||||
Mark discusses common misconceptions about minutes: "I have seen some secretaries write a 35 page paper of minutes. It is not necessary to make a story of what happened during the meeting about they said this, they said that and so on. That is not required for minutes. The result, the motion, seconded - for seconded, you don't need the name of the person, but do need the name of who made the motions. Do not include the debate of who said what. Then vote - the vote must show the numbers to support or against / passed or failed. All motions or action items that were proposed must be in the minutes so we will not forget that they happened. That is the basic role."
|
||||
|
||||
Mark: "I am going to pull up a powerpoint to show some rules from RONR."
|
||||
|
||||
The scene is a screenshot of a virtual meeting or webinar, likely through a screen-sharing application. On the left side of the screen is an open Microsoft PowerPoint presentation titled "Duties of the Secretary." The current slide lists key duties of the secretary, numbered and bulleted, in large black text on a white background. To the right of the presentation are two vertically stacked video feeds of Eliza and Mark.
|
||||
|
||||
Mark: "The responsibilities of a secretary in RONR 47:33 - that is where you will find it. Number 1 is the most important one and that is to keep a record of the minutes. What the secretary puts down and then when it is approved or accepted as is or with corrections, the minutes become a legal document. Often in court, the judge will ask to see the minutes and if you do not provide the minutes or if it is not in the minutes, the judge will deny."
|
||||
|
||||
Mark: "The second thing is to keep a file of all committee reports. It is important that the organization has committees and most popular is finance, governance, or strategy plan of the members, and fundraising. All those committees are very important and they meet in between board meetings: board meeting, committee meeting, and then board meeting. Each of those committees will give the secretary the minutes, reports from their meeting. The secretary is responsible to review them and make sure that they are in order to put them in files."
|
||||
|
||||
Mark explains the purpose of committee reports: "The reason is that, for example, you are on the fundraising committee and I'm on the finance committee - two different committees, now I want to know as a board what is up with the other committee that I'm not involved with when the next board meeting comes up? The board will get a copy of the committee minutes and report to review. I may have some questions about the fundraising committee that you as chair of the finance committee can answer. While reading the reports and there are no questions, then the president or the chair will state 'any questions about the minutes from the fundraising committee?' If there are no questions, then file it and move on since reading it is not required. So that is another important role of the secretary."
|
||||
|
||||
Mark: "The fourth one is to keep a list of all the members. For example, suppose that an organization has 500 members and the secretary must notify all 500 members about the upcoming annual meeting. If the secretary sends it out and misses 10 people, you cannot have a meeting."
|
||||
|
||||
Eliza: "Oh, really?"
|
||||
|
||||
Mark: "Oh no! Every member on record must get notice that there is a meeting. If you miss one member, you cannot have a meeting because they object 'they had a meeting and voted on it but I was not there because I did not get notice.' It could become a legal issue."
|
||||
|
||||
Mark shares a cautionary example: "I remember one organization where a group of members came together and they invited officers but the officers refused to come and what they decided was that the officers refused to follow, so they saw a lawyer and the lawyer asked the first question if they kept a list of all the members' names. Did you contact all those members - ALL the members? The person who set the meeting admitted no, so the lawyer could not do anything. Your meeting is null and void because not all the members were aware of the meeting. When you pay dues to be a member, you have every right to know when the meeting is happening. That is an important role of the secretary to make sure that the list of members is always updated."
|
||||
|
||||
Mark: "The fourth one is the minutes must be available to those that ask for them and you cannot abuse it. I have seen minutes that are required by RONR to be shown, especially if special rules of order state otherwise, unless the special rules of order are silent on it, then RONR states that you must provide them if I approach the secretary of that organization to see the meeting minutes, which you have to show. It is nicer and saves time and headache if the minutes are put on the organization's website and are right there to read and then if you have questions, go to the next meeting and bring it up."
|
||||
|
||||
The screen shows a computer interface displaying another presentation slide titled "Duties of the Secretary," listing additional responsibilities as bullet points.
|
||||
|
||||
Mark: "The next one will be: Notify the officers and member delegates of their election appointment, give the documents to the committee, require a performance of jobs, and have everything ready to hand over. Number six is credentials - go to the organization line and sign delegates' credentials and give to someone with a badge or delegate to go in and sign. Number seven, sign all certified copies of actions of the organization. What they vote on and motion that was passed, the action taken must be certified by documenting it in the minutes to certify that action. Keep good records, books, bylaws, special rules and all those document amendments with proper records."
|
||||
|
||||
Mark continues listing duties: "Number nine is to send members notice of the next meeting. If you miss one, then you cannot really have the meeting because all must receive notice. Number ten is to prepare, really work with the president or whoever is running the meeting to develop the agenda, order of business. Just sit down with the managing officer to help develop those and plan for the next meeting. The last one is when you have an absent president and vice president, the secretary can call the meeting to order until someone is elected to chair the meeting - pro temp."
|
||||
|
||||
Mark: "Those are some of the responsibilities from RONR that are not all in the bylaws. Minutes, giving members notice, and lastly keeping an updated members list. Those are the important functions and to gather reports to send out for the board to review. Those all are basic responsibilities of the secretary."
|
||||
|
||||
Mark emphasizes the importance of documentation: "It is worth keeping documents of any bylaw amendments because if you do not document them in the minutes, it means they never happened, unless you make corrections to the minutes. If there are no corrections and the minutes are accepted, then that means you can go back and make corrections in the minutes back in time. It does not stop there."
|
||||
|
||||
Eliza: "Is there a time limit?"
|
||||
|
||||
Mark: "No, there is no time limit. Again, just one organization with 10-12 years amendment after amendment every two years and you're talking about 5 business meetings, records amended in minutes but you have nothing, which means it did not happen. It means you have to go back to the beginning and follow the original bylaws. Some organization secretaries do not keep good records."
|
||||
|
||||
Mark shares personal experiences: "I am a member of an organization and I'm never informed of when my membership is due. I mean, they inform me that they have a business meeting coming, but my membership dues expired two years ago and I've been waiting for it. So if I come to the meeting, technically I am not a member. One organization has 'lifetime members' and they kept records of those members who paid up for lifetime members. For those who did not want lifetime membership and prefer yearly renewals, there are no records of those members. I ask you to call a meeting and you only list the lifetime members—what about all the rest of the members? They are members and no notice, no records, nothing? The secretary did not do their job."
|
||||
|
||||
Mark: "Yeah, I have seen some meetings where the secretary used their phone and took notes."
|
||||
|
||||
Eliza: "Their phone?"
|
||||
|
||||
Mark: "I have a notepad so I fill it in properly. I would encourage using a laptop during board meetings or annual meetings to finish it and send it to those board members. You know that many organizations will establish a committee with three people to review the minutes, because if the conference or annual meeting is one year from now, it means the secretary will type it all up and hold it for one year? You think people will remember what happened one year ago?"
|
||||
|
||||
Mark explains a better approach: "So set up a committee, ad hoc committee, and they will receive the minutes from the secretary and they will review and approve it and will inform the secretary and board president that they are accepted. Then at the next board meeting, the board approves the minutes that were reviewed for the next annual meeting. During the next annual meeting, members do not need to approve the minutes because the committee already did the work and will let you know that the committee finished. You picked the people last year to review everything, all good, then finished and passed by the board. That will be in the procedure policy guidelines."
|
||||
|
||||
Mark stresses record-keeping: "Again, it is very important to keep a copy of the minutes. I have seen some secretaries with two years of no minutes who then decide not to run again and they do not give the minutes to the next secretary. They need the minutes!"
|
||||
|
||||
Eliza: "It is supposed to be passed on."
|
||||
|
||||
Mark: "Some organizations forget that. It is not good and organizations should have a centralized place with all governance documents and all the minutes and everything. Have a safe place for it and you can use it in the future if something legal comes up and you need to review past events, then it is ready right there. But some organizations are not very careful and they lose it in the cracks. It is not good."
|
||||
|
||||
Eliza: "What happens if you lose the meeting minutes?"
|
||||
|
||||
Mark: "It can mean the meeting never happened."
|
||||
|
||||
Eliza: "Very important to have the minutes."
|
||||
|
||||
Mark: "Really, you had a meeting and where are the minutes? Where are the minutes? Minutes should open with the name of the organization, call to order, time, date, and so forth, to meet quorum, reports, motions, new business, adjournment—simply all there. No minutes? Hello, where's the proof? You have quorum, the minutes show if you meet quorum, motions passed. If we do not have the minutes, technically the meeting never happened. The court will say nothing, which means the courts will not waste their time. If you do not list it, then it never happened. That is the basic role of the secretary."
|
||||
|
||||
Mark shares his personal experience: "I have been secretary for one parliamentary organization and I had the role of the secretary and treasurer so I had to oversee and document everything about parliament, president, members, and I had to write everything depending on interpreters. I would list motions, seconds, votes passed and so forth until it is finished, and then present it to members for review and acceptance, make corrections if needed, and then sign and file it. So if the national organization wants to see a copy of the minutes, then you must have them ready. That parliamentary organization is very specific and detailed in making sure everything is there. If the bylaws are there, then it is worry free. If you do not have it, then oh boy. That is basically about the role of secretary, which is important."
|
||||
|
||||
Mark offers advice for secretaries: "It is nice if the secretary has some writing skills, maybe take a class at college or an extended program on how to write better minutes and it is not about storytelling skills. Many come in with no idea what their role is and what they should fill out. It is often that I will nudge the secretary that they do not need a story, just document specific things and follow RONR and that is it."
|
||||
|
||||
Eliza summarizes: "To basically sum it up, the role and responsibilities of a secretary is to make sure that everything is documented, all actions that happened, not a story."
|
||||
|
||||
Mark adds: "Vote passed or failed."
|
||||
|
||||
Eliza continues: "Satisfy quorum, who is there, if president and vice president are not there, the secretary can start the meeting, can make sure to inform ALL members ahead of the meeting, needs to make sure they keep records of all meeting minutes in files ready to provide to the board or members when requested and have all committee reports in files too. That basically covers the responsibilities of the secretary?"
|
||||
|
||||
Mark: "Yes, if it's too much, often they will have an assistant secretary, not a board member, that can help the secretary work with duties if they are overwhelmed. We've talked about small organizations, but if a huge organization with maybe 5000 members, then the secretary needs an assistant to help. Again, it depends on the size, if large or small."
|
||||
|
||||
Eliza emphasizes: "Because meeting minutes are a legal document, that is a very big responsibility for the secretary and that is why it is so important for the secretary to know what needs to be involved in the meeting minutes. For those secretaries that have no idea on how to develop minutes, they have several different options and can go online to learn more about templates. NAP provides examples too, I believe, on what secretary meeting minutes can look like."
|
||||
|
||||
Mark: "They pretty much look the same with AIP (American Institute of Parliamentarians) which has a list of roles and responsibilities and it is the same as RONR."
|
||||
|
||||
Eliza: "So I would strongly recommend for those who become secretary to take initiative to start researching themselves so they can align with the mission of what the meeting minutes should look like."
|
||||
|
||||
Mark: "A person can go into NAP website, you can get 12th edition or this pamphlet."
|
||||
|
||||
Eliza: "Short and simple."
|
||||
|
||||
Mark: "These focus on the secretary. AIP has different books on how to write effective minutes. AIP books can be purchased on Amazon since they do not sell books themselves and refer people to Amazon. AIP also has many good resources. I use them and I am a member of AIP. NAP has a website that you can go into their book store and look for any information about secretary like this pamphlet. Another one is which is also sold on Amazon. The person who wrote this is certified by both NAP and AIP organizations, so they combined them into one and all the information should be in this. It is easy to read and follow. So I would encourage all the organization secretaries to get those books and become a great secretary."
|
||||
|
||||
Eliza: "So resources are available out there and people just have to take the initiative and have the passion for it."
|
||||
|
||||
Mark: "I am always looking at different ways to improve to improve my understanding in finances, in non-profits, with grants and multiple other things. I research and find resources because I want to know what my role is and how to do it. It is an investment into my education and I make sure to practice it. It is worth it and you become an extremely valuable member of the community."
|
||||
|
||||
Eliza: "That would be very beneficial not just for the secretary but for the organization itself, so if anything comes up, then you are ready to provide because the secretary does their job."
|
||||
|
||||
Mark: "That is right. Do it right if you want to do it."
|
||||
|
||||
Eliza: "Right, and then not have to go back and fix everything."
|
||||
|
||||
Mark: "Yes, I have often seen secretaries type and then learn that they do not need all those things and delete what is not required, clean it up and then present it to the members for approval."
|
||||
|
||||
Eliza: "That is a great option to know that you can go back and clean up so you can go forward."
|
||||
|
||||
Mark: "Yup, you can."
|
||||
|
||||
Eliza: "That very clearly explains the specifics of the role and responsibilities of the secretary. I hope this will help improve meeting minutes out there to make it easier for all to understand exactly what happens. Thank you, Mark, for the clarification."
|
||||
|
||||
Mark: "Thank you."
|
||||
0
public/videos/.gitkeep
Normal file
0
public/videos/.gitkeep
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
server.ts
Normal file
57
server.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import express from 'express'
|
||||
import dotenv from 'dotenv'
|
||||
import { POST as handleContact } from './src/api/contact.js'
|
||||
import { POST as handleSubscribe } from './src/api/subscribe.js'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||
res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
|
||||
next()
|
||||
})
|
||||
|
||||
app.use(express.static(join(__dirname, 'dist')))
|
||||
|
||||
app.post('/contact', async (req, res) => {
|
||||
try {
|
||||
const response = await handleContact(req)
|
||||
res.status(response.status).json(response.body)
|
||||
} catch {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/subscribe', async (req, res) => {
|
||||
try {
|
||||
await handleSubscribe(req, res)
|
||||
} catch (error) {
|
||||
console.error('Server error in subscribe route:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(join(__dirname, 'dist', 'index.html'))
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 804
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.info(`Server running on port ${PORT}`)
|
||||
})
|
||||
29
src/App.tsx
29
src/App.tsx
|
|
@ -5,20 +5,25 @@ import Services from './pages/Services'
|
|||
import About from './pages/About'
|
||||
import Contact from './pages/Contact'
|
||||
import Resources from './pages/Resources'
|
||||
import { NotificationProvider } from './context/NotificationProvider'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/services" element={<Services />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/resources" element={<Resources />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
<NotificationProvider>
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/services" element={<Services />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/resources" element={<Resources />} />
|
||||
<Route path="/resources/video/:videoId" element={<Resources />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
)
|
||||
}
|
||||
export default App
|
||||
|
||||
export default App
|
||||
|
|
|
|||
120
src/api/contact.ts
Normal file
120
src/api/contact.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { Request } from 'express'
|
||||
import { rateLimit } from '../lib/rate-limit.js'
|
||||
import { sendEmail } from '../lib/email.js'
|
||||
|
||||
interface FormData {
|
||||
name: string
|
||||
email: string
|
||||
message: string
|
||||
website?: string // Honeypot field
|
||||
}
|
||||
|
||||
const validateEmail = (email: string): boolean => {
|
||||
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return pattern.test(email)
|
||||
}
|
||||
|
||||
const validateInput = (data: FormData): { isValid: boolean; error?: string } => {
|
||||
if (!data.name || data.name.length < 2 || data.name.length > 50) {
|
||||
return { isValid: false, error: 'Invalid name length' }
|
||||
}
|
||||
|
||||
if (!data.email || !validateEmail(data.email)) {
|
||||
return { isValid: false, error: 'Invalid email format' }
|
||||
}
|
||||
|
||||
if (!data.message || data.message.length < 10 || data.message.length > 1000) {
|
||||
return { isValid: false, error: 'Invalid message length' }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
const sanitizeInput = (input: string): string => {
|
||||
return input
|
||||
.trim()
|
||||
.replace(/[<>]/g, '')
|
||||
.slice(0, 1000)
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
if (!req.body) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Request body is missing'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rateLimitResult = await rateLimit(req)
|
||||
if (!rateLimitResult.success) {
|
||||
return {
|
||||
status: 429,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Too many requests. Please try again later.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data: FormData = req.body
|
||||
|
||||
// Honeypot check - if website field is filled, it's a bot
|
||||
if (data.website && data.website.trim() !== '') {
|
||||
console.log('Honeypot triggered - bot submission blocked:', {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
website: data.website
|
||||
})
|
||||
// Return success to the bot but don't send email
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
message: 'Message sent successfully'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validation = validateInput(data)
|
||||
|
||||
if (!validation.isValid) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
success: false,
|
||||
message: validation.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedData = {
|
||||
name: sanitizeInput(data.name),
|
||||
email: sanitizeInput(data.email),
|
||||
message: sanitizeInput(data.message)
|
||||
}
|
||||
|
||||
await sendEmail(sanitizedData)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
message: 'Message sent successfully'
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Contact form error:', err)
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Failed to send message. Please try again later.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/api/route.ts
Normal file
113
src/api/route.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { Request, Response } from 'express'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { rateLimit } from '../lib/rate-limit'
|
||||
|
||||
interface FormData {
|
||||
name: string
|
||||
email: string
|
||||
service: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const validateEmail = (email: string): boolean => {
|
||||
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return pattern.test(email)
|
||||
}
|
||||
|
||||
const validateInput = (data: FormData): { isValid: boolean; error?: string } => {
|
||||
if (!data.name || data.name.length < 2 || data.name.length > 50) {
|
||||
return { isValid: false, error: 'Invalid name length' }
|
||||
}
|
||||
|
||||
if (!data.email || !validateEmail(data.email)) {
|
||||
return { isValid: false, error: 'Invalid email format' }
|
||||
}
|
||||
|
||||
if (!data.service) {
|
||||
return { isValid: false, error: 'Service selection required' }
|
||||
}
|
||||
|
||||
if (!data.message || data.message.length < 10 || data.message.length > 1000) {
|
||||
return { isValid: false, error: 'Invalid message length' }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
const sanitizeInput = (input: string): string => {
|
||||
return input
|
||||
.trim()
|
||||
.replace(/[<>]/g, '')
|
||||
.slice(0, 1000)
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response) {
|
||||
try {
|
||||
const rateLimitResult = await rateLimit(req)
|
||||
if (!rateLimitResult.success) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
message: 'Too many requests. Please try again later.'
|
||||
})
|
||||
}
|
||||
|
||||
const data: FormData = req.body
|
||||
|
||||
const validation = validateInput(data)
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: validation.error
|
||||
})
|
||||
}
|
||||
|
||||
const sanitizedData = {
|
||||
name: sanitizeInput(data.name),
|
||||
email: sanitizeInput(data.email),
|
||||
service: sanitizeInput(data.service),
|
||||
message: sanitizeInput(data.message)
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.GOOGLE_EMAIL,
|
||||
pass: process.env.GOOGLE_APP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.GOOGLE_EMAIL,
|
||||
to: process.env.RECIPIENT_EMAIL,
|
||||
subject: `New Contact Form Submission - ${sanitizedData.service}`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">New Contact Form Submission</h2>
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px;">
|
||||
<p><strong>Name:</strong> ${sanitizedData.name}</p>
|
||||
<p><strong>Email:</strong> ${sanitizedData.email}</p>
|
||||
<p><strong>Service:</strong> ${sanitizedData.service}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p style="white-space: pre-wrap;">${sanitizedData.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
await transporter.sendMail(mailOptions)
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Email sent successfully'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error)
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to send email'
|
||||
})
|
||||
}
|
||||
}
|
||||
80
src/api/subscribe.ts
Normal file
80
src/api/subscribe.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Request, Response } from 'express'
|
||||
import { rateLimit } from '../lib/rate-limit.js'
|
||||
import { sendSubscriptionEmail } from '../lib/email.js'
|
||||
|
||||
interface SubscriptionData {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
const validateEmail = (email: string): boolean => {
|
||||
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return pattern.test(email)
|
||||
}
|
||||
|
||||
const validateInput = (data: SubscriptionData): { isValid: boolean; error?: string } => {
|
||||
if (!data.name || data.name.length < 2 || data.name.length > 50) {
|
||||
return { isValid: false, error: 'Invalid name length' }
|
||||
}
|
||||
|
||||
if (!data.email || !validateEmail(data.email)) {
|
||||
return { isValid: false, error: 'Invalid email format' }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
const sanitizeInput = (input: string): string => {
|
||||
return input
|
||||
.trim()
|
||||
.replace(/[<>]/g, '')
|
||||
.slice(0, 1000)
|
||||
}
|
||||
|
||||
export async function POST(req: Request, res: Response) {
|
||||
try {
|
||||
if (!req.body) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Request body is missing'
|
||||
})
|
||||
}
|
||||
|
||||
const rateLimitResult = await rateLimit(req)
|
||||
if (!rateLimitResult.success) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
message: 'Too many requests. Please try again later.'
|
||||
})
|
||||
}
|
||||
|
||||
const data: SubscriptionData = req.body
|
||||
const validation = validateInput(data)
|
||||
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: validation.error
|
||||
})
|
||||
}
|
||||
|
||||
const sanitizedData = {
|
||||
name: sanitizeInput(data.name),
|
||||
email: sanitizeInput(data.email)
|
||||
}
|
||||
|
||||
await sendSubscriptionEmail(sanitizedData)
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Subscription successful'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing subscription:', error)
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to process subscription. Please try again later.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,7 @@ const Footer = () => {
|
|||
DeafGain LLC
|
||||
</h3>
|
||||
<p className="text-accent-lake">
|
||||
Empowering the Deaf community<br />
|
||||
through innovation
|
||||
Empowering the Deaf community through <br />innovation and shared progress.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { HiMenu, HiX } from 'react-icons/hi'
|
||||
|
||||
const Navbar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const location = useLocation()
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Home', path: '/' },
|
||||
{ name: 'Services', path: '/services' },
|
||||
|
|
@ -14,11 +16,31 @@ const Navbar = () => {
|
|||
{ name: 'Contact', path: '/contact' }
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// Close mobile menu when route changes
|
||||
useEffect(() => {
|
||||
setIsOpen(false)
|
||||
}, [location])
|
||||
|
||||
return (
|
||||
<nav className="bg-primary text-accent-snow">
|
||||
<nav className="bg-primary text-accent-snow relative z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-24">
|
||||
<Link to="/" className="font-black text-3xl tracking-wider text-accent-snow drop-shadow-lg">
|
||||
<Link
|
||||
to="/"
|
||||
className="font-black text-2xl sm:text-3xl tracking-wider text-accent-snow drop-shadow-lg"
|
||||
aria-label="DeafGain Home"
|
||||
>
|
||||
DeafGain
|
||||
</Link>
|
||||
|
||||
|
|
@ -29,7 +51,11 @@ const Navbar = () => {
|
|||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className="text-xl font-bold tracking-wide text-accent-snow hover:text-accent-lake transition-colors drop-shadow-md"
|
||||
className={`text-lg font-bold tracking-wide transition-colors drop-shadow-md
|
||||
${location.pathname === item.path
|
||||
? 'text-accent-lake'
|
||||
: 'text-accent-snow hover:text-accent-lake'}`}
|
||||
aria-current={location.pathname === item.path ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
|
|
@ -38,38 +64,53 @@ const Navbar = () => {
|
|||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="text-accent-snow"
|
||||
>
|
||||
{isOpen ? <HiX size={24} /> : <HiMenu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden inline-flex items-center justify-center p-2 rounded-md
|
||||
text-accent-snow hover:text-accent-lake hover:bg-secondary/20
|
||||
transition-colors duration-200 bg-transparent"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="mobile-menu"
|
||||
aria-label={isOpen ? "Close menu" : "Open menu"}
|
||||
>
|
||||
<span className="block">
|
||||
{isOpen ? <HiX className="w-7 h-7" /> : <HiMenu className="w-7 h-7" />}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="md:hidden"
|
||||
>
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className="block px-3 py-2 hover:bg-secondary rounded-md"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
id="mobile-menu"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="md:hidden absolute w-full bg-primary shadow-lg"
|
||||
>
|
||||
<div className="px-4 pt-2 pb-3 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.path}
|
||||
className={`block px-3 py-4 text-base font-medium rounded-md
|
||||
transition-colors duration-200
|
||||
${location.pathname === item.path
|
||||
? 'bg-secondary text-accent-lake'
|
||||
: 'text-accent-snow hover:bg-secondary/20 hover:text-accent-lake'}`}
|
||||
aria-current={location.pathname === item.path ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ const VideoPlayer = ({ src, poster, vttSrc }: VideoPlayerProps) => {
|
|||
const [showCaptions, setShowCaptions] = useState(false)
|
||||
const [playbackRate, setPlaybackRate] = useState(1)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [playError, setPlayError] = useState(false)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null)
|
||||
|
||||
const playbackRates = [0.5, 1, 2, 3]
|
||||
|
||||
|
|
@ -39,16 +40,23 @@ const VideoPlayer = ({ src, poster, vttSrc }: VideoPlayerProps) => {
|
|||
}, 3000)
|
||||
}, [isPlaying])
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (videoRef.current) {
|
||||
if (isPlaying) {
|
||||
videoRef.current.pause()
|
||||
setShowControls(true)
|
||||
setIsPlaying(false)
|
||||
} else {
|
||||
videoRef.current.play()
|
||||
hideControlsWithDelay()
|
||||
try {
|
||||
await videoRef.current.play()
|
||||
setPlayError(false)
|
||||
setIsPlaying(true)
|
||||
hideControlsWithDelay()
|
||||
} catch {
|
||||
setIsPlaying(false)
|
||||
setPlayError(true)
|
||||
}
|
||||
}
|
||||
setIsPlaying(!isPlaying)
|
||||
}
|
||||
}, [isPlaying, hideControlsWithDelay])
|
||||
|
||||
|
|
@ -133,7 +141,15 @@ const VideoPlayer = ({ src, poster, vttSrc }: VideoPlayerProps) => {
|
|||
/>
|
||||
</video>
|
||||
|
||||
<div
|
||||
{playError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/70">
|
||||
<p className="text-white text-sm font-medium px-4 py-2 bg-red-700/80 rounded">
|
||||
Video unavailable — please try again later.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 transition-opacity duration-300 ${
|
||||
showControls ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
|
|
|
|||
68
src/components/shared/Toast.tsx
Normal file
68
src/components/shared/Toast.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface ToastProps {
|
||||
message: string
|
||||
type: 'success' | 'error'
|
||||
isVisible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function Toast({ message, type, isVisible, onClose }: ToastProps) {
|
||||
const bgColor = type === 'success'
|
||||
? 'from-primary to-secondary'
|
||||
: 'from-red-600 to-red-800'
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
const timer = setTimeout(onClose, 5000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isVisible, onClose])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 px-4 py-6 pointer-events-none sm:p-6 sm:items-start flex items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 50 }}
|
||||
className="w-full max-w-sm pointer-events-auto"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg shadow-2xl">
|
||||
<div className={`h-2 bg-gradient-to-r ${bgColor}`} />
|
||||
<div className="p-4 bg-white relative">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-1 pt-0.5">
|
||||
<p className="text-sm sm:text-base text-accent-mountain text-center pr-8">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 flex-shrink-0 flex">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="inline-flex text-accent-mountain rounded-md focus:outline-none focus:ring-2 focus:ring-primary hover:text-secondary transition-colors p-1.5"
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<FaTimes className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
32
src/context/NotificationProvider.tsx
Normal file
32
src/context/NotificationProvider.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import Toast from "../components/shared/Toast"
|
||||
import { NotificationContext } from './notification-context'
|
||||
|
||||
export function NotificationProvider({ children }: { children: React.ReactNode }) {
|
||||
const [message, setMessage] = useState('')
|
||||
const [type, setType] = useState<'success' | 'error'>('success')
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
const showNotification = (message: string, type: 'success' | 'error') => {
|
||||
setMessage(message)
|
||||
setType(type)
|
||||
setIsVisible(true)
|
||||
}
|
||||
|
||||
const hideNotification = () => {
|
||||
setIsVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{ showNotification, hideNotification }}>
|
||||
{children}
|
||||
<Toast
|
||||
message={message}
|
||||
type={type}
|
||||
isVisible={isVisible}
|
||||
onClose={hideNotification}
|
||||
/>
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
13
src/context/notification-context.ts
Normal file
13
src/context/notification-context.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
|
||||
interface NotificationContextType {
|
||||
showNotification: (message: string, type: 'success' | 'error') => void
|
||||
hideNotification: () => void
|
||||
}
|
||||
|
||||
export const NotificationContext = createContext<NotificationContextType>({
|
||||
showNotification: () => {},
|
||||
hideNotification: () => {}
|
||||
})
|
||||
|
||||
export const useNotification = () => useContext(NotificationContext)
|
||||
84
src/lib/email.ts
Normal file
84
src/lib/email.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import nodemailer from 'nodemailer'
|
||||
|
||||
interface EmailData {
|
||||
name: string
|
||||
email: string
|
||||
message: string
|
||||
}
|
||||
|
||||
interface SubscriptionData {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export async function sendEmail(data: EmailData) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.GOOGLE_EMAIL,
|
||||
pass: process.env.GOOGLE_APP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.GOOGLE_EMAIL,
|
||||
to: process.env.RECIPIENT_EMAIL,
|
||||
subject: 'New Contact Form Submission',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">New Contact Form Submission</h2>
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px;">
|
||||
<p><strong>Name:</strong> ${data.name}</p>
|
||||
<p><strong>Email:</strong> ${data.email}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p style="white-space: pre-wrap;">${data.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions)
|
||||
return info
|
||||
}
|
||||
|
||||
export async function sendSubscriptionEmail(data: SubscriptionData) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.GOOGLE_EMAIL,
|
||||
pass: process.env.GOOGLE_APP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
const timestamp = now.toLocaleString('en-US', {
|
||||
timeZone: 'America/Chicago',
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long'
|
||||
})
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.GOOGLE_EMAIL,
|
||||
to: process.env.RECIPIENT_EMAIL,
|
||||
subject: 'New Video Notification Subscription',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">New Video Notification Subscription</h2>
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px;">
|
||||
<p><strong>Subscriber Name:</strong> ${data.name}</p>
|
||||
<p><strong>Email Address:</strong> ${data.email}</p>
|
||||
<p><strong>Subscription Date:</strong> ${timestamp}</p>
|
||||
<p><strong>Source:</strong> Resources Page Video Notifications</p>
|
||||
</div>
|
||||
<p style="margin-top: 20px; color: #6b7280;">This user would like to be notified when new training videos are added to the Resources page.</p>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions)
|
||||
return info
|
||||
}
|
||||
82
src/lib/rate-limit.ts
Normal file
82
src/lib/rate-limit.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { createClient, RedisClientType } from 'redis'
|
||||
import { Request } from 'express'
|
||||
|
||||
export interface RateLimitConfig {
|
||||
interval: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export interface RateLimitResult {
|
||||
success: boolean
|
||||
limit: number
|
||||
remaining: number
|
||||
reset: number
|
||||
}
|
||||
|
||||
export class RateLimit {
|
||||
private redis: RedisClientType
|
||||
private prefix: string
|
||||
|
||||
constructor() {
|
||||
this.redis = createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
})
|
||||
this.prefix = 'ratelimit'
|
||||
}
|
||||
|
||||
private async ensureConnected() {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect()
|
||||
}
|
||||
}
|
||||
|
||||
private getIP(req: Request): string {
|
||||
return req.ip || req.socket.remoteAddress || 'unknown'
|
||||
}
|
||||
|
||||
private getKey(identifier: string): string {
|
||||
return `${this.prefix}:${identifier}`
|
||||
}
|
||||
|
||||
async check(req: Request, config: RateLimitConfig): Promise<RateLimitResult> {
|
||||
await this.ensureConnected()
|
||||
|
||||
const ip = this.getIP(req)
|
||||
const key = this.getKey(ip)
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const windowStart = now - (now % config.interval)
|
||||
const windowKey = `${key}:${windowStart}`
|
||||
|
||||
const multi = this.redis.multi()
|
||||
multi.incr(windowKey)
|
||||
multi.expire(windowKey, config.interval)
|
||||
|
||||
const results = await multi.exec()
|
||||
const count = Number(results[0]) || 1
|
||||
const remaining = Math.max(0, config.limit - count)
|
||||
|
||||
return {
|
||||
success: count <= config.limit,
|
||||
limit: config.limit,
|
||||
remaining,
|
||||
reset: windowStart + config.interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function rateLimit(req: Request, config: RateLimitConfig = { interval: 60 * 60, limit: 5 }): Promise<RateLimitResult> {
|
||||
try {
|
||||
const limiter = new RateLimit()
|
||||
return await limiter.check(req, config)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.warn('Redis rate limiting failed, allowing request:', errorMessage)
|
||||
// Fallback: allow the request when Redis is unavailable
|
||||
return {
|
||||
success: true,
|
||||
limit: config.limit,
|
||||
remaining: config.limit - 1,
|
||||
reset: Math.floor(Date.now() / 1000) + config.interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,17 +7,17 @@ const About = () => {
|
|||
{
|
||||
icon: <FaGraduationCap />,
|
||||
title: "Education",
|
||||
details: "Master's in Human Resource Development - Rochester Institute of Technology"
|
||||
details: "Master's in Human Resource Development - Rochester Institute of Technology, Certificate in Deaf Studies and Social Justice - University of Kansas"
|
||||
},
|
||||
{
|
||||
icon: <FaAward />,
|
||||
title: "Certifications",
|
||||
details: "Certified ADA Coordinator, Member of National Association of Parliamentarians"
|
||||
details: "Certified ADA Coordinator, Insured Registered Parliamentarian (RP) of the National Association of Parliamentarians, Member of the American Institute of Parliamentarians"
|
||||
},
|
||||
{
|
||||
icon: <FaHandsHelping />,
|
||||
title: "Leadership",
|
||||
details: "President of Missoula Council of the Deaf, Founder of DeafGain LLC"
|
||||
details: "Founding Former President of Missoula Council of the Deaf (MCDi), Owner of DeafGain LLC"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ const About = () => {
|
|||
through strategic partnerships and innovative solutions.
|
||||
</p>
|
||||
<h3 className="text-xl font-semibold text-secondary">
|
||||
"Advocacy nudges the status quo, and we are here to shift paradigms."
|
||||
"Advocacy nudges the status quo and we are here to shift paradigms."
|
||||
</h3>
|
||||
</motion.div>
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ const About = () => {
|
|||
Eliza Kragh
|
||||
</h2>
|
||||
<div className="w-48 h-48 mx-auto mb-8 rounded-full overflow-hidden bg-accent-lake border-4 border-primary">
|
||||
<img src="/images/eliza.jpg" alt="Eliza Kragh" className="w-full h-full object-cover" />
|
||||
<img src="/images/eliza-2026.jpg" alt="Eliza Kragh" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<motion.div>
|
||||
<p className="text-accent-mountain">
|
||||
|
|
|
|||
|
|
@ -1,28 +1,58 @@
|
|||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaEnvelope, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import { useNotification } from '../context/notification-context'
|
||||
|
||||
const Contact = () => {
|
||||
const { showNotification } = useNotification()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
message: '',
|
||||
website: '' // Honeypot field
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Form submission logic will be implemented here
|
||||
if (isSubmitting) return
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Message sent successfully!', 'success')
|
||||
setFormData({ name: '', email: '', message: '', website: '' })
|
||||
} else {
|
||||
showNotification(data.message || 'Failed to send message', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'An error occurred. Please try again later.'
|
||||
showNotification(errorMessage, 'error')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: <FaEnvelope />,
|
||||
icon: <FaEnvelope className="text-2xl sm:text-3xl" />,
|
||||
title: 'Email',
|
||||
content: 'contact@deafgain.com'
|
||||
},
|
||||
{
|
||||
icon: <FaMapMarkerAlt />,
|
||||
icon: <FaMapMarkerAlt className="text-2xl sm:text-3xl" />,
|
||||
title: 'Location',
|
||||
content: 'Montana, United States'
|
||||
}
|
||||
|
|
@ -30,18 +60,20 @@ const Contact = () => {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4 py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-16"
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center mb-8 sm:mb-16"
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
|
||||
<p className="text-accent-mountain text-lg">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
|
||||
<p className="text-accent-mountain text-base sm:text-lg max-w-2xl mx-auto px-4">
|
||||
We're here to help and answer any questions you might have
|
||||
</p>
|
||||
</motion.div>
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-16 max-w-2xl mx-auto">
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-6 mb-12 max-w-2xl mx-auto">
|
||||
{contactInfo.map((info, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
|
|
@ -50,21 +82,22 @@ const Contact = () => {
|
|||
transition={{ delay: index * 0.2 }}
|
||||
className="bg-white p-6 rounded-lg shadow-lg text-center"
|
||||
>
|
||||
<div className="text-primary text-3xl mb-4">{info.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{info.title}</h3>
|
||||
<div className="text-primary mb-4">{info.icon}</div>
|
||||
<h3 className="text-lg sm:text-xl font-semibold mb-2">{info.title}</h3>
|
||||
<p className="text-accent-mountain">{info.content}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-8"
|
||||
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 sm:p-8"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="name" className="block text-accent-mountain mb-2">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -72,13 +105,16 @@ const Contact = () => {
|
|||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
|
||||
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
||||
text-base sm:text-lg"
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="email" className="block text-accent-mountain mb-2">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -86,35 +122,40 @@ const Contact = () => {
|
|||
id="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
|
||||
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
||||
text-base sm:text-lg"
|
||||
required
|
||||
pattern="[^@\s]+@[^@\s]+\.[^@\s]+"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="subject" className="block text-accent-mountain mb-2">
|
||||
Subject
|
||||
</label>
|
||||
{/* Honeypot field - hidden from humans, visible to bots */}
|
||||
<div className="absolute left-[-5000px] opacity-0" aria-hidden="true">
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
value={formData.subject}
|
||||
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
|
||||
required
|
||||
id="website"
|
||||
name="website"
|
||||
value={formData.website}
|
||||
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="message" className="block text-accent-mountain mb-2">
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary h-32"
|
||||
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
||||
text-base sm:text-lg min-h-[120px] sm:min-h-[150px]"
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={1000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -122,9 +163,14 @@ const Contact = () => {
|
|||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="submit"
|
||||
className="w-full bg-primary text-accent-snow py-3 rounded-lg font-semibold hover:bg-secondary transition-colors"
|
||||
disabled={isSubmitting}
|
||||
className={`w-full py-4 rounded-lg font-semibold text-base sm:text-lg transition-colors
|
||||
${isSubmitting
|
||||
? 'bg-accent-mountain text-accent-snow cursor-not-allowed'
|
||||
: 'bg-primary text-accent-snow hover:bg-secondary active:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
Send Message
|
||||
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||
</motion.button>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const Home = () => {
|
|||
Our Mission
|
||||
</h2>
|
||||
<p className="text-lg text-accent-mountain">
|
||||
We're dedicated to bridging communication gaps and fostering inclusivity <br /> through innovative solutions for the Deaf community.
|
||||
We are dedicated to bridging language gaps, fostering inclusivity, and celebrating <br /> the unique contributions of the Deaf community through innovative solutions that benefit everyone.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useNotification } from '../context/notification-context'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FaBookReader, FaTimes, FaVideo } from 'react-icons/fa'
|
||||
import { FaBookReader, FaTimes, FaVideo, FaLink } from 'react-icons/fa'
|
||||
import VideoPlayer from '../components/VideoPlayer'
|
||||
|
||||
interface Video {
|
||||
|
|
@ -20,7 +22,11 @@ interface Category {
|
|||
videos: Video[]
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 6
|
||||
const ITEMS_PER_PAGE = {
|
||||
mobile: 3,
|
||||
tablet: 4,
|
||||
desktop: 6
|
||||
}
|
||||
|
||||
const resourceCategories: Category[] = [
|
||||
{
|
||||
|
|
@ -28,6 +34,24 @@ const resourceCategories: Category[] = [
|
|||
title: 'Professional Development & Governance',
|
||||
description: 'Essential training materials for effective organizational management and parliamentary procedures',
|
||||
videos: [
|
||||
{
|
||||
id: 'governance-documents',
|
||||
title: 'Governance Documents: Understanding the Hierarchy',
|
||||
description: 'Expert parliamentarian Mark Apodaca explains the complete hierarchy of governance documents for non-profit organizations, from state laws and Articles of Incorporation down to bylaws, standing rules, and customs',
|
||||
thumbnail: '/images/thumbnails/governance-documents.png',
|
||||
videoSrc: '/videos/governance-documents.mp4',
|
||||
vttSrc: '/subtitles/governance-documents.vtt',
|
||||
transcriptSrc: '/transcriptions/governance-documents.txt'
|
||||
},
|
||||
{
|
||||
id: 'secretary-role-responsibilities',
|
||||
title: 'Secretary Role & Responsibilities: Comprehensive Guide',
|
||||
description: 'Expert parliamentarian Mark Apodaca provides a detailed overview of a secretary\'s essential duties, including keeping meeting minutes, maintaining member records, and managing organizational documentation',
|
||||
thumbnail: '/images/thumbnails/secretary-role-responsibilities.png',
|
||||
videoSrc: '/videos/secretary-role-responsibilities.mp4',
|
||||
vttSrc: '/subtitles/secretary-role-responsibilities.vtt',
|
||||
transcriptSrc: '/transcriptions/secretary-role-responsibilities.txt'
|
||||
},
|
||||
{
|
||||
id: 'minutes-approval-procedure',
|
||||
title: 'Meeting Minutes Approval: Expert Guidelines',
|
||||
|
|
@ -69,13 +93,47 @@ const resourceCategories: Category[] = [
|
|||
]
|
||||
|
||||
const Resources = () => {
|
||||
const { videoId } = useParams<{ videoId?: string }>()
|
||||
const navigate = useNavigate()
|
||||
const { showNotification } = useNotification()
|
||||
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null)
|
||||
const [currentPage, setCurrentPage] = useState<Record<string, number>>({})
|
||||
const [showTranscript, setShowTranscript] = useState(false)
|
||||
const [transcriptContent, setTranscriptContent] = useState('')
|
||||
const [screenWidth, setScreenWidth] = useState(window.innerWidth)
|
||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
||||
const [subscriptionForm, setSubscriptionForm] = useState({
|
||||
name: '',
|
||||
email: ''
|
||||
})
|
||||
const popupRef = useRef<HTMLDivElement>(null)
|
||||
const transcriptRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Handle direct video links
|
||||
useEffect(() => {
|
||||
if (videoId) {
|
||||
const video = resourceCategories
|
||||
.flatMap(category => category.videos)
|
||||
.find(v => v.id === videoId)
|
||||
|
||||
if (video) {
|
||||
setSelectedVideo(video)
|
||||
}
|
||||
}
|
||||
}, [videoId])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setScreenWidth(window.innerWidth)
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const getItemsPerPage = () => {
|
||||
if (screenWidth < 640) return ITEMS_PER_PAGE.mobile
|
||||
if (screenWidth < 1024) return ITEMS_PER_PAGE.tablet
|
||||
return ITEMS_PER_PAGE.desktop
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
|
||||
|
|
@ -102,59 +160,154 @@ const Resources = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const getPageCount = (videoCount: number) => Math.ceil(videoCount / ITEMS_PER_PAGE)
|
||||
const getPageCount = (videoCount: number) => Math.ceil(videoCount / getItemsPerPage())
|
||||
|
||||
const getCurrentPageVideos = (videos: Video[], categoryId: string) => {
|
||||
const page = currentPage[categoryId] || 1
|
||||
const start = (page - 1) * ITEMS_PER_PAGE
|
||||
return videos.slice(start, start + ITEMS_PER_PAGE)
|
||||
const itemsPerPage = getItemsPerPage()
|
||||
const start = (page - 1) * itemsPerPage
|
||||
return videos.slice(start, start + itemsPerPage)
|
||||
}
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isSubscribing) return
|
||||
setIsSubscribing(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subscriptionForm),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Thanks! You\'ll be notified when new videos are added.', 'success')
|
||||
setSubscriptionForm({ name: '', email: '' })
|
||||
} else {
|
||||
showNotification(data.message || 'Failed to subscribe', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'An error occurred. Please try again later.'
|
||||
showNotification(errorMessage, 'error')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsSubscribing(false)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4 py-20">
|
||||
<h1 className="text-4xl font-bold text-secondary mb-16 text-center">
|
||||
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-20">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-secondary mb-12 sm:mb-16 text-center">
|
||||
Resource Library
|
||||
</h1>
|
||||
|
||||
{resourceCategories.map((category) => (
|
||||
<div key={category.id} className="mb-20">
|
||||
<div key={category.id} className="mb-16">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-secondary mb-2">{category.title}</h2>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-secondary mb-2">{category.title}</h2>
|
||||
<p className="text-accent-mountain">{category.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{getCurrentPageVideos(category.videos, category.id).map((video) => (
|
||||
<div key={video.id} className="group">
|
||||
<div className="relative aspect-video overflow-hidden rounded-lg shadow-lg">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{getCurrentPageVideos(category.videos, category.id).map((video, index) => (
|
||||
<motion.div
|
||||
key={video.id}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="group bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-500 transform hover:-translate-y-2 border border-gray-100"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-4">
|
||||
<button
|
||||
onClick={() => setSelectedVideo(video)}
|
||||
className="bg-primary/90 hover:bg-primary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
|
||||
{/* Gradient overlay for better text readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-60 group-hover:opacity-80 transition-opacity duration-300" />
|
||||
|
||||
{/* Always visible play icon */}
|
||||
<div className="absolute top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full p-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<FaVideo className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
|
||||
{/* Hover overlay with buttons */}
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col items-center justify-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => {
|
||||
setSelectedVideo(video)
|
||||
navigate(`/resources/video/${video.id}`, { replace: true })
|
||||
}}
|
||||
className="bg-gradient-to-r from-primary to-secondary hover:from-secondary hover:to-primary text-white px-6 py-3 rounded-xl flex items-center gap-3 font-semibold shadow-lg backdrop-blur-sm border border-white/20 transition-all duration-300"
|
||||
aria-label={`Watch ${video.title}`}
|
||||
>
|
||||
<FaVideo className="w-4 h-4" /> Watch Video
|
||||
</motion.button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => {
|
||||
const url = `${window.location.origin}/resources/video/${video.id}`
|
||||
navigator.clipboard.writeText(url)
|
||||
showNotification('Link copied to clipboard!', 'success')
|
||||
}}
|
||||
className="bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-300 border border-white/20"
|
||||
aria-label={`Copy link to ${video.title}`}
|
||||
>
|
||||
<FaVideo /> Watch Video
|
||||
</button>
|
||||
<FaLink className="w-4 h-4" /> Share
|
||||
</motion.button>
|
||||
|
||||
{video.transcriptSrc && (
|
||||
<button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => fetchTranscript(video.id)}
|
||||
className="bg-secondary/90 hover:bg-secondary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
|
||||
className="bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-300 border border-white/20"
|
||||
aria-label={`Read transcript of ${video.title}`}
|
||||
>
|
||||
<FaBookReader /> Read Transcript
|
||||
</button>
|
||||
<FaBookReader className="w-4 h-4" /> Transcript
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold text-secondary">{video.title}</h3>
|
||||
<p className="text-sm text-accent-mountain">{video.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold text-secondary mb-3 line-clamp-2 leading-tight group-hover:text-primary transition-colors duration-300">
|
||||
{video.title}
|
||||
</h3>
|
||||
<p className="text-accent-mountain line-clamp-3 leading-relaxed text-sm">
|
||||
{video.description}
|
||||
</p>
|
||||
|
||||
{/* Bottom action bar */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||
<span className="text-xs text-accent-mountain/70 uppercase tracking-wide font-medium">
|
||||
Professional Training
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 rounded-full bg-primary/20"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -164,11 +317,13 @@ const Resources = () => {
|
|||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentPage({ ...currentPage, [category.id]: idx + 1 })}
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
(currentPage[category.id] || 1) === idx + 1
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-colors
|
||||
${(currentPage[category.id] || 1) === idx + 1
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-white text-primary border border-primary'
|
||||
}`}
|
||||
: 'bg-white text-primary border border-primary hover:bg-primary/10'
|
||||
}`}
|
||||
aria-label={`Page ${idx + 1}`}
|
||||
aria-current={(currentPage[category.id] || 1) === idx + 1 ? 'page' : undefined}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
|
|
@ -177,16 +332,86 @@ const Resources = () => {
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Subscription Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 sm:p-8"
|
||||
>
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-secondary mb-2">Stay Updated</h2>
|
||||
<p className="text-accent-mountain">Get notified when we add new training videos</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubscribe} className="space-y-4">
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="sub-name" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sub-name"
|
||||
value={subscriptionForm.name}
|
||||
onChange={(e) => setSubscriptionForm({ ...subscriptionForm, name: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
||||
text-base sm:text-lg"
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="sub-email" className="block text-accent-mountain mb-2 text-sm sm:text-base">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="sub-email"
|
||||
value={subscriptionForm.email}
|
||||
onChange={(e) => setSubscriptionForm({ ...subscriptionForm, email: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
|
||||
text-base sm:text-lg"
|
||||
required
|
||||
pattern="[^@\s]+@[^@\s]+\.[^@\s]+"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="submit"
|
||||
disabled={isSubscribing}
|
||||
className={`w-full py-3 rounded-lg font-semibold text-base sm:text-lg transition-colors
|
||||
${isSubscribing
|
||||
? 'bg-accent-mountain text-accent-snow cursor-not-allowed'
|
||||
: 'bg-primary text-accent-snow hover:bg-secondary active:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
{isSubscribing ? 'Subscribing...' : 'Subscribe to Updates'}
|
||||
</motion.button>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Video Modal */}
|
||||
{selectedVideo && (
|
||||
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50">
|
||||
<div className="w-4/5 max-w-6xl" ref={popupRef}>
|
||||
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="w-full max-w-6xl" ref={popupRef}>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setSelectedVideo(null)}
|
||||
onClick={() => {
|
||||
setSelectedVideo(null)
|
||||
navigate('/resources', { replace: true })
|
||||
}}
|
||||
className="absolute -top-12 right-0 text-white hover:text-accent-lake bg-black/50 p-2 rounded-full transition-colors"
|
||||
aria-label="Close video"
|
||||
>
|
||||
<FaTimes className="w-6 h-6" />
|
||||
</button>
|
||||
|
|
@ -207,13 +432,13 @@ const Resources = () => {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 50, opacity: 0 }}
|
||||
className="w-4/5 max-w-4xl bg-white rounded-xl shadow-2xl"
|
||||
className="w-full max-w-4xl bg-white rounded-xl shadow-2xl"
|
||||
ref={transcriptRef}
|
||||
>
|
||||
<div className="p-6 flex justify-between items-center border-b border-gray-200">
|
||||
|
|
@ -221,11 +446,12 @@ const Resources = () => {
|
|||
<button
|
||||
onClick={() => setShowTranscript(false)}
|
||||
className="text-accent-mountain hover:text-secondary transition-colors bg-accent-snow/50 p-2 rounded-full"
|
||||
aria-label="Close transcript"
|
||||
>
|
||||
<FaTimes size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 max-h-[70vh] overflow-y-auto prose prose-slate">
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto prose prose-slate">
|
||||
{transcriptContent.split('\n\n').map((paragraph, index) => (
|
||||
<p key={index} className="mb-4 text-accent-mountain leading-relaxed">
|
||||
{paragraph}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { ComposableMap, Geographies, Geography, Marker } from 'react-simple-maps'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { FaFileAlt, FaGlobe, FaUsers, FaChalkboardTeacher, FaTimes, FaMapMarkerAlt, FaClock } from 'react-icons/fa'
|
||||
import { FaUsers, FaTimes, FaMapMarkerAlt, FaClock } from 'react-icons/fa'
|
||||
|
||||
const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"
|
||||
|
||||
|
|
@ -139,29 +139,157 @@ const relationshipPoints: RelationshipPoint[] = [
|
|||
startDate: "December 15, 2024",
|
||||
endDate: "December 15, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-112.0740, 33.4484] as Coordinates,
|
||||
city: "Phoenix",
|
||||
state: "Arizona",
|
||||
events: [
|
||||
{
|
||||
name: "DANE",
|
||||
description: "Deaf Abuse Network and Education - Advocacy Round-table Discussion",
|
||||
startDate: "May 31, 2025",
|
||||
endDate: "May 31, 2025"
|
||||
},
|
||||
{
|
||||
name: "NAP",
|
||||
description: "National Association of Parliamentarians - NAP 45th Biennial Convention",
|
||||
startDate: "August 20, 2025",
|
||||
endDate: "August 24, 2025"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
coordinates: [-113.9940, 46.8721] as Coordinates,
|
||||
city: "Missoula",
|
||||
state: "Montana",
|
||||
events: [
|
||||
{
|
||||
name: "RID",
|
||||
description: "Registry of Interpreters for the Deaf - 2026 Biennial Conference Parliamentarian",
|
||||
startDate: "January 10, 2026",
|
||||
endDate: "January 10, 2026"
|
||||
},
|
||||
{
|
||||
name: "AIP",
|
||||
description: "American Institute of Parliamentarians - 2025 Spring Practicum (Online)",
|
||||
startDate: "June 12, 2025",
|
||||
endDate: "June 14, 2025"
|
||||
},
|
||||
{
|
||||
name: "MCDi",
|
||||
description: "Missoula Council of the Deaf, Inc. - Board Training",
|
||||
startDate: "April 25, 2026",
|
||||
endDate: "April 25, 2026"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
coordinates: [-97.3375, 37.6872] as Coordinates,
|
||||
city: "Wichita",
|
||||
state: "Kansas",
|
||||
events: [{
|
||||
name: "KAD",
|
||||
description: "Kansas Association of the Deaf - 2025 General Business Meeting",
|
||||
startDate: "June 21, 2025",
|
||||
endDate: "June 21, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-91.1871, 30.4515] as Coordinates,
|
||||
city: "Baton Rouge",
|
||||
state: "Louisiana",
|
||||
events: [{
|
||||
name: "LAD",
|
||||
description: "Louisiana Association of the Deaf - 2025 Conference",
|
||||
startDate: "June 26, 2025",
|
||||
endDate: "June 28, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-77.0369, 38.9072] as Coordinates,
|
||||
city: "Washington",
|
||||
state: "DC",
|
||||
events: [{
|
||||
name: "ASLTA",
|
||||
description: "American Sign Language Teachers Association - National Professional Development Conference Parliamentarian",
|
||||
startDate: "July 9, 2025",
|
||||
endDate: "July 12, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-94.5786, 39.0997] as Coordinates,
|
||||
city: "Kansas City",
|
||||
state: "Missouri",
|
||||
events: [{
|
||||
name: "NBDA",
|
||||
description: "National Black Deaf Advocates - Building Our Legacy: The Road To Success",
|
||||
startDate: "August 13, 2025",
|
||||
endDate: "August 16, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-90.0715, 29.9511] as Coordinates,
|
||||
city: "New Orleans",
|
||||
state: "Louisiana",
|
||||
events: [{
|
||||
name: "LAD",
|
||||
description: "Louisiana Association of the Deaf - Board Training",
|
||||
startDate: "September 26, 2025",
|
||||
endDate: "September 28, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-97.7431, 30.2672] as Coordinates,
|
||||
city: "Austin",
|
||||
state: "Texas",
|
||||
events: [{
|
||||
name: "Deaf Collab: Austin 2025",
|
||||
description: "Deaf Collab: Austin 2025",
|
||||
startDate: "October 29, 2025",
|
||||
endDate: "November 1, 2025"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-92.1735, 38.5767] as Coordinates,
|
||||
city: "Jefferson City",
|
||||
state: "Missouri",
|
||||
events: [{
|
||||
name: "MOAD",
|
||||
description: "Missouri Association of the Deaf - Board Training",
|
||||
startDate: "February 7, 2026",
|
||||
endDate: "February 7, 2026"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-122.4194, 37.7749] as Coordinates,
|
||||
city: "San Francisco",
|
||||
state: "California",
|
||||
events: [{
|
||||
name: "NAD",
|
||||
description: "National Association of the Deaf Conference",
|
||||
startDate: "June 30, 2026",
|
||||
endDate: "July 5, 2026"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-86.8025, 33.5207] as Coordinates,
|
||||
city: "Birmingham",
|
||||
state: "Alabama",
|
||||
events: [{
|
||||
name: "NAP NTC",
|
||||
description: "National Association of Parliamentarians - National Training Conference - Presenter",
|
||||
startDate: "August 26, 2026",
|
||||
endDate: "August 30, 2026"
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
const services = [
|
||||
{
|
||||
icon: <FaFileAlt className="text-4xl" />,
|
||||
title: "ADA-Compliant Transcripts",
|
||||
description: "High-quality, accurate transcripts with speaker identification and context differentiation."
|
||||
},
|
||||
{
|
||||
icon: <FaGlobe className="text-4xl" />,
|
||||
title: "Universal Website Design",
|
||||
description: "Visually appealing and accessible websites adhering to universal design principles."
|
||||
},
|
||||
{
|
||||
icon: <FaUsers className="text-4xl" />,
|
||||
title: "Board Communication",
|
||||
description: "Comprehensive protocol and tools for effective board communication."
|
||||
},
|
||||
{
|
||||
icon: <FaChalkboardTeacher className="text-4xl" />,
|
||||
title: "Training & Workshops",
|
||||
description: "Leadership development and parliamentary procedure training."
|
||||
title: "DeafGain Consultants",
|
||||
description: "Contact us to learn how we can make your organization GREAT. We offer a variety of services customized to fit your needs."
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -195,18 +323,18 @@ const Services = () => {
|
|||
<h1 className="text-4xl font-bold text-center text-secondary mb-16">
|
||||
Our Services
|
||||
</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="flex justify-center">
|
||||
{services.map((service, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.2 }}
|
||||
className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow"
|
||||
className="bg-white p-8 rounded-lg shadow-lg hover:shadow-xl transition-shadow max-w-2xl w-full text-center"
|
||||
>
|
||||
<div className="text-primary mb-4">{service.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{service.title}</h3>
|
||||
<p className="text-accent-mountain">{service.description}</p>
|
||||
<div className="text-primary mb-6 flex justify-center">{service.icon}</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">{service.title}</h3>
|
||||
<p className="text-accent-mountain text-lg">{service.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ export default {
|
|||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
screens: {
|
||||
'xs': '375px',
|
||||
'sm': '640px',
|
||||
'md': '768px',
|
||||
'lg': '1024px',
|
||||
'xl': '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
'primary': '#647C90', // Slate blue
|
||||
|
|
@ -14,8 +22,53 @@ export default {
|
|||
'mountain': '#4A5568', // Mountain gray
|
||||
'lake': '#A4C3D2', // Alpine lake blue
|
||||
}
|
||||
}
|
||||
},
|
||||
fontSize: {
|
||||
'xs': ['0.75rem', { lineHeight: '1rem' }],
|
||||
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
'base': ['1rem', { lineHeight: '1.5rem' }],
|
||||
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
||||
},
|
||||
spacing: {
|
||||
'18': '4.5rem',
|
||||
'22': '5.5rem',
|
||||
},
|
||||
maxWidth: {
|
||||
'xxs': '16rem',
|
||||
'mobile': '20rem',
|
||||
},
|
||||
minHeight: {
|
||||
'screen-75': '75vh',
|
||||
},
|
||||
padding: {
|
||||
'safe': 'env(safe-area-inset-bottom)',
|
||||
},
|
||||
zIndex: {
|
||||
'60': '60',
|
||||
'70': '70',
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-in-out',
|
||||
'slide-up': 'slideUp 0.4s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
columns: false // This will disable the column utilities
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
tsconfig.server.json
Normal file
13
tsconfig.server.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["server.ts"]
|
||||
}
|
||||
|
|
@ -3,13 +3,38 @@ import react from '@vitejs/plugin-react'
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
copyPublicDir: true
|
||||
},
|
||||
preview: {
|
||||
port: 804,
|
||||
host: true,
|
||||
strictPort: true
|
||||
},
|
||||
server: {
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
host: true, // Allow external access
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:804',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['react-icons']
|
||||
define: {
|
||||
'process.env': {
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
SMTP_PORT: process.env.SMTP_PORT,
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
RECIPIENT_EMAIL: process.env.RECIPIENT_EMAIL,
|
||||
GOOGLE_EMAIL: process.env.GOOGLE_EMAIL,
|
||||
GOOGLE_APP_PASSWORD: process.env.GOOGLE_APP_PASSWORD,
|
||||
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
|
||||
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
184
vpn-manager.command
Executable file
184
vpn-manager.command
Executable file
|
|
@ -0,0 +1,184 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# VPN Configuration
|
||||
VPN_CONFIG="$HOME/wg1.conf"
|
||||
VPN_NAME="Proxmox Colocation VPN"
|
||||
VPN_IP="10.6.0.3"
|
||||
VPN_NETWORK="10.6.0.0/24, 10.4.0.0/24"
|
||||
|
||||
# Function to get active VPN interface
|
||||
get_active_vpn() {
|
||||
sudo wg show interfaces 2>/dev/null
|
||||
}
|
||||
|
||||
# Function to show detailed status
|
||||
show_status() {
|
||||
echo "🔍 Checking VPN connection..."
|
||||
echo ""
|
||||
|
||||
local active_interface=$(get_active_vpn)
|
||||
|
||||
if [ -z "$active_interface" ]; then
|
||||
echo "🔴 VPN not connected"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "🟢 VPN Connected:"
|
||||
echo ""
|
||||
sudo wg show
|
||||
echo ""
|
||||
|
||||
# Show routing information
|
||||
echo "📡 Network Routes:"
|
||||
netstat -rn | grep -E "10\.(4|6)\.0" | awk '{printf " %-20s -> %s\n", $1, $NF}'
|
||||
}
|
||||
|
||||
# Function to connect VPN
|
||||
connect_vpn() {
|
||||
if [ ! -f "$VPN_CONFIG" ]; then
|
||||
echo "❌ Configuration file not found: $VPN_CONFIG"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "🔄 Connecting to $VPN_NAME..."
|
||||
result=$(sudo wg-quick up "$VPN_CONFIG" 2>&1)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Connected Successfully!"
|
||||
echo " VPN IP: $VPN_IP"
|
||||
echo " Networks: $VPN_NETWORK"
|
||||
sleep 1
|
||||
return 0
|
||||
else
|
||||
if echo "$result" | grep -q "already exists"; then
|
||||
echo "ℹ️ VPN is already connected"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to connect:"
|
||||
echo "$result" | head -5
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to disconnect VPN
|
||||
disconnect_vpn() {
|
||||
if [ ! -f "$VPN_CONFIG" ]; then
|
||||
echo "❌ Configuration file not found: $VPN_CONFIG"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "🔄 Disconnecting $VPN_NAME..."
|
||||
result=$(sudo wg-quick down "$VPN_CONFIG" 2>&1)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Disconnected Successfully!"
|
||||
return 0
|
||||
else
|
||||
if echo "$result" | grep -q "is not a WireGuard interface"; then
|
||||
echo "ℹ️ VPN is not connected"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to disconnect:"
|
||||
echo "$result" | head -5
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test connectivity
|
||||
test_connectivity() {
|
||||
local active_interface=$(get_active_vpn)
|
||||
|
||||
if [ -z "$active_interface" ]; then
|
||||
echo "❌ VPN not connected. Please connect first."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "🔍 Testing connectivity..."
|
||||
echo ""
|
||||
|
||||
# Test internal network (10.4.0.0/24)
|
||||
echo "Testing Internal Network (10.4.0.0/24):"
|
||||
if ping -c 1 -W 2 10.4.0.1 >/dev/null 2>&1; then
|
||||
echo " ✅ Gateway (10.4.0.1): Reachable"
|
||||
else
|
||||
echo " ⚠️ Gateway (10.4.0.1): Not reachable"
|
||||
fi
|
||||
|
||||
if ping -c 1 -W 2 10.4.0.148 >/dev/null 2>&1; then
|
||||
echo " ✅ Proxmox Host (10.4.0.148): Reachable"
|
||||
else
|
||||
echo " ⚠️ Proxmox Host (10.4.0.148): Not reachable"
|
||||
fi
|
||||
|
||||
if ping -c 1 -W 2 10.4.0.205 >/dev/null 2>&1; then
|
||||
echo " ✅ DeafGain Server (10.4.0.205): Reachable"
|
||||
else
|
||||
echo " ⚠️ DeafGain Server (10.4.0.205): Not reachable"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear screen and show header
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🔐 WireGuard VPN Manager - Proxmox Colocation"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
# Main menu
|
||||
while true; do
|
||||
# Check VPN status
|
||||
active_interface=$(get_active_vpn)
|
||||
|
||||
if [ -n "$active_interface" ]; then
|
||||
echo "🟢 Status: VPN Connected"
|
||||
else
|
||||
echo "🔴 Status: VPN Disconnected"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "Options:"
|
||||
echo "1) Connect to VPN"
|
||||
echo "2) Disconnect from VPN"
|
||||
echo "3) Show Detailed Status"
|
||||
echo "4) Test Connectivity"
|
||||
echo "5) Quit"
|
||||
echo ""
|
||||
read -p "Enter your choice (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
echo ""
|
||||
connect_vpn
|
||||
;;
|
||||
2)
|
||||
echo ""
|
||||
disconnect_vpn
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
show_status
|
||||
;;
|
||||
4)
|
||||
echo ""
|
||||
test_connectivity
|
||||
;;
|
||||
5)
|
||||
echo ""
|
||||
echo "👋 Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
echo "❌ Invalid choice. Please enter 1-5."
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
done
|
||||
10
wg1.conf
Normal file
10
wg1.conf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[Interface]
|
||||
PrivateKey = eDcRLqehay1uqtv+ltkxgWc4EtPMlFyZRVzfQSZ6zWM=
|
||||
Address = 10.6.0.3/24
|
||||
DNS = 1.1.1.1
|
||||
|
||||
[Peer]
|
||||
PublicKey = ee4ywiTbFZ3Dlg74FqIwTQQMo4vR6IgKrDQ+Yos7gHo=
|
||||
Endpoint = 74.80.182.50:51820
|
||||
AllowedIPs = 10.6.0.0/24, 10.4.0.0/24
|
||||
PersistentKeepalive = 25
|
||||
Loading…
Add table
Reference in a new issue