Compare commits

..

98 commits

Author SHA1 Message Date
friday-bot
d3cb7795b7 Handle video play() promise rejection with visible error state (closes #23) 2026-07-14 11:10:21 -06:00
f91bbd384d Add Presenter to NAP NTC Birmingham event on service map (closes #22) 2026-05-24 11:55:22 -06:00
54bfdbedce Remove San Diego AIP Annual Session from service map (closes #21) 2026-05-24 11:40:29 -06:00
Eliza kragh
95c7ad944c Update memory bank: Add node_modules installation entry 2026-04-06 15:04:32 -06:00
Eliza kragh
3906d069a4 Update memory bank: Photo update and AIP membership addition (April 6, 2026) 2026-04-06 15:00:15 -06:00
Eliza kragh
4f9a7ac018 Add American Institute of Parliamentarians membership to certifications 2026-04-06 14:56:53 -06:00
Eliza kragh
8cc11c3352 Update Eliza's photo - rename to eliza-2026.jpg to bypass CDN cache 2026-04-06 14:52:54 -06:00
Eliza kragh
9a9be40151 Update Eliza's photo on About page 2026-04-06 14:43:35 -06:00
Eliza kragh
036fdb93f2 Update memory bank: Added 4 new network relationships to map (now 23 locations) 2026-04-04 18:46:18 -06:00
Eliza kragh
833fb12305 Add AIP Annual Session to San Diego CA on interactive map 2026-04-04 18:42:52 -06:00
Eliza kragh
2e990e72a9 Add NAP NTC to Birmingham AL on interactive map 2026-04-04 18:36:12 -06:00
Eliza kragh
107b8831b4 Add NAD Conference to San Francisco CA on interactive map 2026-04-04 18:31:01 -06:00
Eliza kragh
ea27cfa5fb Add MCDi Board Training to Missoula MT on interactive map 2026-04-04 18:26:07 -06:00
Eliza kragh
02989ce809 Update memory bank: Document March 13 About page certification update 2026-03-13 12:19:43 -06:00
Eliza kragh
be03ba9f7c Update About page: Add 'Insured' prefix to Registered Parliamentarian certification 2026-03-13 12:16:15 -06:00
Eliza kragh
d9156a9f21 Update memory bank: document About page changes from March 5, 2026 2026-03-05 13:44:28 -07:00
Eliza kragh
e660b6d8cc Remove comma from advocacy quote on About page 2026-03-05 13:41:46 -07:00
Eliza kragh
1537926e0c Add RP abbreviation and 'the' to National Association of Parliamentarians certification 2026-03-05 13:36:38 -07:00
Eliza kragh
4c768e0edf Add Certificate in Deaf Studies and Social Justice from University of Kansas to Education section 2026-03-05 13:30:22 -07:00
Eliza kragh
a410bb853e Update About page: refine Leadership text to reflect founding role at MCDi and ownership of DeafGain LLC 2026-03-05 13:25:13 -07:00
Eliza kragh
bc05b0106e Update About page: change NAP certification to Registered Parliamentarian 2026-03-05 13:02:51 -07:00
Eliza kragh
abb6f511fb Update memory bank: add MOAD event and update map location count to 20 2026-03-05 12:58:51 -07:00
Eliza kragh
a25e858a35 Add MOAD Board Training event to Jefferson City, MO on interactive map 2026-03-05 12:55:37 -07:00
Eliza kragh
ee5f7bcb6e Update memory bank: document automated deployment workflow with deploy.sh 2026-03-05 12:52:02 -07:00
Eliza kragh
1d63dfddef Add automated deployment script with VPN connection and Docker rebuild 2026-03-05 12:50:33 -07:00
107f2de66c Add WireGuard VPN configuration for Proxmox Colocation 2026-03-05 12:42:12 -07:00
Eliza kragh
efae0c098c Update memory bank: correct server IP to 10.4.0.205 and add VPN workflow 2026-03-05 12:37:57 -07:00
7d079f9b37 Add simplified VPN manager script for Proxmox Colocation only 2026-03-05 12:34:06 -07:00
Eliza kragh
596c5033a0 Add AI assistant documentation: memory bank and git workflow 2026-03-05 12:28:00 -07:00
78aade0c94 Update interactive map: Add RID conference to Missoula MT and update ASLTA description 2026-01-26 10:25:09 -07:00
4755cd3fd6 Consolidate services into single DeafGain Consultants offering with clear, accessible language 2026-01-20 11:06:37 -07:00
a565932eeb Remove service selection field from contact form - simplify form to just name, email, and message 2026-01-20 09:57:56 -07:00
ae7c52db20 Add anti-spam honeypot field and update contact form service options 2026-01-20 09:35:55 -07:00
a0cf3e9f35 Update memory bank status summary with current system state 2026-01-09 14:09:00 -07:00
df6c69684a Fix email configuration: Update SMTP port to 587 and regenerate Gmail app password 2026-01-09 13:58:07 -07:00
379c4e1e44 Add Parliamentarian service and update Austin TX event to Deaf Collab: Austin 2025 2025-10-27 09:06:53 -06:00
8b2b421156 Update email configuration: change SMTP port to 2525 and temporarily disable Redis 2025-09-11 15:00:55 -06:00
5d49c9ec72 Remove ADA-compliant transcripts service. 2025-08-28 10:40:45 -05:00
67c775a66f Add two locations 2025-08-13 11:57:53 -05:00
86096d99e3 Switch to Missoula from Helena 2025-06-15 21:06:25 -06:00
b1e7d29677 Add rebuild flags 2025-06-15 16:50:23 -06:00
b214427fda Added geographic locations for upcoming events 2025-06-15 16:29:06 -06:00
a2b3b1756d Fix environment variable references to match .env file
- Change GMAIL_USER/GMAIL_PASS to GOOGLE_EMAIL/GOOGLE_APP_PASSWORD
- Reference actual variables that exist in .env file
- Use SMTP_HOST, SMTP_PORT, SMTP_SECURE from .env
- Use RECIPIENT_EMAIL=eliza@deafgain.org from .env
2025-05-27 13:08:18 -05:00
7a752771c8 Fix email environment variables for subscription system
- Add missing SMTP configuration variables
- Map GMAIL_USER/GMAIL_PASS to expected GOOGLE_EMAIL/GOOGLE_APP_PASSWORD
- Add RECIPIENT_EMAIL and SMTP_HOST/PORT/SECURE settings
- Fixes 'Failed to process subscription' error
2025-05-27 13:07:17 -05:00
e3930cf87a Fix nginx logging configuration for statistics
- Create log directory in Dockerfile
- Add proper logging configuration to default.conf
- Mount logs directory in docker-compose.yml for persistence
- Fixes 502 error caused by missing log directory
2025-05-27 12:54:13 -05:00
3e1892a6f7 update Docker deployment 2025-05-27 12:45:16 -05:00
b6f727be5d Add Docker deployment setup and subscription functionality
- Complete Docker containerization for development and production
- Add newsletter subscription API with rate limiting
- Update governance documents with video content
- Enhance email functionality and configurations
- Update memory bank documentation
2025-05-27 12:23:45 -05:00
6e4db3ff67 Update memory bank 2025-03-25 23:26:11 -05:00
0fa61e4c55 Add Secretary's role and responsibilities video 2025-03-25 18:35:56 -05:00
4ee567e9af Add direct links for videos in Resource Library 2025-01-25 08:46:06 -06:00
28c341ea00 Fix menu button 2024-12-13 09:12:41 -08:00
8709bf66f1 Faster buffer flush for instant statistic generations 2024-12-11 22:27:57 -06:00
d65f8a7e80 Update verison number 2024-12-11 22:21:53 -06:00
03286c9f75 Change ordering 2024-12-11 22:20:55 -06:00
53ddc334f1 Remove duplicate log section 2024-12-11 22:18:17 -06:00
f18ceeb663 Rearrange log tracking sections 2024-12-11 22:07:29 -06:00
fd00d16c37 Re-do the tracking section 2024-12-11 21:59:51 -06:00
f0135e4e54 Move logging to http section 2024-12-11 21:56:01 -06:00
13d137847c Add server side logging tracking 2024-12-11 21:51:56 -06:00
28c81bddaf Update footer quote 2024-12-11 17:40:29 -06:00
4bdde33534 Updated footer quote 2024-12-11 15:59:45 -06:00
dcce8c0b3e Remove quotation mark 2024-12-11 15:47:46 -06:00
78c1422fe4 Updated a quote 2024-12-11 15:41:42 -06:00
3f23aa5e93 Add development enviornment and update documents 2024-12-10 23:43:33 -06:00
d13838758e Make correction to email account 2024-12-10 19:52:09 -06:00
3bf017543c Update contact info 2024-12-10 19:48:09 -06:00
e238c56480 Update version number 2024-12-10 19:47:21 -06:00
195aeea56b Improve mobile and small screen experiences 2024-12-10 19:42:20 -06:00
185c70e76e Make contact form functional 2024-12-10 19:21:09 -06:00
d20a4f74ed chore: update dependencies to latest versions
- React and React DOM to 19.0.0
- @types/react to 19.0.1
- @types/react-dom to 19.0.2
- @types/node to 22.10.1
- Vite to 6.0.3
- react-simple-maps to 4.0.0-beta.6
2024-12-10 11:32:38 -06:00
a09c831b2e Fix assets to be included 2024-12-09 19:18:23 -06:00
f56c7ab126 Update copying order 2024-12-09 18:43:33 -06:00
4c0fde6415 Copy all source first 2024-12-09 18:41:11 -06:00
545b49b65f add builder 2024-12-09 18:39:02 -06:00
da49f378c6 Update with assets path 2024-12-09 18:36:56 -06:00
6365a2ecb2 Fix assets to be copied 2024-12-09 18:29:25 -06:00
547b3b1440 Fix port 2024-12-09 18:24:23 -06:00
33a2488869 Update to add for videos 2024-12-09 18:20:06 -06:00
0823a15744 Fix docker related issues 2024-12-09 18:18:49 -06:00
620666e7f1 fix: copy public assets to dist 2024-12-09 18:08:59 -06:00
7b27845812 fix: create required directories in Dockerfile 2024-12-09 18:05:46 -06:00
a85d4e2ee9 fix: ensure all public assets are copied to dist 2024-12-09 18:02:23 -06:00
b4a89287bd fix: ensure public directories exist 2024-12-09 17:56:19 -06:00
2dfa0daa42 fix: ensure all public assets are copied to dist 2024-12-09 17:53:09 -06:00
05786a9517 fix: update config for video serving 2024-12-09 17:30:49 -06:00
55c2bc5e12 fix: ensure public files are copied to dist 2024-12-09 17:26:10 -06:00
90ca68e939 fix: ensure public directory exists in container 2024-12-09 17:19:50 -06:00
aec55f02c8 chore: add public directory structure and update docker-compose 2024-12-09 17:16:04 -06:00
d426b49670 Update build files 2024-12-09 17:12:44 -06:00
e41ce01162 fix: update vite config for static file serving 2024-12-09 17:03:36 -06:00
a29b2bb2b5 Add video_content directory 2024-12-09 16:37:06 -06:00
bf4b633935 chore: merge gitignore changes 2024-12-09 16:20:42 -06:00
f3f144fdab chore: ignore video files 2024-12-09 16:19:00 -06:00
8389c4a040 Update for videos to be added to resource 2024-12-09 16:17:35 -06:00
aa355375e9 temporary: untrack large files 2024-12-09 16:01:29 -06:00
140e9a532c Add Git LFS tracking for videos 2024-12-09 15:22:05 -06:00
c0bb3a6e91 Add more videos and relevant files to Resources 2024-12-09 13:45:55 -06:00
0b10547bd5 Switch from github 2024-11-30 18:37:33 -06:00
65 changed files with 8388 additions and 1035 deletions

View file

@ -2,4 +2,7 @@ node_modules
dist
.git
*.log
public/
public/videos/*
!public/videos/.gitkeep
**/.DS_Store

7
.env Normal file
View 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
View 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
View file

@ -0,0 +1,2 @@
*.mov filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text

24
.gitignore vendored
View file

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

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

View file

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

@ -1,102 +1,163 @@
# DeafGain LLC Website 🌐
# DeafGain Website
[![PNPM](https://img.shields.io/badge/package%20manager-pnpm-f69220.svg)](https://pnpm.io/)
[![TypeScript](https://img.shields.io/badge/TypeScript-4.9.5-blue.svg)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18.2.0-61dafb.svg)](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

View file

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

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

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

View file

@ -1,110 +0,0 @@
#!/bin/bash
cat > README.md << 'EOL'
# DeafGain LLC Website 🌐
[![PNPM](https://img.shields.io/badge/package%20manager-pnpm-f69220.svg)](https://pnpm.io/)
[![TypeScript](https://img.shields.io/badge/TypeScript-4.9.5-blue.svg)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18.2.0-61dafb.svg)](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
View 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
View 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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

0
public/.gitkeep Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

View 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

View 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 memberswhat 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, adjournmentsimply 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.

View 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."

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

57
server.ts Normal file
View 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}`)
})

View file

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

View file

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

View file

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

View file

@ -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'
}`}

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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