Initial commit
This commit is contained in:
commit
0fb8045def
47 changed files with 6679 additions and 0 deletions
9
.env
Normal file
9
.env
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
GOOGLE_EMAIL=system@sigd.net
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
RECIPIENT_EMAIL=chris@sigd.net
|
||||||
|
GOOGLE_APP_PASSWORD=fvjivgqewtmtpdez
|
||||||
|
UPSTASH_REDIS_REST_URL=https://gusc1-resolved-jaybird-30779.upstash.io
|
||||||
|
UPSTASH_REDIS_REST_TOKEN=AXg7ASQgNDMzMDE0MTYtNWE0Ni00OTc3LThhYjktM2IxMzMxNGMyMzMxNWRiNGMyOGU2MjE0NGUyNTkwMTQyNmUxZTU2NzE4NDI=
|
||||||
|
|
||||||
9
.env.local
Normal file
9
.env.local
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
GOOGLE_EMAIL=system@sigd.net
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
RECIPIENT_EMAIL=chris@sigd.net
|
||||||
|
GOOGLE_APP_PASSWORD=fvjivgqewtmtpdez
|
||||||
|
UPSTASH_REDIS_REST_URL=https://gusc1-resolved-jaybird-30779.upstash.io
|
||||||
|
UPSTASH_REDIS_REST_TOKEN=AXg7ASQgNDMzMDE0MTYtNWE0Ni00OTc3LThhYjktM2IxMzMxNGMyMzMxNWRiNGMyOGU2MjE0NGUyNTkwMTQyNmUxZTU2NzE4NDI=
|
||||||
|
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Explicitly run PostCSS/Tailwind build before Next.js build
|
||||||
|
RUN pnpm dlx tailwindcss -i ./src/styles/globals.css -o ./src/styles/output.css
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=803
|
||||||
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder /app/src/styles/output.css ./src/styles/output.css
|
||||||
|
COPY .env.local .env.local
|
||||||
|
|
||||||
|
EXPOSE 803
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
19
Dockerfile.dev
Normal file
19
Dockerfile.dev
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
ENV DEBUG=next:build
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["pnpm", "dev"]
|
||||||
0
ROADMAP
Normal file
0
ROADMAP
Normal file
23
docker-compose.dev.yml
Normal file
23
docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
services:
|
||||||
|
web-dev:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- web-dev-node-modules:/app/node_modules
|
||||||
|
- web-dev-next:/app/.next
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- WATCHPACK_POLLING=true
|
||||||
|
- CHOKIDAR_USEPOLLING=true
|
||||||
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
env_file:
|
||||||
|
- .env.local
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
web-dev-node-modules:
|
||||||
|
web-dev-next:
|
||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
pull_policy: build
|
||||||
|
ports:
|
||||||
|
- "803:803"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=803
|
||||||
|
- GOOGLE_EMAIL=${GOOGLE_EMAIL}
|
||||||
|
- SMTP_HOST=${SMTP_HOST}
|
||||||
|
- SMTP_PORT=${SMTP_PORT}
|
||||||
|
- SMTP_SECURE=${SMTP_SECURE}
|
||||||
|
- RECIPIENT_EMAIL=${RECIPIENT_EMAIL}
|
||||||
|
- GOOGLE_APP_PASSWORD=${GOOGLE_APP_PASSWORD}
|
||||||
|
- UPSTASH_REDIS_REST_URL=${UPSTASH_REDIS_REST_URL}
|
||||||
|
- UPSTASH_REDIS_REST_TOKEN=${UPSTASH_REDIS_REST_TOKEN}
|
||||||
|
env_file:
|
||||||
|
- .env.local
|
||||||
|
restart: unless-stopped
|
||||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
14
next.config.js
Normal file
14
next.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
serverActions: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
40
package.json
Normal file
40
package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "chris-haulmark-website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@upstash/redis": "^1.34.3",
|
||||||
|
"framer-motion": "^11.12.0",
|
||||||
|
"lucide-react": "^0.462.0",
|
||||||
|
"next": "^14.1.0",
|
||||||
|
"nodemailer": "^6.9.16",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-icons": "^5.3.0",
|
||||||
|
"react-simple-maps": "^3.0.0",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
|
"xss": "^1.0.14",
|
||||||
|
"validator": "^13.9.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.1",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@types/react-simple-maps": "^3.0.6",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
|
"@types/topojson-specification": "^1.0.4",
|
||||||
|
"@types/validator": "^13.7.17",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
|
||||||
|
}
|
||||||
1547
pnpm-lock.yaml
generated
Normal file
1547
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
10
postcss.config.js
Normal file
10
postcss.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'tailwindcss': {},
|
||||||
|
'autoprefixer': {
|
||||||
|
flexbox: true,
|
||||||
|
grid: true,
|
||||||
|
overrideBrowserslist: ['last 2 versions', '> 2%']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
public/images/hero-image.jpg
Normal file
BIN
public/images/hero-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 438 KiB |
116
src/app/api/contact/route.ts
Normal file
116
src/app/api/contact/route.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
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, '') // Basic XSS prevention
|
||||||
|
.slice(0, 1000) // Length limitation
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
// Rate limiting check
|
||||||
|
const rateLimitResult = await rateLimit()
|
||||||
|
if (!rateLimitResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Too many requests. Please try again later.' },
|
||||||
|
{ status: 429 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: FormData = await req.json()
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
const validation = validateInput(data)
|
||||||
|
if (!validation.isValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: validation.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize inputs
|
||||||
|
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 NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email sent successfully'
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending email:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Failed to send email' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/app/biography/page.tsx
Normal file
102
src/app/biography/page.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import RegionMap from '@/components/biography/RegionMap'
|
||||||
|
import JourneyVisual from '@/components/biography/JourneyVisual'
|
||||||
|
|
||||||
|
export default function Biography() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const yearsOfAdvocacy = currentYear - 2010
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="container mx-auto px-4 py-16"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-4xl md:text-5xl font-bold mb-8 bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent"
|
||||||
|
initial={{ y: -20 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
>
|
||||||
|
A Journey of Advocacy and Leadership
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<div className="prose prose-lg max-w-none">
|
||||||
|
<motion.div
|
||||||
|
className="space-y-6"
|
||||||
|
initial={{ y: 20 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
>
|
||||||
|
<p className="text-xl font-medium text-gray-700 leading-relaxed">
|
||||||
|
My life's journey has been one of profound discovery and purposeful transformation, shaped by experiences that have forged me into an unwavering advocate for the Deaf community.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-6 rounded-lg border-l-4 border-blue-600 my-8">
|
||||||
|
<p className="italic text-gray-700">
|
||||||
|
"When you daydream, you envision your authentic self. I've learned to use these visions as blueprints for building the person I want to become."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="my-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Early Foundations</h2>
|
||||||
|
<p>
|
||||||
|
Growing up in Arkansas's river valley, I became Deaf at age one. My early years were uniquely shaped by being the only Deaf person in my family, navigating the hearing world while developing my identity. It wasn't until my teenage years that I encountered Deaf adults who would profoundly influence my understanding of Deaf culture and community.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Professional Evolution</h2>
|
||||||
|
<p>
|
||||||
|
My professional journey began in 1998 in the technology sector. Starting as a PC technician, I ascended through the ranks to become an IT manager for a government agency by 2009. This decade-long experience in technology equipped me with valuable skills in problem-solving and systematic thinking that I now apply to advocacy.
|
||||||
|
</p>
|
||||||
|
<div className="my-8">
|
||||||
|
<JourneyVisual />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Global Awakening</h2>
|
||||||
|
<p>
|
||||||
|
In 2015, I embarked on a transformative journey that would reshape my understanding of Deaf culture and identity. Over two years, I traversed 47 countries, living with Deaf families and immersing myself in their unique sign languages and cultures. This wasn't merely travel – it was a profound exploration of global Deaf perspectives that transformed my approach to advocacy.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
It was during this journey, sitting on a cliff in the Himalayas, that I experienced a profound realization: for the first time, I felt truly proud of myself, not for others' approval, but for my own growth and authenticity.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Leadership in Action</h2>
|
||||||
|
<p>
|
||||||
|
My dedication to advocacy has culminated in significant leadership roles that span from state to national levels. Since 2021, I've served the Kansas Association of the Deaf (KAD), first as an appointed board member and now as Vice President since 2023. In July 2024, I embraced a new challenge as Region II Board Representative for the National Association of the Deaf (NAD), where I'm developing strategies to champion the interests of Deaf communities across the Midwest heartland - a vibrant mosaic of eleven states stretching from the Great Lakes to the Great Plains.
|
||||||
|
</p>
|
||||||
|
<div className="my-8">
|
||||||
|
<RegionMap />
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">
|
||||||
|
While I'm a seasoned advocate within KAD, my role with NAD represents an exciting new chapter in my leadership journey, allowing me to expand my impact to a national scale while continuing to serve my state community.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Breaking Communication Barriers</h2>
|
||||||
|
<p>
|
||||||
|
For the past {yearsOfAdvocacy} years, I've dedicated myself to dismantling communication barriers within the Deaf community. My work involves advocating for those who cannot advocate for themselves and educating the broader community about Deaf history, arts, and culture. This mission is deeply personal – it's about creating the change I wish I had seen in my early years.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-12 bg-gradient-to-r from-blue-50 to-purple-50 p-8 rounded-xl">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Building Tomorrow's Leaders</h2>
|
||||||
|
<p>
|
||||||
|
Today, I focus on nurturing and strengthening leadership within the Deaf community. Through mentorship, workshops, and advocacy training, I'm committed to developing the next generation of Deaf leaders. Like planting a tree whose shade will benefit future generations, I'm building a legacy of empowerment and leadership that will continue to grow and create positive change long into the future.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
54
src/app/layout.tsx
Normal file
54
src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import '@/styles/globals.css'
|
||||||
|
import React from 'react'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { NotificationProvider } from '@/components/providers/notification-provider'
|
||||||
|
import Layout from '@/components/layout/Layout'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Chris Haulmark - Deaf Advocacy & Leadership',
|
||||||
|
description: 'Empowering the Deaf community through advocacy, leadership, and cultural bridge-building.',
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
statusBarStyle: 'default',
|
||||||
|
title: 'Chris Haulmark',
|
||||||
|
},
|
||||||
|
formatDetection: {
|
||||||
|
telephone: true,
|
||||||
|
date: true,
|
||||||
|
address: true,
|
||||||
|
email: true,
|
||||||
|
url: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false,
|
||||||
|
viewportFit: 'cover',
|
||||||
|
themeColor: '#ffffff',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="h-full antialiased">
|
||||||
|
<head>
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
</head>
|
||||||
|
<body className="flex min-h-full flex-col bg-white">
|
||||||
|
<NotificationProvider>
|
||||||
|
<Layout>
|
||||||
|
{children}
|
||||||
|
</Layout>
|
||||||
|
</NotificationProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/app/page.tsx
Normal file
16
src/app/page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import Hero from '@/components/home/Hero'
|
||||||
|
import ServiceGrid from '@/components/home/ServiceGrid'
|
||||||
|
import Timeline from '@/components/home/Timeline'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col min-h-screen">
|
||||||
|
<Hero />
|
||||||
|
<ServiceGrid />
|
||||||
|
<Timeline />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
216
src/app/services/asl-bridging/page.tsx
Normal file
216
src/app/services/asl-bridging/page.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { BsTranslate, BsFileText } from 'react-icons/bs'
|
||||||
|
import { FaSignLanguage, FaClosedCaptioning, FaChalkboardTeacher } from 'react-icons/fa'
|
||||||
|
import { MdAccessibility, MdOutlineGroups } from 'react-icons/md'
|
||||||
|
|
||||||
|
const offerings = [
|
||||||
|
{
|
||||||
|
icon: <BsTranslate className="w-8 h-8" />,
|
||||||
|
title: "Translation Services",
|
||||||
|
description: "I provide precise translations from ASL to English, capturing the nuances and depth of this visual language, and converting it into comprehensible English text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BsFileText className="w-8 h-8" />,
|
||||||
|
title: "Content Transformation",
|
||||||
|
description: "I transform ASL content, whether video-based or in-person, into written English narratives or scripts, bridging the communication gap between hearing and Deaf communities"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaClosedCaptioning className="w-8 h-8" />,
|
||||||
|
title: "Video Captions",
|
||||||
|
description: "I create accurate, easy-to-follow captions for ASL videos, enhancing accessibility for viewers who may not understand ASL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaChalkboardTeacher className="w-8 h-8" />,
|
||||||
|
title: "Training & Workshops",
|
||||||
|
description: "I organize workshops emphasizing the intricacies of converting ASL expressions into English – valuable for interpreters, educators, and those working with the Deaf community"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const impactAreas = [
|
||||||
|
{
|
||||||
|
icon: <MdAccessibility className="w-12 h-12" />,
|
||||||
|
title: "Communication Accessibility",
|
||||||
|
description: "I offer consultations on making information more accessible to Deaf individuals through ASL conversion and enriched English translations",
|
||||||
|
gradient: "from-blue-400 to-blue-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MdOutlineGroups className="w-12 h-12" />,
|
||||||
|
title: "Community Advocacy",
|
||||||
|
description: "As a Deaf advocate, I highlight the importance of accurate ASL-to-English translation and captioning in fostering understanding between communities",
|
||||||
|
gradient: "from-blue-600 to-mcdi-maroon"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const ServiceNavigation = () => {
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/services/asl-bridging')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<Link href={prevService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href={nextService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ASLBridging() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<FaSignLanguage className="w-16 h-16 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
ASL-to-English Linguistic Bridging
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
As an experienced Deaf professional, I specialize in accurately translating the richness and
|
||||||
|
diversity of ASL expressions into English. My personal background and expertise enable me to
|
||||||
|
ensure seamless communication while preserving the authentic voice of ASL.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||||
|
{offerings.map((offering, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={offering.title}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 + 0.4 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-blue-600 mb-4">
|
||||||
|
{offering.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">{offering.title}</h3>
|
||||||
|
<p className="text-gray-600">{offering.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-mcdi-maroon rounded-2xl p-8 text-white max-w-4xl mx-auto mb-16"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">My Commitment</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Drawing from my personal journey as a Deaf individual and my extensive experience in advocacy,
|
||||||
|
I am dedicated to bridging the linguistic gap between ASL and English. My goal is to foster
|
||||||
|
genuine understanding and respect between Deaf and hearing communities through accurate,
|
||||||
|
culturally-sensitive translations.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{impactAreas.map((area, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={area.title}
|
||||||
|
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 + index * 0.2 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className={`absolute h-2 w-full bg-gradient-to-r ${area.gradient} top-0`} />
|
||||||
|
<div className="p-8 bg-white">
|
||||||
|
<div className={`bg-gradient-to-r ${area.gradient} text-white p-4 rounded-full w-20 h-20 flex items-center justify-center mb-6`}>
|
||||||
|
{area.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">{area.title}</h3>
|
||||||
|
<p className="text-gray-600">{area.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
222
src/app/services/deafhood/page.tsx
Normal file
222
src/app/services/deafhood/page.tsx
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { FaChalkboardTeacher, FaUserGraduate, FaHandsHelping } from 'react-icons/fa'
|
||||||
|
import { GiPublicSpeaker } from 'react-icons/gi'
|
||||||
|
import { BiHistory, BiWorld } from 'react-icons/bi'
|
||||||
|
import { RiCommunityLine } from 'react-icons/ri'
|
||||||
|
|
||||||
|
const offerings = [
|
||||||
|
{
|
||||||
|
icon: <GiPublicSpeaker className="w-8 h-8" />,
|
||||||
|
title: "Public Speaking Engagements",
|
||||||
|
description: "I deliver enlightening presentations on Deaf history, culture, and empowerment, drawing from my personal journey and extensive global experience"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaChalkboardTeacher className="w-8 h-8" />,
|
||||||
|
title: "Deafhood Advocacy Workshops",
|
||||||
|
description: "I facilitate transformative workshops exploring Deafhood philosophy, cultural pride, and identity development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaUserGraduate className="w-8 h-8" />,
|
||||||
|
title: "Role Model Visits",
|
||||||
|
description: "I visit classrooms and institutions as a Deaf role model, empowering Deaf students and educating hearing students about Deaf culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <RiCommunityLine className="w-8 h-8" />,
|
||||||
|
title: "Community Building",
|
||||||
|
description: "I actively work to strengthen connections within Deaf spaces, drawing from my experience visiting Deaf communities across 54 countries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const impactAreas = [
|
||||||
|
{
|
||||||
|
icon: <BiHistory className="w-12 h-12" />,
|
||||||
|
title: "Cultural Heritage",
|
||||||
|
description: "I share insights about the rich history of Deaf culture, drawing from my extensive research and personal experiences worldwide",
|
||||||
|
gradient: "from-mcdi-maroon to-red-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaHandsHelping className="w-12 h-12" />,
|
||||||
|
title: "Advocacy Leadership",
|
||||||
|
description: "I bring extensive leadership experience from state and national Deaf organizations to strengthen advocacy initiatives",
|
||||||
|
gradient: "from-mcdi-maroon to-red-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BiWorld className="w-12 h-12" />,
|
||||||
|
title: "Global Perspective",
|
||||||
|
description: "I bring unique insights from my two-year journey across 54 countries, living with Deaf families and experiencing diverse sign languages",
|
||||||
|
gradient: "from-mcdi-maroon to-red-600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function ServiceNavigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/services/deafhood')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(prevService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(nextService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Deafhood() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-red-50 via-white to-red-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<RiCommunityLine className="w-16 h-16 text-mcdi-maroon" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-mcdi-maroon to-red-600 bg-clip-text text-transparent">
|
||||||
|
Deafhood Representation & Education
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
As a dedicated Deaf advocate with global experience, I provide comprehensive Deafhood representation services,
|
||||||
|
sharing authentic perspectives and fostering understanding through personal experience and expertise.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||||
|
{offerings.map((offering, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={offering.title}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 + 0.4 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-mcdi-maroon mb-4">
|
||||||
|
{offering.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">{offering.title}</h3>
|
||||||
|
<p className="text-gray-600">{offering.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-mcdi-maroon to-red-600 rounded-2xl p-8 text-white max-w-4xl mx-auto mb-16"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">My Commitment to You</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Drawing from my journey as a Deaf individual and my extensive global experiences,
|
||||||
|
I am dedicated to sharing authentic Deaf perspectives and fostering understanding.
|
||||||
|
My role as Vice President of KAD and NAD Region II Board Representative allows me
|
||||||
|
to bring valuable insights and connections to every engagement.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{impactAreas.map((area, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={area.title}
|
||||||
|
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 + index * 0.2 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className={`absolute h-2 w-full bg-gradient-to-r ${area.gradient} top-0`} />
|
||||||
|
<div className="p-8 bg-white">
|
||||||
|
<div className={`bg-gradient-to-r ${area.gradient} text-white p-4 rounded-full w-20 h-20 flex items-center justify-center mb-6`}>
|
||||||
|
{area.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">{area.title}</h3>
|
||||||
|
<p className="text-gray-600">{area.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
234
src/app/services/leadership/page.tsx
Normal file
234
src/app/services/leadership/page.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { FaUsers, FaLightbulb, FaHandsHelping, FaChalkboardTeacher } from 'react-icons/fa'
|
||||||
|
import { GiPathDistance, GiMountainRoad } from 'react-icons/gi'
|
||||||
|
import { RiTeamLine, RiMentalHealthLine } from 'react-icons/ri'
|
||||||
|
|
||||||
|
const offerings = [
|
||||||
|
{
|
||||||
|
icon: <FaLightbulb className="w-8 h-8" />,
|
||||||
|
title: "Vision Development",
|
||||||
|
description: "I guide individuals in discovering and articulating their unique leadership vision within the Deaf community, drawing from my personal journey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaHandsHelping className="w-8 h-8" />,
|
||||||
|
title: "One-on-One Mentorship",
|
||||||
|
description: "I provide personalized guidance drawing from my decades of advocacy experience and leadership roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaChalkboardTeacher className="w-8 h-8" />,
|
||||||
|
title: "Leadership Workshops",
|
||||||
|
description: "I facilitate interactive sessions focused on developing core leadership competencies, sharing insights from my global Deaf community experiences"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <RiTeamLine className="w-8 h-8" />,
|
||||||
|
title: "Community Building",
|
||||||
|
description: "I share proven strategies for creating and nurturing strong Deaf community networks, based on my experience across 54 countries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const impacts = [
|
||||||
|
{
|
||||||
|
icon: <GiPathDistance className="w-12 h-12" />,
|
||||||
|
title: "Personal Growth Journey",
|
||||||
|
description: "Drawing from my transition from IT professional to Deaf community leader, I personally guide others in discovering their authentic leadership path",
|
||||||
|
gradient: "from-purple-400 to-purple-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <RiMentalHealthLine className="w-12 h-12" />,
|
||||||
|
title: "Individual Empowerment",
|
||||||
|
description: "As your mentor, I help develop your confidence and self-advocacy skills through proven leadership development techniques",
|
||||||
|
gradient: "from-blue-400 to-blue-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <GiMountainRoad className="w-12 h-12" />,
|
||||||
|
title: "Strategic Development",
|
||||||
|
description: "Together, we'll create your personalized roadmap for leadership growth based on your unique strengths and community needs",
|
||||||
|
gradient: "from-mcdi-maroon to-red-600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function ServiceNavigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/services/leadership')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(prevService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(nextService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Leadership() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-purple-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<FaUsers className="w-16 h-16 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-purple-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Transformational Leadership Development
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
As your dedicated leadership mentor, I personally guide and empower the next generation
|
||||||
|
of Deaf leaders through individualized mentorship, strategic guidance, and proven
|
||||||
|
leadership development approaches drawn from my extensive experience.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||||
|
{offerings.map((offering, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={offering.title}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 + 0.4 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-purple-600 mb-4">
|
||||||
|
{offering.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">{offering.title}</h3>
|
||||||
|
<p className="text-gray-600">{offering.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-purple-600 to-mcdi-maroon rounded-2xl p-8 text-white max-w-4xl mx-auto mb-16"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Why Work With Me?</h2>
|
||||||
|
<ul className="space-y-4">
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-3">•</span>
|
||||||
|
<p>I bring proven leadership experience spanning state and national levels in prominent Deaf organizations</p>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-3">•</span>
|
||||||
|
<p>I offer unique insights from my two-year journey across 54 countries engaging with global Deaf communities</p>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-3">•</span>
|
||||||
|
<p>I provide personalized mentorship drawing from my transition from IT management to Deaf advocacy</p>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-3">•</span>
|
||||||
|
<p>I focus on individual growth while maintaining a strong connection to community impact</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{impacts.map((impact, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={impact.title}
|
||||||
|
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 + index * 0.2 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className={`absolute h-2 w-full bg-gradient-to-r ${impact.gradient} top-0`} />
|
||||||
|
<div className="p-8 bg-white">
|
||||||
|
<div className={`bg-gradient-to-r ${impact.gradient} text-white p-4 rounded-full w-20 h-20 flex items-center justify-center mb-6`}>
|
||||||
|
{impact.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">{impact.title}</h3>
|
||||||
|
<p className="text-gray-600">{impact.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
231
src/app/services/legal-literacy/page.tsx
Normal file
231
src/app/services/legal-literacy/page.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { GiTeacher, GiScales } from 'react-icons/gi'
|
||||||
|
import { FaHandsHelping, FaPencilAlt, FaUserFriends, FaExclamationTriangle } from 'react-icons/fa'
|
||||||
|
import { MdGavel, MdForum } from 'react-icons/md'
|
||||||
|
|
||||||
|
const offerings = [
|
||||||
|
{
|
||||||
|
icon: <GiTeacher className="w-8 h-8" />,
|
||||||
|
title: "Self-Advocacy Training",
|
||||||
|
description: "I equip individuals with tools to understand and assert their legal rights, drawing from my extensive experience in Deaf advocacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaHandsHelping className="w-8 h-8" />,
|
||||||
|
title: "Peer Advocacy Workshops",
|
||||||
|
description: "I facilitate workshops that empower Deaf individuals to effectively advocate for others within our community"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MdGavel className="w-8 h-8" />,
|
||||||
|
title: "Rights Education",
|
||||||
|
description: "I share comprehensive knowledge about legal rights and anti-discrimination laws specifically affecting the Deaf community"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaPencilAlt className="w-8 h-8" />,
|
||||||
|
title: "Letter Writing Guidance",
|
||||||
|
description: "I provide personalized assistance in crafting effective communications that assert and defend legal rights"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const impactAreas = [
|
||||||
|
{
|
||||||
|
icon: <FaUserFriends className="w-12 h-12" />,
|
||||||
|
title: "Community Empowerment",
|
||||||
|
description: "I foster a network of legally literate Deaf individuals who can confidently navigate their rights and responsibilities",
|
||||||
|
gradient: "from-yellow-400 to-yellow-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MdForum className="w-12 h-12" />,
|
||||||
|
title: "Open Dialogue",
|
||||||
|
description: "I create spaces for sharing experiences and strategies, building collective knowledge within our community",
|
||||||
|
gradient: "from-yellow-500 to-amber-600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function ServiceNavigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/services/legal-literacy')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(prevService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(nextService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LegalLiteracy() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-yellow-50 via-white to-yellow-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-8 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-amber-50 to-yellow-50 border-l-4 border-amber-500 rounded-lg p-6 mb-12 shadow-lg"
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<FaExclamationTriangle className="text-amber-500 w-6 h-6 mr-3" />
|
||||||
|
<h2 className="text-xl font-bold text-amber-700">Important Notice</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-amber-800 leading-relaxed">
|
||||||
|
While I bring extensive experience in Deaf advocacy and rights education, I am not a licensed attorney.
|
||||||
|
The services I provide focus on education and advocacy skills, but do not constitute legal advice or
|
||||||
|
the practice of law. For specific legal matters, please consult with a qualified attorney.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<GiScales className="w-16 h-16 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-yellow-600 to-amber-600 bg-clip-text text-transparent">
|
||||||
|
Legal Literacy & Advocacy
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
As an experienced advocate, I provide comprehensive legal education and advocacy training,
|
||||||
|
empowering the Deaf community to understand and assert their rights effectively.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||||
|
{offerings.map((offering, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={offering.title}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 + 0.4 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-yellow-600 mb-4">
|
||||||
|
{offering.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">{offering.title}</h3>
|
||||||
|
<p className="text-gray-600">{offering.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-yellow-600 to-amber-600 rounded-2xl p-8 text-white max-w-4xl mx-auto mb-16"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">My Approach</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Drawing from my years of advocacy experience and deep understanding of the Deaf community's
|
||||||
|
legal challenges, I provide practical, actionable guidance. My goal is to empower you with
|
||||||
|
the knowledge and confidence to advocate effectively for yourself and others.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{impactAreas.map((area, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={area.title}
|
||||||
|
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 + index * 0.2 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className={`absolute h-2 w-full bg-gradient-to-r ${area.gradient} top-0`} />
|
||||||
|
<div className="p-8 bg-white">
|
||||||
|
<div className={`bg-gradient-to-r ${area.gradient} text-white p-4 rounded-full w-20 h-20 flex items-center justify-center mb-6`}>
|
||||||
|
{area.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">{area.title}</h3>
|
||||||
|
<p className="text-gray-600">{area.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
213
src/app/services/narratives/page.tsx
Normal file
213
src/app/services/narratives/page.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { BiBookHeart, BiMessageDetail, BiMovie } from 'react-icons/bi'
|
||||||
|
import { FaUserTie, FaChalkboardTeacher, FaBlog } from 'react-icons/fa'
|
||||||
|
|
||||||
|
const offerings = [
|
||||||
|
{
|
||||||
|
icon: <BiMessageDetail className="w-8 h-8" />,
|
||||||
|
title: "Personal Storytelling",
|
||||||
|
description: "I take my own experiences and lessons as a Deaf person and weave them into compelling narratives that inspire and challenge societal perceptions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaUserTie className="w-8 h-8" />,
|
||||||
|
title: "Community Story Collection",
|
||||||
|
description: "I reach into the depths of the Deaf community to bring their experiences to the forefront, creating informative and emotionally resonant content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BiMovie className="w-8 h-8" />,
|
||||||
|
title: "Video Content Creation",
|
||||||
|
description: "I produce high-quality video content that distills complex ideas into accessible and relatable narratives"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaChalkboardTeacher className="w-8 h-8" />,
|
||||||
|
title: "Educational Resources",
|
||||||
|
description: "I develop comprehensive materials about Deaf culture, ASL, and advocacy strategies"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const impactAreas = [
|
||||||
|
{
|
||||||
|
icon: <BiBookHeart className="w-12 h-12" />,
|
||||||
|
title: "Inspiring Deaf Role Models",
|
||||||
|
description: "I create content featuring Deaf individuals from various walks of life, showcasing their accomplishments and journeys",
|
||||||
|
gradient: "from-green-400 to-green-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaBlog className="w-12 h-12" />,
|
||||||
|
title: "Written Articles",
|
||||||
|
description: "I provide written narratives covering various aspects of Deaf culture and living, stirring empathy and fostering inclusivity",
|
||||||
|
gradient: "from-green-400 to-green-600"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function ServiceNavigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/services/narratives')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(prevService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={`flex-1 px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => router.push(nextService.path)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Narratives() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<BiBookHeart className="w-16 h-16 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-green-600 to-green-800 bg-clip-text text-transparent">
|
||||||
|
Deaf Community Narratives
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
I illuminate the vivid collage of experiences, resilience, triumphs, and challenges
|
||||||
|
within the Deaf community through compelling storytelling and content creation.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||||
|
{offerings.map((offering, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={offering.title}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 + 0.4 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-green-600 mb-4">
|
||||||
|
{offering.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">{offering.title}</h3>
|
||||||
|
<p className="text-gray-600">{offering.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-green-600 to-green-800 rounded-2xl p-8 text-white max-w-4xl mx-auto mb-16"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">My Commitment</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Through my Deaf Community Narratives services, I aim to enhance visibility, foster understanding,
|
||||||
|
and empower the Deaf community at large. Together, let's express the diverse, enriching
|
||||||
|
perspectives that our community holds.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{impactAreas.map((area, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={area.title}
|
||||||
|
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1.2 + index * 0.2 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className={`absolute h-2 w-full bg-gradient-to-r ${area.gradient} top-0`} />
|
||||||
|
<div className="p-8 bg-white">
|
||||||
|
<div className={`bg-gradient-to-r ${area.gradient} text-white p-4 rounded-full w-20 h-20 flex items-center justify-center mb-6`}>
|
||||||
|
{area.icon}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">{area.title}</h3>
|
||||||
|
<p className="text-gray-600">{area.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
151
src/app/services/page.tsx
Normal file
151
src/app/services/page.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, useInView } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { FaSignLanguage, FaUsers } from 'react-icons/fa'
|
||||||
|
import { RiCommunityLine } from 'react-icons/ri'
|
||||||
|
import { BiBookHeart } from 'react-icons/bi'
|
||||||
|
import { GiScales } from 'react-icons/gi'
|
||||||
|
import { MdVideoLibrary } from 'react-icons/md'
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
description: 'Specializing in accurately translating the richness and diversity of ASL expressions into English, ensuring seamless communication and fostering understanding and inclusivity.',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
icon: <FaSignLanguage className="w-12 h-12" />,
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
description: 'Offering comprehensive leadership services focusing on inspiring and motivating Deaf individuals to exceed expectations and achieve incredible personal growth.',
|
||||||
|
path: '/services/leadership',
|
||||||
|
icon: <FaUsers className="w-12 h-12" />,
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
description: 'Dedicated to amplifying the voices of Deafhood, actively engaging with various audiences, and promoting broad awareness and understanding of our diverse community.',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
icon: <RiCommunityLine className="w-12 h-12" />,
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
description: 'Illuminating the vivid collage of experiences, resilience, triumphs, and challenges within the Deaf community through compelling storytelling.',
|
||||||
|
path: '/services/narratives',
|
||||||
|
icon: <BiBookHeart className="w-12 h-12" />,
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
description: 'Providing comprehensive legal education to the Deaf community, promoting self-advocacy and peer advocacy through knowledge and empowerment.',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
icon: <GiScales className="w-12 h-12" />,
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
description: 'Real stories and experiences from individuals who have been impacted by our services, showcasing the transformative power of our work in the Deaf community.',
|
||||||
|
path: '/testimonials',
|
||||||
|
icon: <MdVideoLibrary className="w-12 h-12" />,
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function ServicesPage() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: false, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardVariants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 50
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
ease: "easeOut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-gray-50">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.h2
|
||||||
|
className="text-4xl font-bold mb-12 text-center bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
Services
|
||||||
|
</motion.h2>
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
{services.map((service) => (
|
||||||
|
<motion.div
|
||||||
|
key={service.title}
|
||||||
|
variants={cardVariants}
|
||||||
|
>
|
||||||
|
<Link href={service.path}>
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-xl shadow-xl overflow-hidden h-full transform transition-all duration-300 hover:-translate-y-2 flex flex-col"
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.02,
|
||||||
|
transition: { duration: 0.2 }
|
||||||
|
}}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className={`h-2 bg-gradient-to-r ${service.color}`} />
|
||||||
|
<div className="p-6 flex-grow">
|
||||||
|
<motion.div
|
||||||
|
className="text-4xl mb-4"
|
||||||
|
whileHover={{ rotate: 10, scale: 1.1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
|
>
|
||||||
|
{service.icon}
|
||||||
|
</motion.div>
|
||||||
|
<h3 className="text-xl font-bold mb-3">{service.title}</h3>
|
||||||
|
<p className="text-gray-600">{service.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`p-4 bg-gradient-to-r ${service.color} border-t-2 border-white/20`}>
|
||||||
|
<span className="text-base md:text-lg font-semibold text-white tracking-wider flex items-center justify-between">
|
||||||
|
Learn More
|
||||||
|
<motion.span
|
||||||
|
initial={{ x: 0 }}
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
className="transform transition-transform"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</motion.span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
181
src/app/services/testimonials/page.tsx
Normal file
181
src/app/services/testimonials/page.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { MdVideoLibrary, MdOutlineRateReview } from 'react-icons/md'
|
||||||
|
import { BiMessageSquareDetail } from 'react-icons/bi'
|
||||||
|
import { FaSignLanguage, FaClosedCaptioning } from 'react-icons/fa'
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function ServiceNavigation() {
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/testimonials')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<Link href={prevService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href={nextService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Testimonials() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<MdVideoLibrary className="w-16 h-16 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Client Testimonials
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
Real stories and experiences from individuals who have been impacted by my advocacy
|
||||||
|
and leadership work in the Deaf community.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-xl shadow-lg p-8 mb-16 max-w-4xl mx-auto"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center space-x-8 mb-8">
|
||||||
|
<FaSignLanguage className="w-12 h-12 text-blue-600" />
|
||||||
|
<FaClosedCaptioning className="w-12 h-12 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-6">Coming Soon</h2>
|
||||||
|
<p className="text-gray-700 text-center mb-8">
|
||||||
|
I am currently collecting powerful testimonials from individuals who have experienced
|
||||||
|
transformative changes through my services. Each story will be presented in both ASL
|
||||||
|
and written English to ensure full accessibility.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8 mt-12">
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<MdVideoLibrary className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">ASL Videos</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Authentic testimonials in American Sign Language</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<BiMessageSquareDetail className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">English Transcripts</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Full written translations for complete accessibility</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<MdOutlineRateReview className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">Impact Stories</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Real experiences of transformation and growth</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-mcdi-maroon rounded-2xl p-8 text-white max-w-4xl mx-auto"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Share Your Story</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Have you worked with me? Your story can inspire others and contribute to
|
||||||
|
the growing narrative of empowerment in the Deaf community. Contact me to share your
|
||||||
|
experience and potentially be featured in my testimonials.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
181
src/app/testimonials/page.tsx
Normal file
181
src/app/testimonials/page.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { MdVideoLibrary, MdOutlineRateReview } from 'react-icons/md'
|
||||||
|
import { BiMessageSquareDetail } from 'react-icons/bi'
|
||||||
|
import { FaSignLanguage, FaClosedCaptioning } from 'react-icons/fa'
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
path: '/testimonials',
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
path: '/services/leadership',
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
path: '/services/narratives',
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const ServiceNavigation = () => {
|
||||||
|
const currentIndex = services.findIndex(s => s.path === '/testimonials')
|
||||||
|
const prevService = services[(currentIndex - 1 + services.length) % services.length]
|
||||||
|
const nextService = services[(currentIndex + 1) % services.length]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="mt-16 flex items-center justify-between gap-8 max-w-6xl mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.4 }}
|
||||||
|
>
|
||||||
|
<Link href={prevService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${prevService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span>←</span>
|
||||||
|
<span className="font-medium">{prevService.title}</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href={nextService.path} className="flex-1">
|
||||||
|
<motion.div
|
||||||
|
className={`px-6 py-4 rounded-xl bg-gradient-to-r ${nextService.color} text-white shadow-lg`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{nextService.title}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Testimonials() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 py-24"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-4xl mx-auto mb-16 px-4"
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<MdVideoLibrary className="w-16 h-16 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-center mb-8 leading-tight pb-2 bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Client Testimonials
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-700 text-center leading-relaxed">
|
||||||
|
Real stories and experiences from individuals who have been impacted by my advocacy
|
||||||
|
and leadership work in the Deaf community.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-xl shadow-lg p-8 mb-16 max-w-4xl mx-auto"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center space-x-8 mb-8">
|
||||||
|
<FaSignLanguage className="w-12 h-12 text-blue-600" />
|
||||||
|
<FaClosedCaptioning className="w-12 h-12 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-6">Coming Soon</h2>
|
||||||
|
<p className="text-gray-700 text-center mb-8">
|
||||||
|
I am currently collecting powerful testimonials from individuals who have experienced
|
||||||
|
transformative changes through my services. Each story will be presented in both ASL
|
||||||
|
and written English to ensure full accessibility.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8 mt-12">
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<MdVideoLibrary className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">ASL Videos</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Authentic testimonials in American Sign Language</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<BiMessageSquareDetail className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">English Transcripts</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Full written translations for complete accessibility</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="bg-blue-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
|
<MdOutlineRateReview className="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold mb-2">Impact Stories</h3>
|
||||||
|
<p className="text-gray-600 text-sm">Real experiences of transformation and growth</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-blue-600 to-mcdi-maroon rounded-2xl p-8 text-white max-w-4xl mx-auto"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Share Your Story</h2>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Have you worked with me? Your story can inspire others and contribute to
|
||||||
|
the growing narrative of empowerment in the Deaf community. Contact me to share your
|
||||||
|
experience and potentially be featured in my testimonials.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
<ServiceNavigation />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
106
src/components/biography/JourneyVisual.tsx
Normal file
106
src/components/biography/JourneyVisual.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const timelineData = [
|
||||||
|
{
|
||||||
|
year: 1998,
|
||||||
|
title: 'IT Career Start',
|
||||||
|
description: 'Began as PC technician',
|
||||||
|
details: 'Started career in technology sector'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2009,
|
||||||
|
title: 'IT Manager',
|
||||||
|
description: 'Government agency IT manager',
|
||||||
|
details: 'Reached senior technology leadership position'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2015,
|
||||||
|
title: 'Global Journey',
|
||||||
|
description: '47 countries, Deaf communities',
|
||||||
|
details: 'Immersive experience with Deaf communities worldwide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2021,
|
||||||
|
title: 'KAD Board Member',
|
||||||
|
description: 'Kansas Association of the Deaf',
|
||||||
|
details: 'Appointed to KAD board, beginning state-level leadership'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2023,
|
||||||
|
title: 'KAD Vice President',
|
||||||
|
description: 'Leadership advancement',
|
||||||
|
details: 'Elected as Vice President of Kansas Association of the Deaf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2024,
|
||||||
|
title: 'NAD Region II Representative',
|
||||||
|
description: 'National Association of the Deaf',
|
||||||
|
details: 'Elected to serve Midwest region on national board'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function JourneyVisual() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="w-full py-8"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="relative grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-8 z-20">
|
||||||
|
{timelineData.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.year}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: index * 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-8 h-8 rounded-full bg-white border-4 border-blue-600 relative z-30 mb-4"
|
||||||
|
whileHover={{ scale: 1.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-blue-600 rounded-full opacity-20"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.5, 1],
|
||||||
|
opacity: [0.2, 0.1, 0.2],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="text-center"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-bold text-xl text-blue-600">{item.year}</h3>
|
||||||
|
<p className="font-medium text-gray-800">{item.title}</p>
|
||||||
|
<p className="text-sm text-gray-600">{item.details}</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute left-0 w-full h-1 bg-gray-200 top-[19%] transform -translate-y-1/2 z-10 hidden lg:block">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-gradient-to-r from-blue-600 to-mcdi-maroon"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
whileInView={{ width: '100%' }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
148
src/components/biography/RegionMap.tsx
Normal file
148
src/components/biography/RegionMap.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react"
|
||||||
|
import { ComposableMap, Geographies, Geography } from "react-simple-maps"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { feature } from "topojson-client"
|
||||||
|
import { GeometryObject, Topology } from "topojson-specification"
|
||||||
|
import usaStates from "./usa-states.json"
|
||||||
|
|
||||||
|
interface StateNameToPostal {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProperties {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RegionMap() {
|
||||||
|
const [hoveredState, setHoveredState] = useState<string | null>(null)
|
||||||
|
const [dimensions, setDimensions] = useState({ width: 800, height: 400 })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const width = window.innerWidth
|
||||||
|
if (width < 640) { // sm breakpoint
|
||||||
|
setDimensions({ width: 300, height: 200 })
|
||||||
|
} else if (width < 768) { // md breakpoint
|
||||||
|
setDimensions({ width: 500, height: 300 })
|
||||||
|
} else {
|
||||||
|
setDimensions({ width: 800, height: 400 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const regionTwoStates = [
|
||||||
|
"IL", "IN", "IA", "KS", "KY",
|
||||||
|
"MI", "MN", "MO", "NE", "ND", "SD", "WI"
|
||||||
|
]
|
||||||
|
|
||||||
|
const stateNameToPostal: StateNameToPostal = {
|
||||||
|
"Illinois": "IL",
|
||||||
|
"Indiana": "IN",
|
||||||
|
"Iowa": "IA",
|
||||||
|
"Kansas": "KS",
|
||||||
|
"Kentucky": "KY",
|
||||||
|
"Michigan": "MI",
|
||||||
|
"Minnesota": "MN",
|
||||||
|
"Missouri": "MO",
|
||||||
|
"Nebraska": "NE",
|
||||||
|
"North Dakota": "ND",
|
||||||
|
"South Dakota": "SD",
|
||||||
|
"Wisconsin": "WI"
|
||||||
|
}
|
||||||
|
|
||||||
|
const geographyData = feature(
|
||||||
|
usaStates as unknown as Topology<{ states: GeometryObject & { properties: StateProperties } }>,
|
||||||
|
usaStates.objects.states as any
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
className="w-full bg-white rounded-lg shadow-lg p-2 sm:p-4"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-2 sm:mb-4">
|
||||||
|
<h3 className="text-lg sm:text-xl font-bold bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
NAD Region II: Midwest Heartland
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1 sm:gap-2 justify-center mb-2 sm:mb-4">
|
||||||
|
{regionTwoStates.map((state) => (
|
||||||
|
<motion.span
|
||||||
|
key={state}
|
||||||
|
className={`px-1.5 sm:px-2 py-0.5 sm:py-1 text-xs font-semibold rounded-full transition-all duration-300 ${
|
||||||
|
hoveredState === state
|
||||||
|
? "bg-gradient-to-r from-blue-600 to-mcdi-maroon text-white"
|
||||||
|
: "bg-gray-200 text-gray-600"
|
||||||
|
}`}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
onMouseEnter={() => setHoveredState(state)}
|
||||||
|
onMouseLeave={() => setHoveredState(null)}
|
||||||
|
>
|
||||||
|
{state}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<ComposableMap
|
||||||
|
projection="geoAlbersUsa"
|
||||||
|
projectionConfig={{
|
||||||
|
scale: dimensions.width * 0.8,
|
||||||
|
center: [-96, 38]
|
||||||
|
}}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
>
|
||||||
|
<Geographies geography={geographyData}>
|
||||||
|
{({ geographies }) =>
|
||||||
|
geographies.map(geo => {
|
||||||
|
const stateName = geo.properties.name
|
||||||
|
const statePostal = stateNameToPostal[stateName] || ""
|
||||||
|
const isRegionTwo = regionTwoStates.includes(statePostal)
|
||||||
|
const isHovered = hoveredState === statePostal
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Geography
|
||||||
|
key={geo.rsmKey}
|
||||||
|
geography={geo}
|
||||||
|
fill={isRegionTwo ? (isHovered ? "#1E40AF" : "#2563EB") : "#E5E7EB"}
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
strokeWidth={0.5}
|
||||||
|
onMouseEnter={() => setHoveredState(statePostal)}
|
||||||
|
onMouseLeave={() => setHoveredState(null)}
|
||||||
|
style={{
|
||||||
|
default: {
|
||||||
|
outline: "none",
|
||||||
|
transition: "all 250ms"
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
fill: isRegionTwo ? "#1E40AF" : "#D1D5DB",
|
||||||
|
outline: "none",
|
||||||
|
cursor: "pointer"
|
||||||
|
},
|
||||||
|
pressed: {
|
||||||
|
outline: "none"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Geographies>
|
||||||
|
</ComposableMap>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
src/components/biography/usa-states.json
Normal file
1
src/components/biography/usa-states.json
Normal file
File diff suppressed because one or more lines are too long
101
src/components/home/Hero.tsx
Normal file
101
src/components/home/Hero.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
return (
|
||||||
|
<section className="relative min-h-[calc(100dvh-80px)] flex items-center bg-gradient-to-br from-blue-50 via-white to-blue-50">
|
||||||
|
<div className="container mx-auto px-4 grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center py-12 lg:py-20">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -50 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
className="z-10 text-center lg:text-left"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="mb-4 lg:mb-6"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg lg:text-xl text-blue-600 font-semibold mb-2">Welcome to a World of Deaf Advocacy</h2>
|
||||||
|
<h1 className="text-4xl lg:text-5xl font-bold mb-3 lg:mb-4 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-mcdi-maroon">
|
||||||
|
Chris Haulmark
|
||||||
|
</h1>
|
||||||
|
<h3 className="text-xl lg:text-2xl text-gray-700">Transforming Lives Through Leadership</h3>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-lg lg:text-xl leading-relaxed text-gray-700 mb-6 lg:mb-8 max-w-lg mx-auto lg:mx-0 font-medium"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
>
|
||||||
|
Empowering the Deaf community through advocacy, leadership, and cultural bridge-building. With experience spanning 54 countries and leadership roles in prominent state and national Deaf organizations.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.7 }}
|
||||||
|
>
|
||||||
|
<Link href="/services" className="w-full sm:w-auto">
|
||||||
|
<motion.button
|
||||||
|
className="w-full sm:w-auto bg-gradient-to-r from-blue-600 to-mcdi-maroon text-white px-6 lg:px-8 py-3 rounded-lg shadow-lg text-base lg:text-lg"
|
||||||
|
whileHover={{ scale: 1.05, boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1)" }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Explore Services
|
||||||
|
</motion.button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/biography" className="w-full sm:w-auto">
|
||||||
|
<motion.button
|
||||||
|
className="w-full sm:w-auto border-2 border-blue-600 text-blue-600 px-6 lg:px-8 py-3 rounded-lg hover:bg-blue-50 transition-colors text-base lg:text-lg"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</motion.button>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="relative z-10 mt-8 lg:mt-0"
|
||||||
|
initial={{ opacity: 0, x: 50 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
>
|
||||||
|
<div className="w-full h-[300px] sm:h-[400px] lg:h-[600px] relative group">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent rounded-2xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/images/hero-image.jpg"
|
||||||
|
alt="Chris Haulmark standing by a Dodge Charger near a serene lake"
|
||||||
|
fill
|
||||||
|
className="absolute inset-0 rounded-2xl shadow-2xl object-cover grayscale hover:grayscale-0 transition-all duration-500"
|
||||||
|
priority
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-0 left-0 right-0 p-4 lg:p-8 text-white opacity-100 lg:opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
initial={{ y: 20 }}
|
||||||
|
whileHover={{ y: 0 }}
|
||||||
|
>
|
||||||
|
<p className="text-xl lg:text-2xl font-semibold mb-2 drop-shadow-lg">Global Deaf Advocate</p>
|
||||||
|
<p className="text-base lg:text-lg drop-shadow-lg">Vice President of KAD & NAD Region II Board Representative</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
73
src/components/home/ServiceGrid.tsx
Normal file
73
src/components/home/ServiceGrid.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { FaSignLanguage } from 'react-icons/fa'
|
||||||
|
import { Users, Users2, BookHeart, Scale, Video } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
title: 'ASL-to-English Linguistic Bridging',
|
||||||
|
description: 'Precise translations capturing ASL nuances',
|
||||||
|
path: '/services/asl-bridging',
|
||||||
|
icon: <FaSignLanguage className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-blue-400 to-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transformational Leadership',
|
||||||
|
description: 'Inspiring and motivating Deaf individuals',
|
||||||
|
path: '/services/leadership',
|
||||||
|
icon: <Users className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-purple-400 to-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deafhood Representation',
|
||||||
|
description: 'Amplifying voices of Deafhood',
|
||||||
|
path: '/services/deafhood',
|
||||||
|
icon: <Users2 className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-mcdi-maroon to-red-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deaf Community Narratives',
|
||||||
|
description: 'Sharing stories that matter',
|
||||||
|
path: '/services/narratives',
|
||||||
|
icon: <BookHeart className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-green-400 to-green-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legal Literacy',
|
||||||
|
description: 'Empowering through legal knowledge',
|
||||||
|
path: '/services/legal-literacy',
|
||||||
|
icon: <Scale className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-yellow-400 to-yellow-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Testimonials',
|
||||||
|
description: 'Real stories and experiences from individuals who have been impacted by our services.',
|
||||||
|
path: '/testimonials',
|
||||||
|
icon: <Video className="w-8 h-8 sm:w-12 sm:h-12" />,
|
||||||
|
color: 'from-indigo-400 to-indigo-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function ServiceGrid() {
|
||||||
|
return (
|
||||||
|
<section className="py-12 sm:py-16 lg:py-20 bg-gray-50 w-full">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 w-full">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold mb-8 sm:mb-12 text-center bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Services
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 w-full">
|
||||||
|
{services.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.title}
|
||||||
|
className="w-full h-full opacity-100" // Added opacity-100 to ensure visibility
|
||||||
|
>
|
||||||
|
{/* Rest of your card content */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
src/components/home/Timeline.tsx
Normal file
95
src/components/home/Timeline.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const timelineData = [
|
||||||
|
{
|
||||||
|
year: 1998,
|
||||||
|
title: 'IT Career Start',
|
||||||
|
description: 'Began as PC technician',
|
||||||
|
details: 'Started career in technology sector'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2009,
|
||||||
|
title: 'IT Manager',
|
||||||
|
description: 'Government agency IT manager',
|
||||||
|
details: 'Reached senior technology leadership position'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2015,
|
||||||
|
title: 'Global Journey',
|
||||||
|
description: '47 countries, Deaf communities',
|
||||||
|
details: 'Immersive experience with Deaf communities worldwide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2021,
|
||||||
|
title: 'KAD Board Member',
|
||||||
|
description: 'Kansas Association of the Deaf',
|
||||||
|
details: 'Appointed to KAD board, beginning state-level leadership'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2023,
|
||||||
|
title: 'KAD Vice President',
|
||||||
|
description: 'Leadership advancement',
|
||||||
|
details: 'Elected as Vice President of Kansas Association of the Deaf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: 2024,
|
||||||
|
title: 'NAD Region II Representative',
|
||||||
|
description: 'National Association of the Deaf',
|
||||||
|
details: 'Elected to serve Midwest region on national board'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Timeline() {
|
||||||
|
return (
|
||||||
|
<div className="py-20">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl font-bold mb-12 text-center"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
Career Timeline
|
||||||
|
</motion.h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-8 justify-items-center">
|
||||||
|
{timelineData.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.year}
|
||||||
|
className="relative text-center w-full max-w-[250px]"
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.6,
|
||||||
|
delay: index * 0.2,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-4 h-4 bg-blue-600 rounded-full mx-auto"
|
||||||
|
whileHover={{ scale: 1.5 }}
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
whileInView={{ scale: 1 }}
|
||||||
|
viewport={{ once: false }}
|
||||||
|
transition={{
|
||||||
|
delay: index * 0.2,
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="font-bold">{item.year}</h3>
|
||||||
|
<p className="text-sm">{item.title}</p>
|
||||||
|
<p className="text-xs text-gray-600">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
113
src/components/layout/Footer.tsx
Normal file
113
src/components/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import ContactModal from '../shared/ContactModal'
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const [isContactModalOpen, setIsContactModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ path: '/services/asl-bridging', label: 'ASL-to-English Linguistic Bridging' },
|
||||||
|
{ path: '/services/leadership', label: 'Transformational Leadership' },
|
||||||
|
{ path: '/services/deafhood', label: 'Deafhood Representation' },
|
||||||
|
{ path: '/services/narratives', label: 'Deaf Community Narratives' },
|
||||||
|
{ path: '/services/legal-literacy', label: 'Legal Literacy' },
|
||||||
|
{ path: '/testimonials', label: 'Testimonials' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="bg-gradient-to-br from-gray-900 to-gray-800 text-white py-12">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||||
|
{/* Quick Links section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Quick Links
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li>
|
||||||
|
<Link href="/" className="hover:text-blue-400 transition-colors">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/biography" className="hover:text-blue-400 transition-colors">
|
||||||
|
Biography
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/services" className="hover:text-blue-400 transition-colors">
|
||||||
|
Services
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Services
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{services.map((service) => (
|
||||||
|
<li key={service.path}>
|
||||||
|
<Link
|
||||||
|
href={service.path}
|
||||||
|
className="hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
{service.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Contact
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300 mb-4">
|
||||||
|
Get in touch for collaborations and services
|
||||||
|
</p>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => setIsContactModalOpen(true)}
|
||||||
|
className="px-6 py-3 bg-gradient-to-r from-blue-400 to-mcdi-maroon text-white rounded-lg shadow-lg hover:shadow-xl transition-all"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Contact Now
|
||||||
|
</motion.button>
|
||||||
|
<ContactModal
|
||||||
|
isOpen={isContactModalOpen}
|
||||||
|
onClose={() => setIsContactModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Copyright section */}
|
||||||
|
<div className="mt-12 pt-8 border-t border-gray-700">
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<div className="text-center flex flex-col">
|
||||||
|
<span className="text-gray-400 whitespace-nowrap">© {new Date().getFullYear()} Chris Haulmark.</span>
|
||||||
|
<span className="text-gray-400 whitespace-nowrap">All rights reserved.</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-400">Designed and Maintained by </span>
|
||||||
|
<a
|
||||||
|
href="http://deafgain.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-yellow-400 hover:text-yellow-300 transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
DeafGain, LLC
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
121
src/components/layout/Header.tsx
Normal file
121
src/components/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { Menu, X } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
return pathname === path
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ path: '/', label: 'Home' },
|
||||||
|
{ path: '/biography', label: 'Biography' },
|
||||||
|
{ path: '/services', label: 'Services' },
|
||||||
|
{ path: '/testimonials', label: 'Testimonials' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const menuVariants = {
|
||||||
|
open: {
|
||||||
|
opacity: 1,
|
||||||
|
x: 0,
|
||||||
|
transition: { type: "spring", stiffness: 300, damping: 30 }
|
||||||
|
},
|
||||||
|
closed: {
|
||||||
|
opacity: 0,
|
||||||
|
x: "100%",
|
||||||
|
transition: { type: "spring", stiffness: 300, damping: 30 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.header
|
||||||
|
className="fixed w-full bg-white/90 backdrop-blur-sm z-50 shadow-lg"
|
||||||
|
initial={{ y: -100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-6 flex justify-between items-center">
|
||||||
|
<motion.div
|
||||||
|
className="text-4xl font-bold"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
<Link href="/" className="relative group bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Chris Haulmark
|
||||||
|
<motion.div
|
||||||
|
className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-mcdi-maroon group-hover:w-full transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<nav className="hidden md:block text-lg">
|
||||||
|
<ul className="flex space-x-12">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<Link href={item.path} className="relative group">
|
||||||
|
<span className={`transition-colors ${
|
||||||
|
isActive(item.path)
|
||||||
|
? 'text-blue-600'
|
||||||
|
: 'hover:text-blue-600'
|
||||||
|
}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<motion.div
|
||||||
|
className={`absolute -bottom-1 left-0 h-0.5 bg-gradient-to-r from-blue-600 to-mcdi-maroon transition-all duration-300 ${
|
||||||
|
isActive(item.path) ? 'w-full' : 'w-0 group-hover:w-full'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
className="md:hidden text-2xl"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{isOpen ? <X /> : <Menu />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile Navigation */}
|
||||||
|
<motion.nav
|
||||||
|
className="fixed top-[88px] right-0 h-screen w-64 bg-white shadow-xl md:hidden"
|
||||||
|
initial="closed"
|
||||||
|
animate={isOpen ? "open" : "closed"}
|
||||||
|
variants={menuVariants}
|
||||||
|
>
|
||||||
|
<ul className="flex flex-col p-4 space-y-4">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<motion.li
|
||||||
|
key={item.path}
|
||||||
|
whileHover={{ x: 10 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={item.path}
|
||||||
|
className={`block p-2 rounded-lg ${
|
||||||
|
isActive(item.path)
|
||||||
|
? 'bg-blue-50 text-blue-600'
|
||||||
|
: 'hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.nav>
|
||||||
|
</div>
|
||||||
|
</motion.header>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/components/layout/Layout.tsx
Normal file
17
src/components/layout/Layout.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import Header from './Header'
|
||||||
|
import Footer from './Footer'
|
||||||
|
import { NotificationProvider } from '@/components/providers/notification-provider'
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col bg-white">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-grow pt-20">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
src/components/providers/notification-provider.tsx
Normal file
36
src/components/providers/notification-provider.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createContext, useContext } from 'react'
|
||||||
|
import Toast from '../shared/Toast'
|
||||||
|
|
||||||
|
interface NotificationContextType {
|
||||||
|
showNotification: (message: string, type: 'success' | 'error') => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationContext = createContext<NotificationContextType>({
|
||||||
|
showNotification: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useNotification = () => useContext(NotificationContext)
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
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)
|
||||||
|
setTimeout(() => setIsVisible(false), 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={{ showNotification }}>
|
||||||
|
{children}
|
||||||
|
<Toast message={message} type={type} isVisible={isVisible} />
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
src/components/shared/AnimatedDot.tsx
Normal file
49
src/components/shared/AnimatedDot.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
interface AnimatedDotProps {
|
||||||
|
active?: boolean
|
||||||
|
onClick?: () => void
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnimatedDot({
|
||||||
|
active = false,
|
||||||
|
onClick,
|
||||||
|
size = 'md'
|
||||||
|
}: AnimatedDotProps) {
|
||||||
|
const sizes = {
|
||||||
|
sm: 'w-3 h-3',
|
||||||
|
md: 'w-4 h-4',
|
||||||
|
lg: 'w-5 h-5'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`${sizes[size]} rounded-full cursor-pointer relative`}
|
||||||
|
initial={{ backgroundColor: '#E5E7EB' }}
|
||||||
|
animate={{
|
||||||
|
backgroundColor: active ? '#2563EB' : '#E5E7EB',
|
||||||
|
scale: active ? 1.2 : 1
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.3 }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full bg-blue-500"
|
||||||
|
initial={{ opacity: 0, scale: 0.5 }}
|
||||||
|
animate={{
|
||||||
|
opacity: [0, 0.5, 0],
|
||||||
|
scale: [1, 1.5, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
src/components/shared/Card.tsx
Normal file
28
src/components/shared/Card.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Card({ title, description, icon, onClick }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-lg shadow-lg p-6 cursor-pointer hover:-translate-y-1 hover:scale-102"
|
||||||
|
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<div className="mb-4 text-blue-600">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="text-xl font-bold mb-2">{title}</h3>
|
||||||
|
<p className="text-gray-600">{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
251
src/components/shared/ContactModal.tsx
Normal file
251
src/components/shared/ContactModal.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import { useNotification } from '@/components/providers/notification-provider'
|
||||||
|
|
||||||
|
interface ContactModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ value: 'asl-bridging', label: 'ASL-to-English Linguistic Bridging' },
|
||||||
|
{ value: 'leadership', label: 'Transformational Leadership' },
|
||||||
|
{ value: 'deafhood', label: 'Deafhood Representation' },
|
||||||
|
{ value: 'narratives', label: 'Deaf Community Narratives' },
|
||||||
|
{ value: 'legal-literacy', label: 'Legal Literacy' },
|
||||||
|
{ value: 'general-inquiry', label: 'General Inquiry or Question' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const validateEmail = (email: string) => {
|
||||||
|
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
return pattern.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContactModal({ isOpen, onClose }: ContactModalProps) {
|
||||||
|
const { showNotification } = useNotification()
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
service: '',
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
service: '',
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateField = (name: string, value: string) => {
|
||||||
|
switch(name) {
|
||||||
|
case 'name':
|
||||||
|
return value.length < 2 ? 'Name must be at least 2 characters' :
|
||||||
|
value.length > 50 ? 'Name must be less than 50 characters' : ''
|
||||||
|
case 'email':
|
||||||
|
return !validateEmail(value) ? 'Please enter a valid email address' : ''
|
||||||
|
case 'service':
|
||||||
|
return !value ? 'Please select a service' : ''
|
||||||
|
case 'message':
|
||||||
|
return value.length < 10 ? 'Message must be at least 10 characters' :
|
||||||
|
value.length > 1000 ? 'Message must be less than 1000 characters' : ''
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }))
|
||||||
|
setErrors(prev => ({ ...prev, [name]: validateField(name, value) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = () => {
|
||||||
|
const newErrors = {
|
||||||
|
name: validateField('name', formData.name),
|
||||||
|
email: validateField('email', formData.email),
|
||||||
|
service: validateField('service', formData.service),
|
||||||
|
message: validateField('message', formData.message)
|
||||||
|
}
|
||||||
|
setErrors(newErrors)
|
||||||
|
return !Object.values(newErrors).some(error => error !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!isFormValid()) {
|
||||||
|
showNotification('Please correct the errors in the form.', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/contact', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setFormData({ name: '', email: '', service: '', message: '' })
|
||||||
|
onClose()
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotification('Message sent successfully! We will get back to you soon.', 'success')
|
||||||
|
}, 300)
|
||||||
|
} else {
|
||||||
|
showNotification('Failed to send message. Please try again.', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error)
|
||||||
|
showNotification('Error sending message. Please try again.', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-2 sm:p-4 bg-black/50 backdrop-blur-sm"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative w-full max-w-4xl bg-white rounded-xl sm:rounded-2xl shadow-xl sm:shadow-2xl"
|
||||||
|
initial={{ scale: 0.9, y: 20 }}
|
||||||
|
animate={{ scale: 1, y: 0 }}
|
||||||
|
exit={{ scale: 0.9, y: 20 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-2 right-2 sm:top-4 sm:right-4 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 md:p-8">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6 bg-gradient-to-r from-blue-600 to-mcdi-maroon bg-clip-text text-transparent">
|
||||||
|
Let's Connect
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1 sm:mb-2">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-lg border ${
|
||||||
|
errors.name ? 'border-red-500' : 'border-gray-300'
|
||||||
|
} focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-gray-900 text-sm sm:text-base`}
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-500">{errors.name}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1 sm:mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-lg border ${
|
||||||
|
errors.email ? 'border-red-500' : 'border-gray-300'
|
||||||
|
} focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-gray-900 text-sm sm:text-base`}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-500">{errors.email}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1 sm:mb-2">
|
||||||
|
Area of Interest
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="service"
|
||||||
|
required
|
||||||
|
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-lg border ${
|
||||||
|
errors.service ? 'border-red-500' : 'border-gray-300'
|
||||||
|
} focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-gray-900 text-sm sm:text-base`}
|
||||||
|
value={formData.service}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<option value="">Select an option</option>
|
||||||
|
{services.map((service) => (
|
||||||
|
<option key={service.value} value={service.value}>
|
||||||
|
{service.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.service && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-500">{errors.service}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1 sm:mb-2">
|
||||||
|
Message
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
required
|
||||||
|
rows={4}
|
||||||
|
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-lg border ${
|
||||||
|
errors.message ? 'border-red-500' : 'border-gray-300'
|
||||||
|
} focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-gray-900 text-sm sm:text-base`}
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{errors.message && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-500">{errors.message}</p>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 sm:py-4 bg-gradient-to-r from-blue-600 to-mcdi-maroon text-white rounded-lg font-medium text-sm sm:text-base shadow-lg hover:shadow-xl transition-all"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
src/components/shared/Toast.tsx
Normal file
42
src/components/shared/Toast.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
message: string
|
||||||
|
type: 'success' | 'error'
|
||||||
|
isVisible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Toast({ message, type, isVisible }: ToastProps) {
|
||||||
|
const bgColor = type === 'success'
|
||||||
|
? 'from-blue-600 to-mcdi-maroon'
|
||||||
|
: 'from-red-600 to-red-800'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isVisible && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
className="w-full max-w-md bg-white rounded-xl shadow-2xl overflow-hidden mx-4"
|
||||||
|
>
|
||||||
|
<div className={`h-2 bg-gradient-to-r ${bgColor}`} />
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-lg font-medium text-gray-800 text-center">{message}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
src/lib/email.ts
Normal file
41
src/lib/email.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
interface EmailData {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
service: string
|
||||||
|
message: 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 - ${data.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> ${data.name}</p>
|
||||||
|
<p><strong>Email:</strong> ${data.email}</p>
|
||||||
|
<p><strong>Service:</strong> ${data.service}</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
|
||||||
|
}
|
||||||
72
src/lib/rate-limit.ts
Normal file
72
src/lib/rate-limit.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Redis } from '@upstash/redis'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
|
export interface RateLimitConfig {
|
||||||
|
interval: number // in seconds
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimitResult {
|
||||||
|
success: boolean
|
||||||
|
limit: number
|
||||||
|
remaining: number
|
||||||
|
reset: number // timestamp in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RateLimit {
|
||||||
|
private redis: Redis
|
||||||
|
private prefix: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.redis = Redis.fromEnv()
|
||||||
|
this.prefix = 'ratelimit'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIP(): string {
|
||||||
|
const headersList = headers()
|
||||||
|
const forwardedFor = headersList.get('x-forwarded-for')
|
||||||
|
const realIP = headersList.get('x-real-ip')
|
||||||
|
const ip = forwardedFor?.split(',')[0] || realIP || 'unknown'
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKey(identifier: string): string {
|
||||||
|
return `${this.prefix}:${identifier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async check(config: RateLimitConfig): Promise<RateLimitResult> {
|
||||||
|
const ip = this.getIP()
|
||||||
|
const key = this.getKey(ip)
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const windowStart = now - (now % config.interval)
|
||||||
|
const windowKey = `${key}:${windowStart}`
|
||||||
|
|
||||||
|
const pipeline = this.redis.pipeline()
|
||||||
|
pipeline.incr(windowKey)
|
||||||
|
pipeline.expire(windowKey, config.interval)
|
||||||
|
|
||||||
|
const [count] = await pipeline.exec()
|
||||||
|
const remaining = Math.max(0, config.limit - (count as number))
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: (count as number) <= config.limit,
|
||||||
|
limit: config.limit,
|
||||||
|
remaining,
|
||||||
|
reset: windowStart + config.interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default configuration
|
||||||
|
const DEFAULT_CONFIG: RateLimitConfig = {
|
||||||
|
interval: 60 * 60, // 1 hour
|
||||||
|
limit: 5 // 5 requests per hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for easy usage
|
||||||
|
export async function rateLimit(
|
||||||
|
config: RateLimitConfig = DEFAULT_CONFIG
|
||||||
|
): Promise<RateLimitResult> {
|
||||||
|
const limiter = new RateLimit()
|
||||||
|
return limiter.check(config)
|
||||||
|
}
|
||||||
42
src/middleware.ts
Normal file
42
src/middleware.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
// Generate nonce using Web Crypto API
|
||||||
|
const nonce = Buffer.from(
|
||||||
|
crypto.getRandomValues(new Uint8Array(16))
|
||||||
|
).toString('base64')
|
||||||
|
|
||||||
|
// Simplified CSP Header without SSL requirements
|
||||||
|
const cspHeader = `
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: blob:;
|
||||||
|
font-src 'self';
|
||||||
|
object-src 'none';
|
||||||
|
base-uri 'self';
|
||||||
|
form-action 'self';
|
||||||
|
frame-ancestors 'none';
|
||||||
|
`
|
||||||
|
|
||||||
|
const response = NextResponse.next()
|
||||||
|
|
||||||
|
// Security Headers without SSL-specific directives
|
||||||
|
response.headers.set('Content-Security-Policy', cspHeader.replace(/\s{2,}/g, ' ').trim())
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY')
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff')
|
||||||
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
|
||||||
|
response.headers.set('X-XSS-Protection', '1; mode=block')
|
||||||
|
response.headers.set('X-DNS-Prefetch-Control', 'on')
|
||||||
|
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/api/contact',
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||||
|
]
|
||||||
|
}
|
||||||
60
src/styles/globals.css
Normal file
60
src/styles/globals.css
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mcdi-maroon: #800020;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply antialiased text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
@apply max-w-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
@apply text-blue-600 no-underline hover:text-blue-700 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1, .prose h2, .prose h3, .prose h4 {
|
||||||
|
@apply text-gray-900 font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p {
|
||||||
|
@apply text-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul {
|
||||||
|
@apply list-disc list-inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol {
|
||||||
|
@apply list-decimal list-inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
@apply border-l-4 border-blue-600 pl-4 italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-blue-600 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-blue-700;
|
||||||
|
}
|
||||||
31
tailwind.config.js
Normal file
31
tailwind.config.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'mcdi-maroon': 'var(--mcdi-maroon)',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
'mega': ['120px', {
|
||||||
|
lineHeight: '1',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
fontWeight: '700',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
'xs': '375px',
|
||||||
|
'sm': '640px',
|
||||||
|
'md': '768px',
|
||||||
|
'lg': '1024px',
|
||||||
|
'xl': '1280px',
|
||||||
|
'2xl': '1536px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
38
tsconfig.json
Normal file
38
tsconfig.json
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}, "include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue