Initial commit with correct file structure
This commit is contained in:
commit
eeeca351ec
36 changed files with 6473 additions and 0 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Production
|
||||
dist
|
||||
build
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
public/videos/
|
||||
public/images/
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Build stage
|
||||
FROM node:18-alpine as build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
72
README.md
Normal file
72
README.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# DeafGain LLC Website
|
||||
|
||||
## 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.
|
||||
|
||||
## Core Features
|
||||
|
||||
Intuitive user interface with responsive design
|
||||
Interactive USA region map displaying network and relationships
|
||||
Video resources with accessible playback controls and captions
|
||||
Detailed service descriptions
|
||||
Secure contact system
|
||||
Key sections: Home, Services, About, Resources, and Contact
|
||||
Technical Stack
|
||||
React 18 with TypeScript
|
||||
Tailwind CSS for styling
|
||||
Framer Motion for animations
|
||||
React Simple Maps for geographic visualization
|
||||
Vite build system
|
||||
Docker containerization
|
||||
Nginx web server
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development
|
||||
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
Visit localhost:3000 in your browser.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
docker-compose up --build
|
||||
|
||||
Application will be served on port 80.
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
├── pages/ # Main application pages
|
||||
├── assets/ # Static assets
|
||||
public/
|
||||
├── images/ # Image assets
|
||||
├── videos/ # Video content
|
||||
├── subtitles/ # Video captions
|
||||
├── transcriptions/# Text transcripts
|
||||
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Development: Uses Vite dev server with hot reload
|
||||
Production: Nginx configuration with optimized cache settings
|
||||
Docker containers for consistent deployment
|
||||
|
||||
# Contributing
|
||||
|
||||
We welcome contributions that align with our mission of improving accessibility and empowerment for the Deaf community. Follow the established patterns for:
|
||||
|
||||
Component architecture
|
||||
Styling conventions
|
||||
Accessibility standards
|
||||
TypeScript typing
|
||||
License
|
||||
Proprietary - DeafGain LLC
|
||||
|
||||
# Contact
|
||||
For questions or support, reach out through our contact form.
|
||||
|
||||
This website represents DeafGain LLC's commitment to creating accessible, user-friendly digital experiences while maintaining high technical standards and professional quality.
|
||||
68
ROADMAP
Normal file
68
ROADMAP
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
|
||||
_______________________________________________________________________
|
||||
|
||||
Executive Summary
|
||||
|
||||
DeafGain LLC is a social impact company dedicated to empowering the Deaf community through effective communication, universal website design, and leadership training. We provide ADA-compliant transcripts, website development, board communication protocol and tools, online payment integration, and training on parliamentary procedure and “Good to Great” leadership for Deaf organizations.
|
||||
|
||||
Company Description
|
||||
|
||||
DeafGain LLC is a limited liability company (LLC) registered in Montana. Our mission is to uplift the Deaf community by providing a platform for empowerment, recruitment, and volunteer engagement. We aim to bridge the communication gap between Deaf individuals and mainstream society, fostering inclusivity and accessibility.
|
||||
|
||||
Services
|
||||
|
||||
ADA-Compliant Transcripts: DeafGain develops and provides high-quality, accurate transcripts of meetings, videos, podcasts, conferences, and etc. with Speaker Identification, Context, and statement/question differentiation with minimal cost of overhead.
|
||||
Universal Website Design: We design both visually appealing and accessible websites to all users, adhering to universal design principles.
|
||||
Website Development: Our team sets up online platforms for Deaf organizations, including communications system setup, online payment integration (e.g., PayPal, Stripe), and content management systems.
|
||||
|
||||
Training and Workshops:
|
||||
|
||||
ADA and Parliamentary Procedure: We train board members on inclusive decision-making processes.
|
||||
Good to Great Leadership Training: We empower leaders with a passionate and authentic mindset to drive Deaf organizations forward.
|
||||
|
||||
Market Analysis
|
||||
|
||||
The demand for accessible communication and website design is high among Deaf organizations, driven by increasing awareness of the importance of inclusivity. Our target market includes:
|
||||
|
||||
Deaf Organizations: National associations, local clubs, schools, and charities catering to the Deaf community.
|
||||
Conference and Event Planners: Individuals organizing events for the Deaf community.
|
||||
Government Agencies: Municipalities and state governments seeking to improve accessibility and inclusivity.
|
||||
Higher Education: Universities that are looking for transcripts for their Deaf students.
|
||||
|
||||
Marketing Strategy
|
||||
|
||||
Networking: Attend conferences, workshops, and networking sessions within the Deaf community to establish relationships and promote services.
|
||||
Digital Marketing: Utilize social media platforms (Facebook, Instagram) and online advertising (Daily Moth, Client newsletters, etc.) to reach target audiences.
|
||||
Collaboration: Partner with other organizations serving the Deaf community to maximize the benefits of collaboration for all parties involved.
|
||||
|
||||
|
||||
DeafGain LLC is poised to revolutionize communication and accessibility for the Deaf community. By providing ADA-compliant transcripts, universal website design, training, and workshops, we empower Deaf organizations to thrive and reach new heights of inclusivity.
|
||||
|
||||
After all, “language is a right, not a luxury.”
|
||||
|
||||
__________________________________________________________________________
|
||||
|
||||
|
||||
Eliza Kragh: A Beacon of Advocacy
|
||||
|
||||
As a proud Montana native, Eliza Kragh has dedicated her life to empowering the Deaf community through advocacy. Born into a family of lawyers, Eliza defied expectations by pursuing her passion for serving others.
|
||||
|
||||
A lifelong learner, Eliza earned her Bachelor's degree in Liberal Studies with a Psychology minor from the University of Montana and later completed her Master's in Human Resource Development at Rochester Institute of Technology. Her academic journey was marked by a relentless pursuit of knowledge and understanding.
|
||||
|
||||
Eliza's career path has been shaped by her unwavering commitment to advocacy. She transitioned from Veterinary Medicine to dedicate herself fully to empowering the Deaf community, establishing DeafGain, LLC – an organization that authentically represents and serves the needs of Montana's Deaf community.
|
||||
|
||||
Certifications & Affiliations
|
||||
|
||||
A Certified ADA Coordinator, Eliza brings expertise in disability rights advocacy. As a member of the National Association of Parliamentarians, she continues to expand her knowledge and impact. Currently pursuing certifications as a Registered Parliamentarian and completing a certificate program in Deaf Studies and Social Justice through Kansas University.
|
||||
|
||||
Visionary Leader
|
||||
|
||||
As President of Missoula Council of the Deaf, a non-profit organization that she helped establish, Deaf Mentor, and founder of DeafGain, LLC, Eliza is a force for change. Her mission to create a deaf-designed statewide hub promoting understanding, empowerment, and inclusivity inspires all who meet her. With two sets of footprints – one in advocacy and one in Montana's beautiful landscape – Eliza embarks on each new day with courage, dedication, and a steadfast commitment to the Deaf community.
|
||||
|
||||
Join Eliza on this enlightening path toward Deaf rights advocacy.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
57
Tech_Stack
Normal file
57
Tech_Stack
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
## 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
|
||||
|
||||
### Backend
|
||||
- Node.js with Express
|
||||
- TypeScript
|
||||
- Nodemailer for email handling
|
||||
- CORS for cross-origin resource sharing
|
||||
- dotenv for environment variable management
|
||||
|
||||
### Development & Build Tools
|
||||
- Docker & Docker Compose
|
||||
- PostCSS
|
||||
- Autoprefixer
|
||||
- npm
|
||||
|
||||
### Production
|
||||
- Nginx (Production server for frontend)
|
||||
- Node.js server for backend API
|
||||
|
||||
### Architecture
|
||||
- Frontend running on port 3000 (development)
|
||||
- Backend API running on port 4000
|
||||
- Containerized development and production environments
|
||||
- REST API for contact form handling
|
||||
- Gmail SMTP for email delivery
|
||||
|
||||
### Key Features
|
||||
- Responsive design
|
||||
- Animated UI components
|
||||
- Interactive USA region map
|
||||
- Contact form with email notifications
|
||||
- Timeline visualization
|
||||
- Service showcase
|
||||
- Biography section
|
||||
- Testimonials section
|
||||
|
||||
### Development Environment
|
||||
- Multi-container setup with Docker Compose
|
||||
- Hot-reloading enabled
|
||||
- Separate development and production builds
|
||||
- Environment variable management for sensitive data
|
||||
|
||||
### Production Environment
|
||||
- Nginx serving static frontend files
|
||||
- Node.js backend API
|
||||
- Containerized deployment
|
||||
- Secure email handling with Google App credentials
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "80:80"
|
||||
restart: always
|
||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
14
index.html
Normal file
14
index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
nginx.conf
Normal file
17
nginx.conf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
}
|
||||
4482
package-lock.json
generated
Normal file
4482
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "deafgain-website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"framer-motion": "^11.11.17",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"tailwindcss": "^3.4.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.12.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.4.11"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
83
public/subtitles/minutes-approval-procedure.vtt
Normal file
83
public/subtitles/minutes-approval-procedure.vtt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
WEBVTT
|
||||
|
||||
00:00:10.590 --> 00:00:11.100
|
||||
Hello!
|
||||
|
||||
00:00:11.120 --> 00:00:15.110
|
||||
This is Eliza Kragh from Montana.
|
||||
|
||||
00:00:15.820 --> 00:00:24.420
|
||||
Today, I'm here with Mark Apodaca, who is an expert in parliamentary procedures.
|
||||
|
||||
00:00:24.420 --> 00:00:28.900
|
||||
He has been advising the National Association of the Deaf on proper parliamentary process during their meetings.
|
||||
|
||||
00:00:28.900 --> 00:00:30.970
|
||||
Now, we have a question for him.
|
||||
|
||||
00:00:30.970 --> 00:00:37.500
|
||||
For you specifically, this concerns the acceptance of minutes from meetings conducted by non-profit organizations.
|
||||
|
||||
00:00:37.500 --> 00:00:40.680
|
||||
What is the proper procedure for accepting meeting minutes?
|
||||
|
||||
00:00:43.020 --> 00:00:51.890
|
||||
When the president presents the minutes by asking the members if any corrections are needed?
|
||||
|
||||
00:00:53.910 --> 00:00:57.880
|
||||
If no corrections are necessary, then the minutes should be accepted as read.
|
||||
|
||||
00:00:57.880 --> 00:00:59.690
|
||||
That concludes this part of the process.
|
||||
|
||||
00:01:00.070 --> 00:01:07.590
|
||||
If, during the request for any necessary corrections, a member proposes a correction,
|
||||
|
||||
00:01:07.790 --> 00:01:12.250
|
||||
and no further corrections are needed,
|
||||
|
||||
00:01:12.250 --> 00:01:15.080
|
||||
then the minutes should be accepted with the corrections made.
|
||||
|
||||
00:01:15.080 --> 00:01:17.400
|
||||
That marks the end of this procedure.
|
||||
|
||||
00:01:17.400 --> 00:01:21.640
|
||||
It is not appropriate to propose a motion regarding the minutes since they as
|
||||
|
||||
00:01:21.640 --> 00:01:25.230
|
||||
records are deemed adopted because being approved is a prior action.
|
||||
|
||||
00:01:25.430 --> 00:01:30.500
|
||||
Proposing a motion would result in an unnecessary duplication of the acceptance of the minutes.
|
||||
|
||||
00:01:30.500 --> 00:01:31.950
|
||||
This would be procedurally incorrect.
|
||||
|
||||
00:01:31.950 --> 00:01:35.100
|
||||
Instead, simply invite members to propose any corrections that need to be made.
|
||||
|
||||
00:01:35.100 --> 00:01:40.030
|
||||
If no corrections are proposed, then the minutes should be accepted as they are presented.
|
||||
|
||||
00:01:40.030 --> 00:01:42.860
|
||||
If corrections are made, then the minutes should be accepted as amended.
|
||||
|
||||
00:01:42.860 --> 00:01:45.980
|
||||
This completes the steps for handling and filing the minutes.
|
||||
|
||||
00:01:46.370 --> 00:01:47.420
|
||||
Wow.
|
||||
|
||||
00:01:47.420 --> 00:01:49.320
|
||||
That was so straightforward.
|
||||
|
||||
00:01:49.320 --> 00:01:53.240
|
||||
Thank you, and we really appreciate your providing us with this clarification.
|
||||
|
||||
00:01:53.240 --> 00:01:54.160
|
||||
Thank you!
|
||||
|
||||
00:01:54.570 --> 00:01:55.590
|
||||
You're welcome.
|
||||
|
||||
23
public/transcriptions/minutes-approval-procedure.txt
Normal file
23
public/transcriptions/minutes-approval-procedure.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Minutes Approval Procedure - Expert Guidelines
|
||||
|
||||
In a warmly lit conference space, standing before large brown doors with vertical metal hinges and gold handles, Eliza Kragh introduces today's topic. With her short dark brown hair framing her face, wearing a beige jacket adorned with decorative studs over a black shirt and gold-toned earrings, she begins:
|
||||
|
||||
"Hello! This is Eliza Kragh from Montana. Today, I'm here with Mark Apodaca, who is an expert in parliamentary procedures. He has been advising the National Association of the Deaf on proper parliamentary process during their meetings."
|
||||
|
||||
Standing beside her is Mark Apodaca, distinguished by his bald head, white beard and mustache, wearing black-rimmed glasses and a light gray button-up shirt with blue jeans. His hands are clasped in front of him as he watches attentively.
|
||||
|
||||
Eliza continues, "Now, we have a question for him. For you specifically, this concerns the acceptance of minutes from meetings conducted by non-profit organizations. What is the proper procedure for accepting meeting minutes?"
|
||||
|
||||
Mark responds thoughtfully, explaining that when the president presents the minutes by asking the members if any corrections are needed, there are two possible scenarios:
|
||||
|
||||
First scenario: If no corrections are necessary, then the minutes should be accepted as read. That concludes this part of the process.
|
||||
|
||||
Second scenario: If, during the request for any necessary corrections, a member proposes a correction, and no further corrections are needed, then the minutes should be accepted with the corrections made. That marks the end of this procedure.
|
||||
|
||||
Mark emphasizes an important point: "It is not appropriate to propose a motion regarding the minutes since they as records are deemed adopted because being approved is a prior action. Proposing a motion would result in an unnecessary duplication of the acceptance of the minutes. This would be procedurally incorrect."
|
||||
|
||||
He concludes by summarizing: "Instead, simply invite members to propose any corrections that need to be made. If no corrections are proposed, then the minutes should be accepted as they are presented. If corrections are made, then the minutes should be accepted as amended. This completes the steps for handling and filing the minutes."
|
||||
|
||||
Eliza responds enthusiastically, "Wow. That was so straightforward. Thank you, and we really appreciate your providing us with this clarification."
|
||||
|
||||
Mark graciously replies, "You're welcome."
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
43
src/App.css
Normal file
43
src/App.css
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* Remove or update these constraints */
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
24
src/App.tsx
Normal file
24
src/App.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import Layout from './components/Layout/Layout'
|
||||
import Home from './pages/Home'
|
||||
import Services from './pages/Services'
|
||||
import About from './pages/About'
|
||||
import Contact from './pages/Contact'
|
||||
import Resources from './pages/Resources'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
export default App
|
||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
42
src/components/Layout/Footer.tsx
Normal file
42
src/components/Layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { FaFacebook } from 'react-icons/fa'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="bg-secondary text-accent-snow py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">
|
||||
DeafGain LLC
|
||||
</h3>
|
||||
<p className="text-accent-lake">
|
||||
Empowering the Deaf community<br />
|
||||
through innovation
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4 text-center">Quick Links</h4>
|
||||
<ul className="space-y-2 text-center">
|
||||
<li><a href="/services" className="hover:text-accent-lake">Services</a></li>
|
||||
<li><a href="/about" className="hover:text-accent-lake">About Us</a></li>
|
||||
<li><a href="/contact" className="hover:text-accent-lake">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4 text-center">Connect With Us</h4>
|
||||
<div className="flex justify-center">
|
||||
<a href="https://www.facebook.com/profile.php?id=61562623814859" target="_blank" rel="noopener noreferrer" className="hover:text-accent-lake">
|
||||
<FaFacebook size={24} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t border-accent-lake/20 text-center">
|
||||
<p>© {new Date().getFullYear()} <span className="text-yellow-400">DeafGain LLC.</span> All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
21
src/components/Layout/Layout.tsx
Normal file
21
src/components/Layout/Layout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ReactNode } from 'react'
|
||||
import Navbar from './Navbar'
|
||||
import Footer from './Footer'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Navbar />
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
77
src/components/Layout/Navbar.tsx
Normal file
77
src/components/Layout/Navbar.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { HiMenu, HiX } from 'react-icons/hi'
|
||||
|
||||
const Navbar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Home', path: '/' },
|
||||
{ name: 'Services', path: '/services' },
|
||||
{ name: 'About', path: '/about' },
|
||||
{ name: 'Resources', path: '/resources' },
|
||||
{ name: 'Contact', path: '/contact' }
|
||||
]
|
||||
|
||||
return (
|
||||
<nav className="bg-primary text-accent-snow">
|
||||
<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">
|
||||
DeafGain
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-center space-x-8">
|
||||
{navItems.map((item) => (
|
||||
<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"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
67
src/components/SnowPeaks.tsx
Normal file
67
src/components/SnowPeaks.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
const SnowPeaks = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
|
||||
// Create static snowflakes
|
||||
const snowflakes = Array.from({ length: 100 }, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 3 + 1,
|
||||
}))
|
||||
|
||||
// Draw peaks and static snow
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Draw peaks
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, canvas.height)
|
||||
ctx.lineTo(canvas.width * 0.2, canvas.height * 0.3)
|
||||
ctx.lineTo(canvas.width * 0.4, canvas.height * 0.6)
|
||||
ctx.lineTo(canvas.width * 0.6, canvas.height * 0.2)
|
||||
ctx.lineTo(canvas.width * 0.8, canvas.height * 0.5)
|
||||
ctx.lineTo(canvas.width, canvas.height * 0.4)
|
||||
ctx.lineTo(canvas.width, canvas.height)
|
||||
ctx.fillStyle = '#F5F7F9'
|
||||
ctx.fill()
|
||||
|
||||
// Draw static snowflakes
|
||||
snowflakes.forEach(flake => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(flake.x, flake.y, flake.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = '#F5F7F9'
|
||||
ctx.fill()
|
||||
})
|
||||
}
|
||||
|
||||
render()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="relative h-48 overflow-hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1 }}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-primary"
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SnowPeaks
|
||||
224
src/components/VideoPlayer.tsx
Normal file
224
src/components/VideoPlayer.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { FaPlay, FaPause, FaExpand, FaBackward, FaForward, FaClosedCaptioning } from 'react-icons/fa'
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src: string
|
||||
poster: string
|
||||
vttSrc: string
|
||||
}
|
||||
|
||||
const VideoPlayer = ({ src, poster, vttSrc }: VideoPlayerProps) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [showControls, setShowControls] = useState(true)
|
||||
const [showCaptions, setShowCaptions] = useState(false)
|
||||
const [playbackRate, setPlaybackRate] = useState(1)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const playbackRates = [0.5, 1, 2, 3]
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const textTracks = video.textTracks[0]
|
||||
if (textTracks) {
|
||||
textTracks.mode = showCaptions ? 'showing' : 'hidden'
|
||||
}
|
||||
}, [showCaptions])
|
||||
|
||||
const hideControlsWithDelay = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (isPlaying) {
|
||||
setShowControls(false)
|
||||
}
|
||||
}, 3000)
|
||||
}, [isPlaying])
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
if (isPlaying) {
|
||||
videoRef.current.pause()
|
||||
setShowControls(true)
|
||||
} else {
|
||||
videoRef.current.play()
|
||||
hideControlsWithDelay()
|
||||
}
|
||||
setIsPlaying(!isPlaying)
|
||||
}
|
||||
}, [isPlaying, hideControlsWithDelay])
|
||||
|
||||
const handleTimeSkip = (seconds: number) => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime += seconds
|
||||
}
|
||||
}
|
||||
|
||||
const handleFullScreen = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.requestFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
togglePlay()
|
||||
}
|
||||
if (e.code === 'ArrowLeft') {
|
||||
handleTimeSkip(-5)
|
||||
}
|
||||
if (e.code === 'ArrowRight') {
|
||||
handleTimeSkip(5)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (video.duration) {
|
||||
setProgress((video.currentTime / video.duration) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress)
|
||||
video.addEventListener('timeupdate', handleTimeUpdate)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress)
|
||||
video.removeEventListener('timeupdate', handleTimeUpdate)
|
||||
}
|
||||
}, [togglePlay])
|
||||
|
||||
const handleMouseMove = () => {
|
||||
setShowControls(true)
|
||||
hideControlsWithDelay()
|
||||
}
|
||||
|
||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const bounds = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - bounds.left
|
||||
const percentage = (x / bounds.width) * 100
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = (percentage / 100) * videoRef.current.duration
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative bg-black rounded-lg overflow-hidden"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={() => isPlaying && setShowControls(false)}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
poster={poster}
|
||||
controls={false}
|
||||
disablePictureInPicture
|
||||
className="w-full aspect-video cursor-pointer"
|
||||
onClick={togglePlay}
|
||||
>
|
||||
<source src={src} type="video/mp4" />
|
||||
<track
|
||||
src={vttSrc}
|
||||
kind="subtitles"
|
||||
srcLang="en"
|
||||
label="English"
|
||||
/>
|
||||
</video>
|
||||
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 transition-opacity duration-300 ${
|
||||
showControls ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="absolute -top-4 left-0 right-0 h-8 cursor-pointer flex items-center"
|
||||
onClick={handleProgressClick}
|
||||
>
|
||||
<div className="w-full h-2 bg-gray-600">
|
||||
<div
|
||||
className="h-full bg-[#4aa63f] transition-all duration-100"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-t from-black/90 to-transparent p-4">
|
||||
<div className="flex items-center justify-between text-white">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="bg-black text-white hover:text-accent-lake transition-colors p-2 rounded"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ?
|
||||
<FaPause title="Pause" className="w-4 h-4" /> :
|
||||
<FaPlay title="Play" className="w-4 h-4" />
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTimeSkip(-5)}
|
||||
className="bg-black text-white hover:text-accent-lake transition-colors p-2 rounded"
|
||||
aria-label="Rewind 5 seconds"
|
||||
>
|
||||
<FaBackward title="Rewind" className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTimeSkip(5)}
|
||||
className="bg-black text-white hover:text-accent-lake transition-colors p-2 rounded"
|
||||
aria-label="Forward 5 seconds"
|
||||
>
|
||||
<FaForward title="Forward" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<select
|
||||
value={playbackRate}
|
||||
onChange={(e) => {
|
||||
setPlaybackRate(Number(e.target.value))
|
||||
if (videoRef.current) {
|
||||
videoRef.current.playbackRate = Number(e.target.value)
|
||||
}
|
||||
}}
|
||||
className="bg-black text-white hover:text-accent-lake transition-colors p-2 rounded"
|
||||
aria-label="Playback Speed"
|
||||
>
|
||||
{playbackRates.map(rate => (
|
||||
<option key={rate} value={rate} className="text-black">
|
||||
{rate}x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => setShowCaptions(!showCaptions)}
|
||||
className={`bg-black p-2 rounded ${showCaptions ? 'text-[#4aa63f] hover:text-[#4aa63f]' : 'text-gray-600 hover:text-accent-lake'}`}
|
||||
aria-label="Toggle Captions"
|
||||
>
|
||||
<FaClosedCaptioning title="Captions" className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleFullScreen}
|
||||
className="bg-black text-white hover:text-accent-lake transition-colors p-2 rounded"
|
||||
aria-label="Full Screen"
|
||||
>
|
||||
<FaExpand title="Full Screen" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoPlayer
|
||||
47
src/index.css
Normal file
47
src/index.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--primary: #647C90;
|
||||
--secondary: #2C4A3E;
|
||||
--accent-snow: #F5F7F9;
|
||||
--accent-mountain: #4A5568;
|
||||
--accent-lake: #A4C3D2;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--accent-snow);
|
||||
color: var(--accent-mountain);
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
125
src/pages/About.tsx
Normal file
125
src/pages/About.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { FaGraduationCap, FaAward, FaHandsHelping } from 'react-icons/fa'
|
||||
import SnowPeaks from '../components/SnowPeaks'
|
||||
|
||||
const About = () => {
|
||||
const [ref, inView] = useInView({
|
||||
triggerOnce: true,
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
const elizaAchievements = [
|
||||
{
|
||||
icon: <FaGraduationCap />,
|
||||
title: "Education",
|
||||
details: "Master's in Human Resource Development - Rochester Institute of Technology"
|
||||
},
|
||||
{
|
||||
icon: <FaAward />,
|
||||
title: "Certifications",
|
||||
details: "Certified ADA Coordinator, Member of National Association of Parliamentarians"
|
||||
},
|
||||
{
|
||||
icon: <FaHandsHelping />,
|
||||
title: "Leadership",
|
||||
details: "President of Missoula Council of the Deaf, Founder of DeafGain LLC"
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<section className="relative py-20 bg-primary text-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center"
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
The Saga of DeafGain
|
||||
</h1>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dynamic Snow Peaks */}
|
||||
<SnowPeaks />
|
||||
|
||||
{/* Team Members Section */}
|
||||
<section className="py-20 bg-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{/* Eliza's Section */}
|
||||
<div>
|
||||
<div className="grid md:grid-cols-2 gap-12 items-start">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-secondary mb-6 text-center">
|
||||
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" />
|
||||
</div>
|
||||
<motion.div>
|
||||
<p className="text-accent-mountain">
|
||||
A proud Montana native, Eliza brings her passion for service and innovation to every aspect of DeafGain. Her journey reflects a deep commitment to transforming accessibility standards and empowering others. When not pioneering new solutions, you'll find her exploring Montana's breathtaking landscapes or engaging with local Deaf communities.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.div className="grid grid-cols-1 gap-6">
|
||||
{elizaAchievements.map((item, index) => (
|
||||
<div key={index} className="bg-white p-6 rounded-lg shadow-lg">
|
||||
<div className="text-primary text-2xl mb-4">{item.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{item.title}</h3>
|
||||
<p className="text-accent-mountain">{item.details}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mountain Peaks Separator */}
|
||||
<div className="max-w-4xl mx-auto mb-20 py-12">
|
||||
<div className="relative h-32">
|
||||
<svg viewBox="0 0 1440 120" className="w-full">
|
||||
<defs>
|
||||
<linearGradient id="mountainGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#647C90" />
|
||||
<stop offset="50%" stopColor="#A4C3D2" />
|
||||
<stop offset="100%" stopColor="#2C4A3E" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="url(#mountainGradient)"
|
||||
strokeWidth="3"
|
||||
d="M0,100 L360,20 L720,90 L1080,30 L1440,80"
|
||||
className="path"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Vision Section */}
|
||||
<section className="py-20 bg-secondary text-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold mb-6">Our Vision</h2>
|
||||
<p className="text-xl max-w-3xl mx-auto">
|
||||
Creating a Deaf-designed nationwide hub promoting understanding, empowerment, and inclusivity. From coast to coast, we're building a nationwide ecosystem of accessibility and empowerment through strategic partnerships and innovative solutions.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
136
src/pages/Contact.tsx
Normal file
136
src/pages/Contact.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaEnvelope, FaMapMarkerAlt, FaClock } from 'react-icons/fa'
|
||||
|
||||
const Contact = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Form submission logic will be implemented here
|
||||
}
|
||||
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: <FaEnvelope />,
|
||||
title: 'Email',
|
||||
content: 'contact@deafgain.com'
|
||||
},
|
||||
{
|
||||
icon: <FaMapMarkerAlt />,
|
||||
title: 'Location',
|
||||
content: 'Montana, United States'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4 py-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
|
||||
<p className="text-accent-mountain text-lg">
|
||||
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">
|
||||
{contactInfo.map((info, 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 text-center"
|
||||
>
|
||||
<div className="text-primary text-3xl mb-4">{info.icon}</div>
|
||||
<h3 className="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"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="name" className="block text-accent-mountain mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="email" className="block text-accent-mountain mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
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"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="subject" className="block text-accent-mountain mb-2">
|
||||
Subject
|
||||
</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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="message" className="block text-accent-mountain mb-2">
|
||||
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"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
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"
|
||||
>
|
||||
Send Message
|
||||
</motion.button>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Contact
|
||||
65
src/pages/Home.tsx
Normal file
65
src/pages/Home.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const Home = () => {
|
||||
const navigate = useNavigate()
|
||||
const [ref, inView] = useInView({
|
||||
triggerOnce: true,
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
const handleLearnMore = () => {
|
||||
navigate('/services')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Hero Section */}
|
||||
<section className="relative h-screen">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src="/images/scene.jpg"
|
||||
alt="Serene mountain lake landscape"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-secondary/70 to-primary/70" />
|
||||
</div>
|
||||
|
||||
<div className="relative h-full flex items-center justify-center text-accent-snow">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center px-4"
|
||||
>
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-8 text-accent-snow">
|
||||
DeafGain LLC
|
||||
</h1>
|
||||
<p className="text-2xl md:text-3xl font-extrabold mb-8 bg-gradient-to-r from-accent-lake via-white to-accent-lake bg-clip-text text-transparent tracking-wider">
|
||||
"Language is a right, not a luxury."
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section> {/* Mission Statement Section */}
|
||||
<motion.section
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={inView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="py-20 bg-accent-snow"
|
||||
>
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl font-bold text-secondary mb-6">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</motion.section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
224
src/pages/Resources.tsx
Normal file
224
src/pages/Resources.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FaBookReader, FaTimes, FaVideo } from 'react-icons/fa'
|
||||
import VideoPlayer from '../components/VideoPlayer'
|
||||
|
||||
interface Video {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
thumbnail: string
|
||||
videoSrc: string
|
||||
vttSrc: string
|
||||
transcriptSrc?: string
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
videos: Video[]
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 6
|
||||
|
||||
const resourceCategories: Category[] = [
|
||||
{
|
||||
id: 'governance',
|
||||
title: 'Professional Development & Governance',
|
||||
description: 'Essential training materials for effective organizational management and parliamentary procedures',
|
||||
videos: [
|
||||
{
|
||||
id: 'minutes-approval-procedure',
|
||||
title: 'Meeting Minutes Approval: Expert Guidelines',
|
||||
description: 'Expert parliamentarian Mark Apodaca explains the proper procedures for reviewing and accepting meeting minutes in non-profit organizations',
|
||||
thumbnail: '/images/thumbnails/minutes-approval-procedure.png',
|
||||
videoSrc: '/videos/minutes-approval-procedure.mp4',
|
||||
vttSrc: '/subtitles/minutes-approval-procedure.vtt',
|
||||
transcriptSrc: '/transcriptions/minutes-approval-procedure.txt'
|
||||
},
|
||||
{
|
||||
id: 'gov2',
|
||||
title: 'Effective Board Meeting Management',
|
||||
description: 'Best practices for conducting productive and inclusive board meetings',
|
||||
thumbnail: '/images/thumbnails/board-meetings.jpg',
|
||||
videoSrc: '/videos/board-meetings.mp4',
|
||||
vttSrc: '/subtitles/board-meetings.vtt'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const Resources = () => {
|
||||
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null)
|
||||
const [currentPage, setCurrentPage] = useState<Record<string, number>>({})
|
||||
const [showTranscript, setShowTranscript] = useState(false)
|
||||
const [transcriptContent, setTranscriptContent] = useState('')
|
||||
const popupRef = useRef<HTMLDivElement>(null)
|
||||
const transcriptRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
|
||||
setSelectedVideo(null)
|
||||
}
|
||||
if (transcriptRef.current && !transcriptRef.current.contains(event.target as Node)) {
|
||||
setShowTranscript(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const fetchTranscript = async (videoId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/transcriptions/${videoId}.txt`)
|
||||
const text = await response.text()
|
||||
setTranscriptContent(text)
|
||||
setShowTranscript(true)
|
||||
} catch (error) {
|
||||
console.error('Error fetching transcript:', error)
|
||||
setTranscriptContent('Transcript not available.')
|
||||
}
|
||||
}
|
||||
|
||||
const getPageCount = (videoCount: number) => Math.ceil(videoCount / ITEMS_PER_PAGE)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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">
|
||||
Resource Library
|
||||
</h1>
|
||||
|
||||
{resourceCategories.map((category) => (
|
||||
<div key={category.id} className="mb-20">
|
||||
<div className="mb-8">
|
||||
<h2 className="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">
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<FaVideo /> Watch Video
|
||||
</button>
|
||||
{video.transcriptSrc && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<FaBookReader /> Read Transcript
|
||||
</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>
|
||||
|
||||
{getPageCount(category.videos.length) > 1 && (
|
||||
<div className="mt-8 flex justify-center items-center space-x-4">
|
||||
{Array.from({ length: getPageCount(category.videos.length) }).map((_, idx) => (
|
||||
<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
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-white text-primary border border-primary'
|
||||
}`}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</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="relative">
|
||||
<button
|
||||
onClick={() => setSelectedVideo(null)}
|
||||
className="absolute -top-12 right-0 text-white hover:text-accent-lake bg-black/50 p-2 rounded-full transition-colors"
|
||||
>
|
||||
<FaTimes className="w-6 h-6" />
|
||||
</button>
|
||||
<VideoPlayer
|
||||
src={selectedVideo.videoSrc}
|
||||
poster={selectedVideo.thumbnail}
|
||||
vttSrc={selectedVideo.vttSrc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transcript Modal */}
|
||||
<AnimatePresence>
|
||||
{showTranscript && (
|
||||
<motion.div
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
ref={transcriptRef}
|
||||
>
|
||||
<div className="p-6 flex justify-between items-center border-b border-gray-200">
|
||||
<h3 className="text-2xl font-bold text-secondary">Video Transcript</h3>
|
||||
<button
|
||||
onClick={() => setShowTranscript(false)}
|
||||
className="text-accent-mountain hover:text-secondary transition-colors bg-accent-snow/50 p-2 rounded-full"
|
||||
>
|
||||
<FaTimes size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 max-h-[70vh] 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}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Resources
|
||||
323
src/pages/Services.tsx
Normal file
323
src/pages/Services.tsx
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
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'
|
||||
|
||||
const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"
|
||||
|
||||
const relationshipPoints = [
|
||||
{
|
||||
coordinates: [-106.6504, 35.0844],
|
||||
city: "Albuquerque",
|
||||
state: "New Mexico",
|
||||
events: [{
|
||||
name: "NLTC",
|
||||
description: "National Leadership Training Conference - NAD",
|
||||
startDate: "September 21, 2023",
|
||||
endDate: "September 24, 2023"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-80.8431, 35.2271],
|
||||
city: "Charlotte",
|
||||
state: "North Carolina",
|
||||
events: [{
|
||||
name: "SERID",
|
||||
description: "Southeast Regional Institute on Deafness Conference",
|
||||
startDate: "October 26, 2023",
|
||||
endDate: "October 29, 2023"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-104.9903, 39.7392],
|
||||
city: "Denver",
|
||||
state: "Colorado",
|
||||
events: [
|
||||
{
|
||||
name: "DBL",
|
||||
description: "Deaf, Blind, Limitless Conference",
|
||||
startDate: "March 12, 2024",
|
||||
endDate: "March 15, 2024"
|
||||
},
|
||||
{
|
||||
name: "EHDI",
|
||||
description: "Early Hearing Detection Intervention Conference",
|
||||
startDate: "March 17, 2024",
|
||||
endDate: "March 19, 2024"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
coordinates: [-84.3879, 33.7490],
|
||||
city: "Atlanta",
|
||||
state: "Georgia",
|
||||
events: [{
|
||||
name: "ADARA",
|
||||
description: "American Deafness and Rehabilitation Association Conference",
|
||||
startDate: "May 28, 2024",
|
||||
endDate: "June 1, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-93.2650, 44.9778],
|
||||
city: "Minneapolis",
|
||||
state: "Minnesota",
|
||||
events: [{
|
||||
name: "ADA",
|
||||
description: "Americans with Disabilities Act Conference",
|
||||
startDate: "June 9, 2024",
|
||||
endDate: "June 12, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-87.6298, 41.8781],
|
||||
city: "Chicago",
|
||||
state: "Illinois",
|
||||
events: [{
|
||||
name: "NAD",
|
||||
description: "National Association of the Deaf Conference",
|
||||
startDate: "July 1, 2024",
|
||||
endDate: "July 6, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-98.4936, 29.4241],
|
||||
city: "San Antonio",
|
||||
state: "Texas",
|
||||
events: [{
|
||||
name: "NAP NTC",
|
||||
description: "National Association of Parliament - National Training Conference",
|
||||
startDate: "September 5, 2024",
|
||||
endDate: "September 8, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-89.4012, 43.0731],
|
||||
city: "Madison",
|
||||
state: "Wisconsin",
|
||||
events: [{
|
||||
name: "ADA",
|
||||
description: "Americans with Disabilities Act Conference",
|
||||
startDate: "October 21, 2024",
|
||||
endDate: "October 22, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-94.8191, 38.8814],
|
||||
city: "Olathe",
|
||||
state: "Kansas",
|
||||
events: [{
|
||||
name: "KSD",
|
||||
description: "Kansas School for the Deaf",
|
||||
startDate: "November 23, 2024",
|
||||
endDate: "November 23, 2024"
|
||||
}]
|
||||
},
|
||||
{
|
||||
coordinates: [-115.1398, 36.1699],
|
||||
city: "Las Vegas",
|
||||
state: "Nevada",
|
||||
events: [{
|
||||
name: "Engagement Conference",
|
||||
description: "2024 Engagement Conference",
|
||||
startDate: "December 15, 2024",
|
||||
endDate: "December 15, 2024"
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
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."
|
||||
}
|
||||
]
|
||||
|
||||
const Services = () => {
|
||||
const [selectedPoint, setSelectedPoint] = useState<typeof relationshipPoints[0] | null>(null)
|
||||
const popupRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isUpcoming = (event: { startDate: string, endDate: string }) => {
|
||||
const eventEndDate = new Date(event.endDate)
|
||||
const now = new Date()
|
||||
// Set the end date to the end of the day
|
||||
eventEndDate.setHours(23, 59, 59, 999)
|
||||
return eventEndDate > now
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
|
||||
setSelectedPoint(null)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<section className="py-20 bg-accent-snow">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<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">
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="text-3xl font-bold text-center text-secondary mb-12">
|
||||
Our Network & Relationships
|
||||
</h2>
|
||||
<div className="relative h-[820px]">
|
||||
<ComposableMap projection="geoAlbersUsa">
|
||||
<Geographies geography={geoUrl}>
|
||||
{({ geographies }) =>
|
||||
geographies.map(geo => (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill="#647C90"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth={0.5}
|
||||
style={{
|
||||
default: { fill: "#647C90", outline: "none" },
|
||||
hover: { fill: "#2C4A3E", outline: "none" },
|
||||
pressed: { fill: "#A4C3D2", outline: "none" }
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Geographies>
|
||||
|
||||
{relationshipPoints.map((point) => {
|
||||
const hasUpcomingEvent = point.events.some(event => isUpcoming(event))
|
||||
return (
|
||||
<Marker key={`${point.city}-${point.state}`} coordinates={point.coordinates}>
|
||||
<motion.circle
|
||||
r={6}
|
||||
fill={hasUpcomingEvent ? "#4aa63f" : "#A4C3D2"}
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth={2}
|
||||
className="cursor-pointer"
|
||||
whileHover={{ scale: 1.5 }}
|
||||
onClick={() => setSelectedPoint(point)}
|
||||
/>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
</ComposableMap>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedPoint && (
|
||||
<motion.div
|
||||
ref={popupRef}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
backdrop-blur-lg bg-white/80 p-8 rounded-2xl shadow-2xl
|
||||
border border-white/20 w-96
|
||||
hover:shadow-accent-lake/20 hover:shadow-2xl
|
||||
transition-all duration-300"
|
||||
style={{
|
||||
background: 'linear-gradient(145deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.8) 100%)',
|
||||
}}
|
||||
>
|
||||
<motion.button
|
||||
whileHover={{ rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={() => setSelectedPoint(null)}
|
||||
className="absolute top-4 right-4 text-accent-mountain hover:text-secondary
|
||||
bg-white/50 p-2 rounded-full"
|
||||
>
|
||||
<FaTimes />
|
||||
</motion.button>
|
||||
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-secondary mb-2 flex items-center gap-2">
|
||||
<FaMapMarkerAlt className="text-primary" />
|
||||
{selectedPoint.city}, {selectedPoint.state}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
{selectedPoint.events
|
||||
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
|
||||
.map((event, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className={`
|
||||
relative overflow-hidden rounded-xl
|
||||
${index !== 0 ? "mt-4" : ""}
|
||||
bg-gradient-to-r from-white/90 to-white/80
|
||||
p-4 hover:from-white/95 hover:to-white/85
|
||||
transition-all duration-300
|
||||
border border-white/30
|
||||
`}
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<h4 className="font-bold text-secondary text-lg mb-1">{event.name}</h4>
|
||||
<p className="text-accent-mountain text-sm mb-2 font-medium">{event.description}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-primary font-semibold">
|
||||
<FaClock className="text-xs" />
|
||||
{event.startDate === event.endDate
|
||||
? event.startDate
|
||||
: `${event.startDate} - ${event.endDate}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-secondary/5" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Services
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
21
tailwind.config.js
Normal file
21
tailwind.config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'primary': '#647C90', // Slate blue
|
||||
'secondary': '#2C4A3E', // Forest green
|
||||
'accent': {
|
||||
'snow': '#F5F7F9', // Snow white
|
||||
'mountain': '#4A5568', // Mountain gray
|
||||
'lake': '#A4C3D2', // Alpine lake blue
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['react-icons']
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue