Improve mobile and small screen experiences

This commit is contained in:
TheMaddax 2024-12-10 19:42:20 -06:00
parent 185c70e76e
commit 195aeea56b
7 changed files with 257 additions and 272 deletions

View file

@ -1,110 +0,0 @@
#!/bin/bash
cat > README.md << 'EOL'
# DeafGain LLC Website 🌐
[![PNPM](https://img.shields.io/badge/package%20manager-pnpm-f69220.svg)](https://pnpm.io/)
[![TypeScript](https://img.shields.io/badge/TypeScript-4.9.5-blue.svg)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18.2.0-61dafb.svg)](https://reactjs.org/)
## Overview
A dynamic web platform dedicated to empowering the Deaf community through innovative communication solutions, training, and accessibility services. This website serves as the digital presence for DeafGain LLC, showcasing our comprehensive services including ADA-compliant transcripts, universal website design, board communication protocols, and leadership training.
## 🚀 Quick Start
\`\`\`bash
pnpm install
pnpm dev
\`\`\`
Visit \`localhost:3000\` in your browser.
## 🛠 Technical Stack
| Technology | Version | Purpose |
|------------|---------|---------|
| React | 18.2.0 | Core Framework |
| TypeScript | 4.9.5 | Type Safety |
| Tailwind CSS | 3.x | Styling |
| Framer Motion | 10.x | Animations |
| React Simple Maps | 3.0.0 | Geographic Visualization |
| Vite | 4.x | Build System |
| Docker | - | Containerization |
| Nginx | - | Web Server |
## 📁 Project Structure
\`\`\`
src/
├── components/ # Reusable UI components
│ ├── layout/ # Layout components
│ ├── ui/ # UI elements
│ └── maps/ # Map visualization components
├── pages/ # Main application pages
├── hooks/ # Custom React hooks
├── utils/ # Utility functions
├── types/ # TypeScript definitions
├── assets/ # Static assets
└── styles/ # Global styles
\`\`\`
## 🎯 Core Features
- ⚡️ Intuitive user interface with responsive design
- 🗺️ Interactive USA region map displaying network and relationships
- 🎥 Video resources with accessible playback controls and captions
- 📱 Mobile-first responsive design
- 🔒 Secure contact system
- 🌐 Key sections: Home, Services, About, Resources, and Contact
## 🔧 Development
### Environment Setup
- Development: Vite dev server with hot reload
- Production: Nginx configuration with optimized cache settings
- Docker containers for consistent deployment
### Scripts
\`\`\`bash
pnpm dev # Start development server
pnpm build # Build for production
pnpm preview # Preview production build
pnpm lint # Run ESLint
pnpm test # Run tests
\`\`\`
## 🚢 Production Deployment
\`\`\`bash
docker-compose up --build
\`\`\`
Application will be served on port 80.
## <20><> Contributing
We welcome contributions that align with our mission of improving accessibility and empowerment for the Deaf community. Follow our established patterns for:
- Component architecture
- Styling conventions (Tailwind CSS)
- Accessibility standards (WCAG 2.1)
- TypeScript typing
- Git commit messages
## 📄 License
Proprietary - DeafGain LLC
## 📞 Contact
For questions or support, reach out through our [contact form](https://deafgain.com/contact).
---
> This website represents DeafGain LLC's commitment to creating accessible, user-friendly digital experiences while maintaining high technical standards and professional quality.
EOL
chmod +x create-readme.sh
echo "README.md has been created successfully!"

View file

@ -1,46 +0,0 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
pull_policy: build
ports:
- "804:804"
depends_on:
- api
volumes:
- ./logs:/var/log/nginx
- type: bind
source: /docker/websites/deafgain/video_content
target: /usr/share/nginx/html/videos
environment:
- NODE_ENV=production
- PORT=804
restart: unless-stopped
networks:
- caddy_network
api:
build:
context: .
dockerfile: Dockerfile.api
volumes:
- ./logs:/app/logs
environment:
- NODE_ENV=production
- PORT=3000
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_SECURE=${SMTP_SECURE}
- RECIPIENT_EMAIL=${RECIPIENT_EMAIL}
- GOOGLE_EMAIL=${GOOGLE_EMAIL}
- GOOGLE_APP_PASSWORD=${GOOGLE_APP_PASSWORD}
- UPSTASH_REDIS_REST_URL=${UPSTASH_REDIS_REST_URL}
- UPSTASH_REDIS_REST_TOKEN=${UPSTASH_REDIS_REST_TOKEN}
restart: unless-stopped
networks:
- caddy_network
networks:
caddy_network:
external: true

View file

@ -1,10 +1,12 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { useState, useEffect, useRef } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { HiMenu, HiX } from 'react-icons/hi'
const Navbar = () => {
const [isOpen, setIsOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const location = useLocation()
const navItems = [
{ name: 'Home', path: '/' },
@ -14,11 +16,31 @@ const Navbar = () => {
{ name: 'Contact', path: '/contact' }
]
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// Close mobile menu when route changes
useEffect(() => {
setIsOpen(false)
}, [location])
return (
<nav className="bg-primary text-accent-snow">
<nav className="bg-primary text-accent-snow relative z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-24">
<Link to="/" className="font-black text-3xl tracking-wider text-accent-snow drop-shadow-lg">
<Link
to="/"
className="font-black text-2xl sm:text-3xl tracking-wider text-accent-snow drop-shadow-lg"
aria-label="DeafGain Home"
>
DeafGain
</Link>
@ -29,7 +51,11 @@ const Navbar = () => {
<Link
key={item.name}
to={item.path}
className="text-xl font-bold tracking-wide text-accent-snow hover:text-accent-lake transition-colors drop-shadow-md"
className={`text-lg font-bold tracking-wide transition-colors drop-shadow-md
${location.pathname === item.path
? 'text-accent-lake'
: 'text-accent-snow hover:text-accent-lake'}`}
aria-current={location.pathname === item.path ? 'page' : undefined}
>
{item.name}
</Link>
@ -38,38 +64,51 @@ const Navbar = () => {
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="text-accent-snow"
>
{isOpen ? <HiX size={24} /> : <HiMenu size={24} />}
</button>
</div>
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden inline-flex items-center justify-center p-2 rounded-md
text-accent-snow hover:text-accent-lake hover:bg-secondary/20
transition-colors duration-200"
aria-expanded={isOpen}
aria-controls="mobile-menu"
aria-label={isOpen ? "Close menu" : "Open menu"}
>
{isOpen ? <HiX size={28} /> : <HiMenu size={28} />}
</button>
</div>
</div>
{/* Mobile Navigation */}
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="md:hidden"
>
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
{navItems.map((item) => (
<Link
key={item.name}
to={item.path}
className="block px-3 py-2 hover:bg-secondary rounded-md"
onClick={() => setIsOpen(false)}
>
{item.name}
</Link>
))}
</div>
</motion.div>
)}
<AnimatePresence>
{isOpen && (
<motion.div
ref={menuRef}
id="mobile-menu"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="md:hidden absolute w-full bg-primary shadow-lg"
>
<div className="px-4 pt-2 pb-3 space-y-1">
{navItems.map((item) => (
<Link
key={item.name}
to={item.path}
className={`block px-3 py-4 text-base font-medium rounded-md
transition-colors duration-200
${location.pathname === item.path
? 'bg-secondary text-accent-lake'
: 'text-accent-snow hover:bg-secondary/20 hover:text-accent-lake'}`}
aria-current={location.pathname === item.path ? 'page' : undefined}
>
{item.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
)
}

View file

@ -2,6 +2,7 @@
import { motion, AnimatePresence } from 'framer-motion'
import { FaTimes } from 'react-icons/fa'
import { useEffect } from 'react'
interface ToastProps {
message: string
@ -15,30 +16,49 @@ export default function Toast({ message, type, isVisible, onClose }: ToastProps)
? 'from-primary to-secondary'
: 'from-red-600 to-red-800'
// Auto-dismiss after 5 seconds
useEffect(() => {
if (isVisible) {
const timer = setTimeout(onClose, 5000)
return () => clearTimeout(timer)
}
}, [isVisible, onClose])
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center"
className="fixed inset-0 z-50 px-4 py-6 pointer-events-none sm:p-6 sm:items-start flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
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"
initial={{ opacity: 0, scale: 0.9, y: 50 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 50 }}
className="w-full max-w-sm pointer-events-auto"
>
<div className={`h-2 bg-gradient-to-r ${bgColor}`} />
<div className="p-6 relative">
<button
onClick={onClose}
className="absolute top-2 right-2 text-accent-mountain hover:text-secondary p-2 rounded-full transition-colors"
>
<FaTimes size={24} />
</button>
<p className="text-lg font-medium text-accent-mountain text-center mt-2">{message}</p>
<div className="overflow-hidden rounded-lg shadow-2xl">
<div className={`h-2 bg-gradient-to-r ${bgColor}`} />
<div className="p-4 bg-white relative">
<div className="flex items-start">
<div className="flex-1 pt-0.5">
<p className="text-sm sm:text-base text-accent-mountain text-center pr-8">
{message}
</p>
</div>
<div className="ml-4 flex-shrink-0 flex">
<button
onClick={onClose}
className="inline-flex text-accent-mountain rounded-md focus:outline-none focus:ring-2 focus:ring-primary hover:text-secondary transition-colors p-1.5"
aria-label="Close notification"
>
<FaTimes className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>

View file

@ -15,9 +15,7 @@ const Contact = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (isSubmitting) return
setIsSubmitting(true)
try {
@ -49,12 +47,12 @@ const Contact = () => {
const contactInfo = [
{
icon: <FaEnvelope />,
icon: <FaEnvelope className="text-2xl sm:text-3xl" />,
title: 'Email',
content: 'contact@deafgain.com'
},
{
icon: <FaMapMarkerAlt />,
icon: <FaMapMarkerAlt className="text-2xl sm:text-3xl" />,
title: 'Location',
content: 'Montana, United States'
}
@ -62,19 +60,20 @@ const Contact = () => {
return (
<div className="min-h-screen bg-accent-snow">
<div className="max-w-7xl mx-auto px-4 py-20">
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
className="text-center mb-8 sm:mb-16"
>
<h1 className="text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
<p className="text-accent-mountain text-lg">
<h1 className="text-3xl sm:text-4xl font-bold text-secondary mb-4">Get in Touch</h1>
<p className="text-accent-mountain text-base sm:text-lg max-w-2xl mx-auto px-4">
We're here to help and answer any questions you might have
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8 mb-16 max-w-2xl mx-auto">
<div className="grid sm:grid-cols-2 gap-6 mb-12 max-w-2xl mx-auto">
{contactInfo.map((info, index) => (
<motion.div
key={index}
@ -83,21 +82,22 @@ const Contact = () => {
transition={{ delay: index * 0.2 }}
className="bg-white p-6 rounded-lg shadow-lg text-center"
>
<div className="text-primary text-3xl mb-4">{info.icon}</div>
<h3 className="text-xl font-semibold mb-2">{info.title}</h3>
<div className="text-primary mb-4">{info.icon}</div>
<h3 className="text-lg sm:text-xl font-semibold mb-2">{info.title}</h3>
<p className="text-accent-mountain">{info.content}</p>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-8"
className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 sm:p-8"
>
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label htmlFor="name" className="block text-accent-mountain mb-2">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-accent-mountain mb-2 text-sm sm:text-base">
Name
</label>
<input
@ -105,15 +105,16 @@ const Contact = () => {
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
text-base sm:text-lg"
required
minLength={2}
maxLength={50}
/>
</div>
<div className="mb-6">
<label htmlFor="email" className="block text-accent-mountain mb-2">
<div>
<label htmlFor="email" className="block text-accent-mountain mb-2 text-sm sm:text-base">
Email
</label>
<input
@ -121,21 +122,23 @@ const Contact = () => {
id="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
text-base sm:text-lg"
required
pattern="[^@\s]+@[^@\s]+\.[^@\s]+"
/>
</div>
<div className="mb-6">
<label htmlFor="service" className="block text-accent-mountain mb-2">
<div>
<label htmlFor="service" className="block text-accent-mountain mb-2 text-sm sm:text-base">
Service Interested In
</label>
<select
id="service"
value={formData.service}
onChange={(e) => setFormData({ ...formData, service: e.target.value })}
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary"
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
text-base sm:text-lg bg-white"
required
>
<option value="">Select a service</option>
@ -146,15 +149,16 @@ const Contact = () => {
</select>
</div>
<div className="mb-6">
<label htmlFor="message" className="block text-accent-mountain mb-2">
<div>
<label htmlFor="message" className="block text-accent-mountain mb-2 text-sm sm:text-base">
Message
</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full px-4 py-2 border border-accent-lake rounded-lg focus:outline-none focus:border-primary h-32"
className="w-full px-4 py-3 border border-accent-lake rounded-lg focus:outline-none focus:border-primary
text-base sm:text-lg min-h-[120px] sm:min-h-[150px]"
required
minLength={10}
maxLength={1000}
@ -166,11 +170,11 @@ const Contact = () => {
whileTap={{ scale: 0.98 }}
type="submit"
disabled={isSubmitting}
className={`w-full py-3 rounded-lg font-semibold transition-colors ${
isSubmitting
className={`w-full py-4 rounded-lg font-semibold text-base sm:text-lg transition-colors
${isSubmitting
? 'bg-accent-mountain text-accent-snow cursor-not-allowed'
: 'bg-primary text-accent-snow hover:bg-secondary'
}`}
: 'bg-primary text-accent-snow hover:bg-secondary active:bg-secondary'
}`}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</motion.button>

View file

@ -20,7 +20,11 @@ interface Category {
videos: Video[]
}
const ITEMS_PER_PAGE = 6
const ITEMS_PER_PAGE = {
mobile: 3,
tablet: 4,
desktop: 6
}
const resourceCategories: Category[] = [
{
@ -73,9 +77,22 @@ const Resources = () => {
const [currentPage, setCurrentPage] = useState<Record<string, number>>({})
const [showTranscript, setShowTranscript] = useState(false)
const [transcriptContent, setTranscriptContent] = useState('')
const [screenWidth, setScreenWidth] = useState(window.innerWidth)
const popupRef = useRef<HTMLDivElement>(null)
const transcriptRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleResize = () => setScreenWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const getItemsPerPage = () => {
if (screenWidth < 640) return ITEMS_PER_PAGE.mobile
if (screenWidth < 1024) return ITEMS_PER_PAGE.tablet
return ITEMS_PER_PAGE.desktop
}
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
@ -102,59 +119,66 @@ const Resources = () => {
}
}
const getPageCount = (videoCount: number) => Math.ceil(videoCount / ITEMS_PER_PAGE)
const getPageCount = (videoCount: number) => Math.ceil(videoCount / getItemsPerPage())
const getCurrentPageVideos = (videos: Video[], categoryId: string) => {
const page = currentPage[categoryId] || 1
const start = (page - 1) * ITEMS_PER_PAGE
return videos.slice(start, start + ITEMS_PER_PAGE)
const itemsPerPage = getItemsPerPage()
const start = (page - 1) * itemsPerPage
return videos.slice(start, start + itemsPerPage)
}
return (
<div className="min-h-screen bg-accent-snow">
<div className="max-w-7xl mx-auto px-4 py-20">
<h1 className="text-4xl font-bold text-secondary mb-16 text-center">
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-20">
<h1 className="text-3xl sm:text-4xl font-bold text-secondary mb-12 sm:mb-16 text-center">
Resource Library
</h1>
{resourceCategories.map((category) => (
<div key={category.id} className="mb-20">
<div key={category.id} className="mb-16">
<div className="mb-8">
<h2 className="text-2xl font-bold text-secondary mb-2">{category.title}</h2>
<h2 className="text-xl sm:text-2xl font-bold text-secondary mb-2">{category.title}</h2>
<p className="text-accent-mountain">{category.description}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{getCurrentPageVideos(category.videos, category.id).map((video) => (
<div key={video.id} className="group">
<div className="relative aspect-video overflow-hidden rounded-lg shadow-lg">
<motion.div
key={video.id}
layout
className="group bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300"
>
<div className="relative aspect-video">
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute inset-0 flex items-center justify-center gap-4">
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-4">
<button
onClick={() => setSelectedVideo(video)}
className="bg-primary/90 hover:bg-primary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
aria-label={`Watch ${video.title}`}
>
<FaVideo /> Watch
</button>
{video.transcriptSrc && (
<button
onClick={() => setSelectedVideo(video)}
className="bg-primary/90 hover:bg-primary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
onClick={() => fetchTranscript(video.id)}
className="bg-secondary/90 hover:bg-secondary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
aria-label={`Read transcript of ${video.title}`}
>
<FaVideo /> Watch Video
<FaBookReader /> Transcript
</button>
{video.transcriptSrc && (
<button
onClick={() => fetchTranscript(video.id)}
className="bg-secondary/90 hover:bg-secondary text-white px-4 py-2 rounded-full flex items-center gap-2 transition-colors"
>
<FaBookReader /> Read Transcript
</button>
)}
</div>
)}
</div>
</div>
<h3 className="mt-4 text-lg font-semibold text-secondary">{video.title}</h3>
<p className="text-sm text-accent-mountain">{video.description}</p>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-secondary mb-2">{video.title}</h3>
<p className="text-sm text-accent-mountain line-clamp-2">{video.description}</p>
</div>
</motion.div>
))}
</div>
@ -164,11 +188,13 @@ const Resources = () => {
<button
key={idx}
onClick={() => setCurrentPage({ ...currentPage, [category.id]: idx + 1 })}
className={`w-10 h-10 rounded-full flex items-center justify-center ${
(currentPage[category.id] || 1) === idx + 1
className={`w-10 h-10 rounded-full flex items-center justify-center transition-colors
${(currentPage[category.id] || 1) === idx + 1
? 'bg-primary text-white'
: 'bg-white text-primary border border-primary'
}`}
: 'bg-white text-primary border border-primary hover:bg-primary/10'
}`}
aria-label={`Page ${idx + 1}`}
aria-current={(currentPage[category.id] || 1) === idx + 1 ? 'page' : undefined}
>
{idx + 1}
</button>
@ -181,12 +207,13 @@ const Resources = () => {
{/* Video Modal */}
{selectedVideo && (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50">
<div className="w-4/5 max-w-6xl" ref={popupRef}>
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="w-full max-w-6xl" ref={popupRef}>
<div className="relative">
<button
onClick={() => setSelectedVideo(null)}
className="absolute -top-12 right-0 text-white hover:text-accent-lake bg-black/50 p-2 rounded-full transition-colors"
aria-label="Close video"
>
<FaTimes className="w-6 h-6" />
</button>
@ -207,13 +234,13 @@ const Resources = () => {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50"
className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4"
>
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 50, opacity: 0 }}
className="w-4/5 max-w-4xl bg-white rounded-xl shadow-2xl"
className="w-full max-w-4xl bg-white rounded-xl shadow-2xl"
ref={transcriptRef}
>
<div className="p-6 flex justify-between items-center border-b border-gray-200">
@ -221,11 +248,12 @@ const Resources = () => {
<button
onClick={() => setShowTranscript(false)}
className="text-accent-mountain hover:text-secondary transition-colors bg-accent-snow/50 p-2 rounded-full"
aria-label="Close transcript"
>
<FaTimes size={24} />
</button>
</div>
<div className="p-6 max-h-[70vh] overflow-y-auto prose prose-slate">
<div className="p-6 max-h-[60vh] overflow-y-auto prose prose-slate">
{transcriptContent.split('\n\n').map((paragraph, index) => (
<p key={index} className="mb-4 text-accent-mountain leading-relaxed">
{paragraph}

View file

@ -5,6 +5,14 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
screens: {
'xs': '375px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
extend: {
colors: {
'primary': '#647C90', // Slate blue
@ -14,7 +22,49 @@ export default {
'mountain': '#4A5568', // Mountain gray
'lake': '#A4C3D2', // Alpine lake blue
}
}
},
fontSize: {
'xs': ['0.75rem', { lineHeight: '1rem' }],
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
'base': ['1rem', { lineHeight: '1.5rem' }],
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
},
spacing: {
'18': '4.5rem',
'22': '5.5rem',
},
maxWidth: {
'xxs': '16rem',
'mobile': '20rem',
},
minHeight: {
'screen-75': '75vh',
},
padding: {
'safe': 'env(safe-area-inset-bottom)',
},
zIndex: {
'60': '60',
'70': '70',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.4s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
plugins: [],