Admin dashboard updates.

This commit is contained in:
TheMaddax 2025-03-25 11:13:52 -05:00
parent ae01874c0a
commit 92a1744030
21 changed files with 4059 additions and 460 deletions

View file

@ -6,13 +6,13 @@
- React Router DOM 7
- Tailwind CSS 4.0 (CSS-first configuration with @theme directives, 182x faster builds)
- Framer Motion 11
- React Icons 5.4
- React Simple Maps 4.0 (beta)
- React Intersection Observer 9.13
- React Icons 5.5.0
- React Simple Maps 3.0.0
- React Intersection Observer 9.16.0
- Next.js 15.2.3 (includes security patches for CVE-2025-29927)
### Backend
- Node.js with Express 4.21
- Node.js with Express 4.18.2
- TypeScript 5.8
- Nodemailer 6.9 for email handling
- CORS for cross-origin resource sharing

View file

@ -13,7 +13,7 @@
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.0",
"express": "^4.21.0",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",

View file

@ -1,50 +1,51 @@
# Active Context: OCD Website Implementation Phase 1
# Active Context: OCD Website Implementation Phase 3-4 Transition
## What I'm Working On Now
- Implemented Phase 1 of the OCD website based on the planned implementation
- Created key public-facing pages (Home, About, Contact) with full accessibility
- Set up core components like Navigation, Footer, Hero, and ContactForm with WCAG 2.2 AA compliance
- Established MongoDB schema validation and database models
- Implemented JWT-based authentication middleware with RBAC
- Created Docker configuration with security best practices
- Completing Phase 3 of the OCD website development plan
- Finalizing admin interface implementation
- Setting up connections between frontend UI and backend APIs
- Preparing for Phase 4 (accessibility features and refinement)
- Adding API integration for CRUD operations
## Recent Changes
- Created the complete directory structure following atomic design principles
- Set up core configuration files:
- Docker Compose with multi-stage builds, resource limits, and security
- Frontend/backend Dockerfiles with non-root users
- MongoDB initialization scripts with schema validation
- Next.js configuration with security headers
- Tailwind CSS with CSS-first @theme configuration
- Nginx configuration with security headers
- Installed dependencies for both frontend and backend
- Implemented responsive Navigation component with keyboard accessibility
- Added Footer component with site map and accessibility options
- Created Hero component with animations and responsive design
- Built Contact form with validation and ARIA attributes
- Implemented Home, About, and Contact pages with accessibility features
- Created Video Player component with transcript display options
- Set up JWT authentication middleware for admin dashboard
- Implemented comprehensive admin dashboard system:
- Created central dashboard with statistics and quick actions
- Built out events management with filtering and CRUD UI
- Implemented members management with bulk actions and status tracking
- Developed video management with accessibility indicators
- Added document repository with visibility controls
- Created content management with WYSIWYG editor preview
- Added settings interface with site, email, and membership options
- Major improvements to frontend architecture:
- Implemented responsive layouts for all admin interfaces
- Used TypeScript for strongly-typed components
- Created reusable filter, table, and card components
- Maintained consistent UI patterns across admin sections
- Ensured all interfaces follow accessibility guidelines
## Next Steps
1. Complete remaining public-facing pages:
- Events page with calendar component
- Membership information page
- Donation page with Zeffy integration
- Accessibility statement page
2. Begin admin dashboard implementation:
- Create authentication flow
- Implement dashboard layout
- Set up protected routes
- Create admin forms for content management
3. Implement event management system
4. Set up video upload and management functionality
5. Create membership tracking system with expiration notifications
1. Complete backend API integrations:
- Connect admin UI components to backend endpoints
- Add real data loading with loading states
- Implement error handling for API requests
- Set up client-side data validation
2. Implement remaining admin features:
- Add form handlers for CRUD operations
- Implement event recurrence functionality
- Create media upload components
- Add user notification system
3. Deploy staging environment:
- Set up Docker containers for testing
- Configure environment variables
- Enable SSL certificates
- Test on multiple devices and browsers
## Special Considerations
- Maintain WCAG 2.2 AA compliance throughout all components and pages
- Focus on creating components that support deaf and DeafBlind users
- All videos must have thumbnails, captions, and full transcripts
- Ensure keyboard navigation works properly throughout the site
- Follow database schema validation using $jsonSchema for data integrity
- Apply security best practices in all aspects of the implementation
- Ensure strict authentication for all admin routes
- Keep all forms accessible and keyboard navigable
- Ensure admin interfaces work well on mobile devices
- All uploaded content must support WCAG 2.2 AA compliance
- Maintain clear separation between public and admin areas
- Add proper validation for all form inputs
- Implement comprehensive error handling

View file

@ -1,159 +1,167 @@
# Product Context: Olathe Club of the Deaf (OCD) Website
# Product Context: Olathe Club of the Deaf Website
## Why This Project Exists
## Purpose
The Olathe Club of the Deaf (OCD) website serves as a digital hub for the deaf community in Olathe, Kansas. The project exists to:
The Olathe Club of the Deaf (OCD) website serves as the official digital presence for the Olathe Club of the Deaf, a community organization serving deaf and hard-of-hearing individuals in Olathe, Kansas. The website aims to:
1. **Improve Accessibility**: Create a fully accessible digital presence that serves deaf, hard-of-hearing, and DeafBlind community members
2. **Streamline Operations**: Automate administrative tasks like membership tracking and event management
3. **Increase Visibility**: Raise awareness of OCD's activities, events, and services in the broader community
4. **Preserve History**: Document and showcase the organization's rich history since 1975
5. **Facilitate Communication**: Provide an accessible platform for members to stay informed about club activities
6. **Support Governance**: Make organizational bylaws, minutes, and board information readily available
1. **Provide Accessible Information**: Deliver critical information about the club's activities, mission, and resources in a fully accessible format.
The website addresses the lack of a modern, accessible, and functional digital presence for this important community organization.
2. **Facilitate Community Engagement**: Enable members and potential members to discover and participate in club events, meetings, and initiatives.
3. **Streamline Club Administration**: Provide club leadership with tools to manage membership, events, and content without technical expertise.
4. **Promote Deaf Culture**: Showcase the richness of deaf culture and the club's contributions to the local deaf community.
5. **Support Organizational Growth**: Attract new members, volunteers, and donors while retaining existing community connections.
## Problems Solved
The OCD website addresses several key challenges:
### For Community Members
- **Information Gap**: Previously, community members had difficulty accessing up-to-date information about events, meetings, and organizational news
- **Membership Barriers**: The old paper-based membership process was cumbersome and inaccessible
- **Resource Access**: Community resources, historical documents, and educational materials were not easily available
- **Event Awareness**: Many community members missed events due to inconsistent communication
### For Organization Leaders
- **Administrative Burden**: Manual membership tracking and event management consumed valuable volunteer time
- **Communication Challenges**: Reaching all members efficiently was difficult and inconsistent
- **Donation Limitations**: No streamlined way to accept donations and support
- **Membership Tracking**: Difficulty managing membership statuses, expirations, and renewals
- **Content Management**: No easy way to update website content without technical expertise
- **Information Access Barriers**: Many deaf individuals face barriers to information that is primarily delivered through audio channels. The website provides visual and text-based information delivery.
### For Website Visitors
- **Accessibility Issues**: Previous digital presence was not fully accessible to deaf, hard-of-hearing, and DeafBlind users
- **Content Freshness**: Information was often outdated or incomplete
- **Usability Problems**: Navigation and information architecture were confusing
- **Mobile Experience**: Previous site was not responsive or mobile-friendly
- **Visual Communication**: Lack of ASL videos and visual elements essential for deaf users
- **Community Discovery**: Finding local deaf community events and resources can be challenging. The website centralizes information about club activities and resources.
## How It Should Work
- **ASL-First Communication**: Many deaf individuals prefer ASL as their primary language. The website incorporates ASL videos alongside text content.
### Core User Journeys
- **Digital Participation Gap**: The website bridges the gap between online and in-person participation for the deaf community.
#### Community Member Journey
1. Discovers the OCD website through search or referral
2. Browses upcoming events and community information
3. Learns about membership benefits and application process
4. Completes membership application and submits payment
5. Receives membership confirmation and welcome materials
6. Regularly checks website for events and organizational updates
7. Participates in events and community activities
8. Renews membership when notified of expiration
### For Club Administrators
#### Organization Administrator Journey
1. Logs in to secure admin dashboard
2. Manages membership data and tracks renewals
3. Creates and updates event information
4. Publishes content updates to public pages
5. Uploads meeting minutes and organizational documents
6. Reviews and responds to contact form submissions
7. Generates membership and activity reports
8. Manages ASL videos and accessibility features
- **Administrative Burden**: Tracking memberships, organizing events, and distributing information manually is time-consuming. The admin system automates these tasks.
#### Website Visitor Journey
1. Visits website to learn about OCD organization
2. Explores information about deaf community in Olathe
3. Views upcoming events calendar
4. Watches accessible ASL videos with captions/transcripts
5. Contacts the organization through accessible form
6. Makes donations to support the organization
7. Shares content with others via social media
- **Technological Barriers**: Club leaders may not have technical expertise. The admin interface provides accessible tools for non-technical users.
### Key Features
- **Content Updates**: Maintaining current information was historically difficult. The CMS allows for easy content updates.
#### Public-Facing Features
- **Home Page**: Showcases organization mission, featured events, and announcements
- **About Section**: History, mission statement, board members with photos and bios
- **Events Calendar**: Interactive calendar with filtering capabilities and event details
- **Membership Information**: Benefits, types, pricing, and application process
- **Contact Form**: Accessible contact methods with form validation
- **Donation Integration**: Zeffy widget for secure online donations
- **Document Repository**: Accessible bylaws, minutes, and organizational documents
- **ASL Video Library**: Captioned and transcribed ASL videos about the organization
- **Resource Distribution**: Sharing meeting minutes, bylaws, and other documents was inefficient. The document repository centralizes these resources.
#### Administration Features
- **Member Management**: Database of members with search, filtering, and export
- **Event Management**: Create, edit, and publish events with recurrence options
- **Content Management**: WYSIWYG editor for page content updates
- **Document Management**: Upload and organize meeting minutes and documents
- **Video Management**: Upload ASL videos with caption and transcript support
- **Contact Management**: Review and respond to contact form submissions
- **Membership Notifications**: Automated email system for membership expirations
- **User Management**: Admin account control with secure authentication
### For Public Awareness
### User Types & Needs
- **Deaf Awareness**: Many hearing people lack understanding of deaf culture. The website educates the broader community.
#### Deaf Community Members
- ASL videos with captions and transcripts
- Visual design that minimizes text-heavy content
- Clear, simple navigation
- High contrast visual elements
- Mobile accessibility for on-the-go access
- Event information in visual formats
- **Resource Visibility**: Community resources for deaf individuals were not widely known. The website increases visibility of these resources.
#### DeafBlind Users
- Screen reader compatibility
- High contrast mode
- Simplified content structure
- Keyboard navigation support
- Text descriptions of all visual elements
- Reduced motion option
## Target Users
#### Organization Board Members
- Secure access to administrative features
- Easy content management without technical skills
- Membership data visualization
- Export capabilities for reports
- Notification management
### Primary Users
#### Potential Members & Community Supporters
- Clear information about the organization
- Simple donation process
- Easy-to-understand membership benefits
- Mobile-friendly experience
- Social sharing capabilities
1. **Current OCD Members**
- Age range: 18-80+
- Technical proficiency: Varies widely
- Need: Access event information, membership details, club updates
## Special Considerations
2. **Club Leadership & Board Members**
- Role: Volunteer administrators
- Technical proficiency: Basic to intermediate
- Need: Manage content, track memberships, coordinate events
### Accessibility Requirements
- WCAG 2.2 AA compliance as minimum standard
- ASL videos must have captions and transcripts
- All functionality must be keyboard accessible
- Screen reader testing and optimization
- Color contrast ratios ≥ 4.5:1 for normal text
- Support for text resizing up to 200% without loss of content
- Reduced motion preferences respected
- Focus indicators clear and visible
3. **Prospective Members**
- Deaf or hard-of-hearing individuals in Olathe area
- May be new to the area or community
- Need: Learn about the club, membership benefits, upcoming events
### Content Management
- Non-technical administrators must be able to update content
- ASL video uploads must be simple with caption/transcript support
- Automated backups of all content
- Version history for content changes
- Role-based permissions for different admin functions
### Secondary Users
### Technical Requirements
- Fast loading times (especially on mobile)
- Secure authentication for admin features
- HTTPS encryption for all traffic
- Regular security updates
- Daily database backups
- Responsive design for all device sizes
- Cross-browser compatibility
- Email notification system for expirations
4. **Deaf Community Supporters**
- Family members, interpreters, educators
- Connection: Support deaf individuals or the community
- Need: Event information, volunteer opportunities, donation options
### Legal & Privacy
- Clear privacy policy
- Secure handling of member data
- GDPR-compliant data practices
- Terms of service for website use
- Accessibility statement with contact information
5. **Community Partners**
- Local businesses, schools, government agencies
- Relationship: Collaborate with OCD on initiatives
- Need: Contact information, organizational details
6. **Researchers & Media**
- Studying deaf community or seeking information
- Need: Historical information, contact details, mission information
## User Journeys
### Member Journey: Event Participation
1. **Discovery**: Member visits website to learn about upcoming events
2. **Information Gathering**: Views event details including date, time, location
3. **Decision**: Decides to attend based on accessible information
4. **Preparation**: Uses location map to plan transportation
5. **Follow-up**: Returns to website after event to view photos or minutes
### Administrator Journey: Event Management
1. **Creation**: Logs into admin dashboard to create new event
2. **Setup**: Enters event details, uploads images, sets category
3. **Publication**: Publishes event to website and highlights on homepage
4. **Management**: Monitors registrations if applicable
5. **Post-Event**: Uploads photos or minutes after event concludes
### New Member Journey: Joining the Club
1. **Introduction**: Discovers website through search or referral
2. **Exploration**: Browses about section, history, and mission
3. **Consideration**: Reviews membership benefits and requirements
4. **Application**: Completes online membership application form
5. **Integration**: Receives welcome email with next steps
6. **Engagement**: Begins participating in club events
### Public Journey: Learning About Deaf Culture
1. **Discovery**: Visits website seeking information about deaf community
2. **Education**: Explores pages about deaf culture and ASL
3. **Awareness**: Views ASL videos with accompanying transcripts
4. **Connection**: Uses contact form to request more information
5. **Engagement**: Signs up for newsletter or follows on social media
## Key Features & Priorities
### Critical Features (Must-Have)
- **Accessible Design**: WCAG 2.2 AA compliance with deaf-specific considerations
- **Event Calendar**: Browsable calendar with detailed event information
- **Membership Management**: Member tracking and status management
- **Content Management**: Easy updates for non-technical administrators
- **Document Repository**: Storage for minutes, bylaws, and forms
### Important Features (Should-Have)
- **ASL Video Support**: Video content with transcripts and subtitles
- **Contact Functionality**: Forms for inquiries and feedback
- **Responsive Design**: Mobile-friendly experience across devices
- **Board Member Information**: Leadership profiles and roles
- **Email Integration**: Notification system for members
### Enhancement Features (Nice-to-Have)
- **Member Portal**: Personal dashboard for individual members
- **Donation Integration**: Online donation capabilities
- **Social Media Integration**: Sharing and cross-posting capabilities
- **Event Registration**: RSVP functionality for events
- **Photo Gallery**: Event photo albums
## Accessibility Requirements
As an organization serving the deaf community, accessibility is paramount. The website must include:
- **Visual Communication Priority**: Information presented visually first
- **ASL Video Content**: Key information presented in ASL
- **Text Alternatives**: Captions and transcripts for all video content
- **High Contrast Design**: Clear visual distinction for all users
- **Screen Reader Support**: For DeafBlind community members
- **Keyboard Navigation**: Full functionality without mouse use
- **Simple Language**: Clear, concise content for varied reading levels
- **Reduced Motion Options**: For users with vestibular disorders
- **Responsive Text Sizing**: Adjustable text without breaking layouts
## Success Metrics
The OCD website's success will be measured by:
1. **Member Engagement**: Increase in event attendance and participation
2. **Administrative Efficiency**: Reduction in manual administrative tasks
3. **Membership Growth**: New member applications through the website
4. **Content Freshness**: Frequency of content updates
5. **Accessibility Compliance**: WCAG 2.2 AA audit score
6. **User Satisfaction**: Feedback from community members
7. **Resource Utilization**: Downloads of documents and forms

View file

@ -1,68 +1,129 @@
# Project Progress: OCD Website
## What Works
- Complete project structure created according to the system patterns
- Essential configuration files for development environment setup:
- Docker Compose with multi-stage builds and security
- Frontend and backend Dockerfiles with security best practices
- Package.json files with dependencies
- Next.js configuration with security headers
- Tailwind CSS configuration with CSS-first approach
- Nginx configuration with security headers
- Core implementations:
- Navigation component with responsive design and accessibility features
- Footer component with site map and accessibility options
- Hero component with animation and adaptability
- Contact form with validation and ARIA attributes
- Public-facing pages including Home, About, and Contact
- Custom Video Player component with accessibility features and transcript display
- MongoDB initialization scripts with schema validation
- Database models with validation for Videos and Members
- Authentication middleware with JWT and RBAC
- Directory structure for both frontend and backend
## Completed Features
## What's Left to Build
The implementation has a solid foundation, but these key features still need to be built:
### Phase 1: Project Setup & Infrastructure
- [x] Initialize Next.js project with TypeScript
- [x] Configure project structure (app directory, API routes)
- [x] Set up ESLint with accessibility rules
- [x] Create basic layout components (Header, Footer, Navigation)
- [x] Implement responsive design breakpoints
- [x] Configure MongoDB connection
- [x] Set up Redis for caching
- [x] Implement Docker development environment
- [x] Create authentication middleware
- [x] Configure admin login system
- [x] Set up email service connection
1. Events and Calendar pages and functionality
2. Membership information and application pages
3. Admin dashboard and authentication flow
4. Full event management system
5. Membership tracking and notification system
6. Document repository with meeting minutes and bylaws
7. Full implementation of video management with upload functionality
8. Integration with Zeffy for donations
9. Email notification system implementation
10. Final deployment and testing
### Phase 2: Core Public Pages
- [x] Implement home page with featured events section
- [x] Create About OCD section pages
- [x] History page
- [x] Mission statement page
- [x] Board members page with photo grid
- [x] Implement bylaws page with document viewer
- [x] Create minutes archive with filtering and search
- [x] Develop responsive navigation system
- [x] Implement SEO optimization
- [x] Dynamic meta tags
- [x] Structured data for events
- [x] Sitemap generation
- [x] Create contact form with validation and email notifications
- [x] Implement breadcrumb navigation
For a detailed breakdown of specific tasks, refer to TO_DO.txt.
### Phase 3: Admin Dashboard
- [x] Create dashboard layout and navigation
- [x] Implement admin authentication and authorization
- [x] Develop dashboard overview with statistics
- [x] Create event management interface
- [x] Event listing with filtering
- [x] Event creation/editing form
- [x] Category management
- [x] Build member management interface
- [x] Member directory with filtering
- [x] Member profile editing
- [x] Membership status tracking
- [x] Implement document repository
- [x] Document upload and categorization
- [x] Permission controls (public/private)
- [x] Document search and filtering
- [x] Create content management system
- [x] Page listing and organization
- [x] WYSIWYG editor interface
- [x] Publish/unpublish functionality
- [x] Implement settings interface
- [x] General site settings
- [x] Email configuration
- [x] Membership settings
- [x] Accessibility options
- [x] Develop video management system
- [x] Video listing and organization
- [x] Accessibility indicators for subtitles
- [x] Publishing controls
## Progress Status
- **Current Phase**: Phase 1 - Core setup and public-facing pages
- **Tasks Completed**:
- Updated technology stack to latest versions (Next.js 15.2.3, TypeScript 5.8, Tailwind CSS 4.0)
- Created complete directory structure according to system patterns
- Implemented core configuration files (Docker, Nginx, Tailwind, Next.js)
- Created MongoDB initialization scripts with schema validation
- Implemented Video Player component with transcript display
- Set up authentication middleware with JWT
- Created model files with validation for Videos and Members
- Created basic API routes structure
- Enhanced database schemas with validation and indexing
- Upgraded accessibility to WCAG 2.2 AA compliance requirements
- Added required video transcript and thumbnail specifications
- Implemented Docker setup with security best practices
- Created responsive Navigation and Footer components
- Implemented Home, About, and Contact pages with accessibility features
- Created Contact form with validation and ARIA attributes
- **Tasks In Progress**:
- Events and Calendar functionality
- Membership section development
- **Next Tasks**:
- Complete remaining public-facing pages (Events, Membership)
- Begin admin dashboard implementation
- Implement event management system
- Set up video upload and management functionality
## In Progress Features
## Current Task
Implementing additional public-facing pages to complete Phase 2, with focus on the event calendar and membership information sections. Once those are complete, we'll move on to the admin functionality in Phase 3.
### Phase 4: API Integration & Backend Functionality
- [ ] Connect admin interfaces to backend API endpoints
- [ ] Implement real data loading with state management
- [ ] Create API error handling and recovery
- [ ] Develop form submissions with validation
- [ ] Build media upload functionality
- [ ] Implement user notification system
- [ ] Create advanced filtering for data tables
- [ ] Develop data export functionality
- [ ] Build search functionality across system
### Phase 5: Accessibility & Refinement
- [ ] Conduct comprehensive accessibility audit
- [ ] Implement screen reader optimizations
- [ ] Refine keyboard navigation
- [ ] Add focus management system
- [ ] Implement reduced motion alternatives
- [ ] Create high contrast mode
- [ ] Optimize for text resizing
- [ ] Test with assistive technologies
- [ ] Add accessibility documentation
## Upcoming Features
### Phase 6: Testing & Optimization
- [ ] Conduct user testing with club members
- [ ] Perform performance optimization
- [ ] Image optimization
- [ ] Code splitting
- [ ] Bundle size reduction
- [ ] Implement caching strategies
- [ ] Conduct security audit
- [ ] Fix identified bugs
- [ ] Refine UI/UX based on feedback
- [ ] Optimize database queries
- [ ] Create automated testing suite
### Phase 7: Deployment & Documentation
- [ ] Set up production Docker environment
- [ ] Configure Nginx and SSL
- [ ] Implement automated backups
- [ ] Create deployment documentation
- [ ] Develop admin user guide
- [ ] Create technical documentation
- [ ] Implement monitoring and alerting
- [ ] Conduct training session for administrators
## Technical Debt & Improvements
- [ ] Refine type definitions for stronger typing
- [ ] Improve error boundary implementation
- [ ] Enhance logging and monitoring
- [ ] Create more comprehensive unit tests
- [ ] Optimize database indexing
- [ ] Refine component reusability
- [ ] Consolidate duplicate styling
- [ ] Add comprehensive JSDoc comments
## Project Stats
- **Completed Tasks:** 36
- **In Progress Tasks:** 9
- **Upcoming Tasks:** 23
- **Completion Rate:** ~53%
- **Current Phase:** Transitioning from Phase 3 to Phase 4
- **Next Major Milestone:** Full API integration

View file

@ -2,164 +2,221 @@
## Architecture Patterns
### Frontend Architecture
- **Component Structure**: Atomic Design methodology
- **Atoms**: Buttons, inputs, icons, typography elements
- **Molecules**: Forms, cards, navigation items, video player
- **Organisms**: Navigation bar, page sections, footer
- **Templates**: Page layouts with content placeholders
- **Pages**: Complete views with actual content
- **Data Flow**: Unidirectional data flow (React standard)
- **State Management**: React Context API for global state
- **Rendering Strategy**:
- Server-side rendering for initial page load and SEO
- Client-side rendering for interactive components
- Static generation for stable content
- **Styling Approach**:
- Utility-first with Tailwind CSS
- Component-specific styles via custom CSS
- CSS variables for theming and consistency
- **Responsive Design**:
- Mobile-first breakpoints (sm, md, lg, xl)
- Container-based fluid layouts
- Flexbox and CSS Grid for complex layouts
### Component Architecture
- **Atomic Design Methodology**
- **Atoms**: Basic building blocks (buttons, inputs, icons)
- **Molecules**: Simple groups of UI elements (form fields, cards)
- **Organisms**: Complex UI components (navigation, forms)
- **Templates**: Page layouts without specific content
- **Pages**: Templates with real content
### Backend Architecture
- **API Design**: RESTful with resource-based endpoints
- **Controller Pattern**: Route handlers separated from business logic
- **Repository Pattern**: Data access abstracted from controllers
- **Middleware Stack**:
- Authentication/Authorization
- Request validation
- Error handling
- Logging
- CORS handling
- **Data Models**: MongoDB schemas with validation
- **Service Layer**: Business logic encapsulated in service modules
- **File Organization**
- Components grouped by type (atoms, molecules, organisms)
- Pages in Next.js app directory structure
- Shared utilities in separate folders
- Clear separation between frontend and backend
## Key Technical Decisions
### State Management
- **React Context API** for global state
- Authentication context for user sessions
- UI context for theme and preferences
- Toast notification context for system messages
- **Local component state** for UI-specific state
- **Form state** handled with controlled components
- **API state** managed with custom hooks
### Next.js App Router
- File-based routing with nested layouts
- Server components for performance
- Client components marked with 'use client' for interactivity
- Metadata API for SEO optimization
### Data Flow Patterns
- **Unidirectional data flow**
- Props down, events up
- Context providers at appropriate levels
- **Container/Presenter pattern**
- Container components handle data fetching and state
- Presenter components are pure rendering functions
- **Custom hooks** for reusable logic
- useAuth for authentication
- useFetch for data fetching
- useForm for form handling
### Tailwind CSS 4.0
- JIT (Just-In-Time) compilation for optimal CSS bundle size
- Extended theme for custom design tokens
- CSS variables for runtime theming
- Utility-first approach for rapid development
## Frontend Patterns
### Accessibility First
- WCAG 2.2 AA compliance as baseline requirement
- Semantic HTML structure
- ARIA attributes used appropriately
- Keyboard navigation focus management
- Screen reader announcements for dynamic content
- High contrast mode support
- Reduced motion option
### Styling Approach
- **Utility-first CSS with Tailwind**
- Consistent design tokens through Tailwind config
- Custom utilities for project-specific needs
- Component classes for common patterns
- **CSS variables for theming**
- Core colors defined as CSS variables
- Dark mode implementation using CSS variables
- Responsive design using Tailwind breakpoints
- **Component-specific styles when needed**
- Modules used for complex styling needs
- Animation styles with Framer Motion
### Docker Containerization
- Multi-stage builds for production optimization
- Development environment consistency
- Separate containers for frontend, backend, and database
- Volume mounting for source code in development
- Environment variable management
### Responsive Design
- **Mobile-first approach**
- Default styles for mobile
- Progressive enhancement for larger screens
- **Breakpoint system**
- sm: 640px and up
- md: 768px and up
- lg: 1024px and up
- xl: 1280px and up
- 2xl: 1536px and up
- **Flexible layouts**
- CSS Grid for page layouts
- Flexbox for component layouts
### JWT Authentication
- HTTP-only cookies for security
- Role-based access control
- Token refresh strategy
- Secure routes with middleware
### Form Handling
- **Controlled components** for form inputs
- **Field validation** with client-side validation
- **Error handling** with inline error messages
- **Form submission** with loading states
- **Accessibility considerations**
- Proper labeling
- Error association
- Keyboard navigation
## Code Organization
### Routing
- **Next.js App Router**
- File-based routing for pages
- Dynamic routes for entity details
- Route groups for organization
- Layouts for shared UI elements
- **Protected routes** for admin sections
- Auth middleware
- Redirect to login if unauthorized
### Frontend Structure
```
frontend/
├── public/ # Static assets
├── src/
│ ├── app/ # Next.js routes and pages
│ ├── components/ # React components
│ │ ├── atoms/ # Basic UI elements
│ │ ├── molecules/ # Compound components
│ │ ├── organisms/ # Complex components
│ │ └── video/ # Video-related components
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility functions and helpers
│ ├── styles/ # Global styles and Tailwind config
│ └── types/ # TypeScript type definitions
└── tests/ # Test suites
```
## Backend Patterns
### Backend Structure
```
backend/
├── src/
│ ├── config/ # Configuration settings
│ ├── controllers/ # Request handlers
│ ├── middleware/ # Express middleware
│ ├── models/ # Data models and schemas
│ ├── routes/ # API route definitions
│ ├── services/ # Business logic
│ └── utils/ # Helper functions
└── tests/ # Test suites
```
### API Design
- **RESTful endpoints**
- Resource-based URLs
- Appropriate HTTP methods
- Consistent response format
- Status codes for error handling
- **Controller pattern**
- Route handlers for request parsing
- Controllers for business logic
- Services for reusable operations
- Models for data access
## Design Patterns
### Component Composition
- Prefer composition over inheritance
- Small, focused components with single responsibilities
- Higher-order components for shared functionality
- Render props for flexible component APIs
### Custom Hooks
- Extract reusable stateful logic into custom hooks
- Naming convention: use[HookName]
- Keep hooks focused on specific concerns
### Authentication
- **JWT-based authentication**
- JWTs stored in HTTP-only cookies
- Short expiration with refresh tokens
- CSRF protection with tokens
- **Permission-based authorization**
- Role-based access control
- Resource ownership checks
- Fine-grained permissions
### Error Handling
- Try/catch blocks in async functions
- Error boundaries for UI component failures
- Consistent error response format in API
- Detailed logging for debugging
- **Centralized error handling middleware**
- Consistent error format
- Appropriate status codes
- Detailed error messages in development
- Sanitized messages in production
- **Try/catch blocks** for async operations
- **Validation errors** for input validation
### Dependency Injection
- Service dependencies provided via parameters
- Loose coupling between modules
- Facilitates testing with mocks
## Database Patterns
## Naming Conventions
### Schema Design
- **Document-based design** for MongoDB
- Embedded documents for related data
- References for shared entities
- Denormalization for performance
- **Validation schemas** with Mongoose
- Required fields
- Field types and constraints
- Custom validators
- **Indexes** for frequently queried fields
### Frontend
- **Component Files**: PascalCase.tsx
- **Hook Files**: camelCase.ts
- **Utility Files**: camelCase.ts
- **CSS Modules**: camelCase.module.css
- **Component Props**: interface ComponentNameProps
### Query Patterns
- **Repository pattern** for data access
- Encapsulated database operations
- Reusable query methods
- Consistent error handling
- **Pagination** for large collections
- Limit/skip approach
- Cursor-based pagination for efficiency
- **Filtering** with query parameters
- Dynamic filter building
- Validation of filter parameters
### Backend
- **Controllers**: camelCase.controller.ts
- **Models**: PascalCase.model.ts
- **Routes**: camelCase.routes.ts
- **Services**: camelCase.service.ts
- **Middleware**: camelCase.middleware.ts
## Testing Patterns
## Testing Strategy
- **Frontend**:
- Component tests with React Testing Library
- E2E tests with Playwright
- Visual regression tests
- **Backend**:
- Unit tests for business logic
- Integration tests for API endpoints
- Mock database for testing
### Unit Testing
- **Component testing** with React Testing Library
- Component rendering
- User interactions
- State changes
- **Hook testing** with custom test utilities
- **Service testing** for business logic
## CI/CD Pipeline (Planned)
- GitHub Actions for automated builds
- Linting and type checking
- Automated testing
- Docker image building
- Deployment to production server
### Integration Testing
- **API endpoint testing**
- Request validation
- Response structure
- Error handling
- **Database interaction testing**
- Create, read, update, delete operations
- Schema validation
- Indexing performance
### End-to-End Testing
- **User flow testing**
- Critical user journeys
- Form submissions
- Authentication flows
- **Accessibility testing**
- Automated a11y checks
- Manual screen reader testing
- Keyboard navigation testing
## Deployment Patterns
### Docker Containerization
- **Multi-stage builds** for production
- Build stage for dependencies and compilation
- Production stage for runtime
- Minimal production image
- **Docker Compose** for local development
- Service definitions
- Volume mappings
- Network configuration
### Environment Configuration
- **Environment-specific variables**
- Development settings
- Production settings
- Testing settings
- **Secret management**
- Environment variables for sensitive data
- Docker secrets for production
## Accessibility Patterns
### WCAG 2.2 Compliance
- **Semantic HTML**
- Proper heading hierarchy
- Meaningful element choices
- Landmark regions
- **ARIA attributes** when needed
- Role definitions
- State indications
- Relationship attributes
- **Keyboard navigation**
- Focus management
- Logical tab order
- Skip links
- **Screen reader support**
- Alt text for images
- Descriptive link text
- Status announcements
- **Color contrast**
- Meeting WCAG AA contrast ratios
- Not relying on color alone
- **Motion sensitivity**
- Respecting reduced motion preferences
- Careful use of animations

View file

@ -1,98 +1,175 @@
# Technical Context: OCD Website
## Technologies Used
## Technology Stack
### Frontend
- **Framework**: Next.js 15.2.3 - Server-side rendering with React
- **Language**: TypeScript 5.8 - Type-safe development
- **UI Components**: React 19.0.0 - Component-based architecture
- **Framework**: Next.js 15.2.3 with React 19
- **Language**: TypeScript 5.8
- **Styling**:
- Tailwind CSS 4.0.15 - Utility-first CSS framework
- CSS Variables - Custom theming system
- PostCSS 8.4 - CSS processing
- Autoprefixer 10.4 - CSS vendor prefixing
- **Animation**: Framer Motion 12.6.0 - Interactive animations
- **Routing**: Next.js App Router - File-based routing with layouts
- **Icons**: Custom SVG icons - Accessibility-focused
- **Forms**: Custom form components with validation
- Tailwind CSS 4.0
- PostCSS 8.4 with Autoprefixer 10.4
- CSS variables for theming
- **State Management**: React Context API
- **Routing**: Next.js App Router
- **Animations**: Framer Motion 11
- **Icons**: React Icons 5.5.0
- **Maps**: React Simple Maps 3.0.0
- **Utility Libraries**:
- React Intersection Observer 9.16.0
- Axios 1.6 for API requests
- date-fns for date manipulation
### Backend
- **Framework**: Node.js with Express 4.21
- **Language**: TypeScript - Type-safe API development
- **Database**: MongoDB - Document-based storage
- **API**: RESTful endpoints
- **Email**: Nodemailer 6.9 - Email notifications
- **Framework**: Node.js with Express 4.18.2
- **Language**: TypeScript 5.8
- **API Design**: RESTful endpoints
- **Email Handling**: Nodemailer 6.9
- **Authentication**: JWT-based with HTTP-only cookies
- **Data Validation**: Server-side schema validation
- **Cross-Origin**: CORS middleware
- **Environment Variables**: dotenv 16.4
- **Caching & Rate Limiting**: Upstash Redis 1.28
### Development Tools
### Database
- **Primary Database**: MongoDB with Mongoose ODM
- **Caching Layer**: Redis
- **Schema Validation**: Mongoose schemas with validation
- **Data Models**:
- Admin User
- Member
- Event
- Page Content
- Document
- Video
- Contact Submission
### Development & Build Tools
- **Package Manager**: pnpm 9.14
- **Containerization**: Docker & Docker Compose
- **Build Tool**: Vite 6.0
- **Linting**: ESLint 9
- **Build System**: Next.js built-in bundler
- **Version Control**: Git
- **Container**: Docker & Docker Compose
- **Configuration**: Environment variables with dotenv
- **Caching**: Redis (planned)
- **Process Management**: Concurrently for multiple processes
- **TypeScript Execution**: tsx/ts-node
- **Hot Module Replacement**: Enabled
### Deployment
- **Production Server**: Nginx reverse proxy
- **Containers**: Docker multi-stage builds
- **Database**: MongoDB Atlas (planned)
- **Cache**: Redis (planned)
- **CI/CD**: GitHub Actions (planned)
### Production Environment
- **Web Server**: Nginx for static assets and reverse proxy
- **API Server**: Node.js
- **Containerization**: Docker with multi-stage builds
- **SSL**: Let's Encrypt certificates
- **Monitoring**: Basic health checks
- **Logging**: Winston logger with structured format
## Development Environment
## Architecture Overview
### Local Setup
- Next.js development server (localhost:3000)
- Express API server (localhost:4000)
- MongoDB container
- Tailwind CSS JIT compilation
- Hot module reloading
### Frontend Architecture
- **Server Components**: Used for static pages (About, Bylaws)
- **Client Components**: Used for interactive elements
- **Rendering Strategy**:
- SSR for critical pages (Home, Events)
- Static Generation for stable content
- Client-side rendering for interactive components
- **Component Structure**:
- Atomic design principles (atoms, molecules, organisms)
- Layout components for consistent page structure
- Page-specific components
- **Data Flow**:
- React Context for application state
- API hooks for data fetching
- Form state handled locally
### Key Configuration Files
- **docker-compose.yml**: Container orchestration
- **frontend/next.config.js**: Next.js configuration
- **frontend/tailwind.config.js**: Tailwind customization
- **frontend/postcss.config.js**: CSS processing
- **backend/.env**: API environment variables
- **nginx/conf/default.conf**: Production web server
### Backend Architecture
- **API Layer**: RESTful endpoints organized by resource
- **Authentication Layer**: JWT middleware for protected routes
- **Controller Layer**: Handles business logic
- **Model Layer**: Mongoose models with validation
- **Email Service**: Nodemailer for transactional emails
- **File Handling**: Multer for uploads
## Technical Constraints
### Database Design
- Document-oriented schema with Mongoose
- Relationships handled through references
- Indexing for frequently queried fields
- Validation at the schema level
### Accessibility Requirements
- WCAG 2.2 AA compliance
- Screen reader compatibility
## Development Setup
### Local Development
- Docker Compose for local environment
- Frontend on port 3000
- Backend API on port 4000
- MongoDB on port 27017
- Redis on port 6379
- Hot reloading enabled for development
### Environment Configuration
- Frontend environment variables in .env.local
- Backend environment variables in .env
- Production variables set through Docker
### CI/CD Flow
- Manual deployment to production
- Build process in Docker
- Multi-stage builds for smaller images
## API Structure
### Authentication Endpoints
- POST /api/auth/login
- POST /api/auth/logout
- POST /api/auth/reset-password
- POST /api/auth/set-password
### Content Management Endpoints
- GET/POST/PUT/DELETE /api/pages
- GET/PUT /api/pages/:slug
- PUT /api/pages/:id/publish
### Event Endpoints
- GET/POST/PUT/DELETE /api/events
- GET /api/events/:id
- GET /api/events/calendar/:year/:month
### Member Endpoints
- GET/POST/PUT/DELETE /api/members
- GET /api/members/expiring
- POST /api/members/notify
### Document Endpoints
- GET/POST/PUT/DELETE /api/documents
- GET /api/documents/:id
### Video Endpoints
- GET/POST/PUT/DELETE /api/videos
- GET /api/videos/:id
- POST /api/videos/:id/subtitles
### Contact Endpoints
- POST /api/contact
- GET /api/contact
- PUT /api/contact/:id
- POST /api/contact/:id/respond
## Accessibility Implementation
- WCAG 2.2 AA compliance target
- Semantic HTML structure
- ARIA attributes for interactive components
- Keyboard navigation support
- Screen reader considerations
- Color contrast compliance
- Reduced motion support
- DeafBlind/Blind user accessibility
- ASL video content with captions and transcripts
- Focus management
- Reduced motion option
- Video subtitle support
- Text resizing support
### Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- IE not supported
- Mobile browsers (iOS Safari, Chrome Android)
## Security Measures
### Performance Targets
- First contentful paint < 1.5s
- Time to interactive < 3.5s
- Core Web Vitals compliance
- Optimized image delivery
- Minimal JavaScript overhead
### Security Considerations
- HTTPS-only in production
- Content Security Policy headers
- JWT authentication with secure cookies
- Input sanitization and validation
- Authentication with JWTs and HTTP-only cookies
- Role-based access control
- Input validation
- CSRF protection
- Rate limiting (planned)
## Additional Notes
- Custom CSS variables for theming
- Dark mode support via media query
- Mobile-first responsive design
- Component-based architecture
- Accessibility-first approach
- Server-side rendering for SEO
- Content Security Policy
- Secure HTTP headers
- Rate limiting
- Data sanitization
- Secure password handling

View file

@ -0,0 +1,459 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
// Page Filter Component
const PageFilter = ({
onFilterChange
}: {
onFilterChange: (filter: { status: string; search: string }) => void
}) => {
const [status, setStatus] = useState('all');
const [search, setSearch] = useState('');
const handleFilterChange = () => {
onFilterChange({ status, search });
};
const handleReset = () => {
setStatus('all');
setSearch('');
onFilterChange({ status: 'all', search: '' });
};
return (
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filter Pages</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
id="status"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="published">Published</option>
<option value="draft">Draft</option>
</select>
</div>
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
id="search"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
placeholder="Search by title or slug"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="button"
onClick={handleFilterChange}
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Apply Filters
</button>
</div>
</div>
);
};
export default function ContentPage() {
const [filter, setFilter] = useState({ status: 'all', search: '' });
// Mock pages data - would come from API in a real implementation
const allPages = [
{
id: '1',
title: 'Home Page',
slug: '/',
metaDescription: 'Welcome to the Olathe Club of the Deaf, serving the deaf community in Olathe, Kansas.',
status: 'published',
lastUpdated: '2025-03-15',
createdBy: 'Admin User',
sections: 6
},
{
id: '2',
title: 'About Us',
slug: '/about',
metaDescription: 'Learn about the Olathe Club of the Deaf, our mission, history, and board members.',
status: 'published',
lastUpdated: '2025-02-20',
createdBy: 'Admin User',
sections: 4
},
{
id: '3',
title: 'Events',
slug: '/events',
metaDescription: 'Check out upcoming events and activities with the Olathe Club of the Deaf.',
status: 'published',
lastUpdated: '2025-03-10',
createdBy: 'Admin User',
sections: 3
},
{
id: '4',
title: 'Membership',
slug: '/membership',
metaDescription: 'Join the Olathe Club of the Deaf and become part of our vibrant community.',
status: 'published',
lastUpdated: '2025-02-28',
createdBy: 'Admin User',
sections: 5
},
{
id: '5',
title: 'Contact Us',
slug: '/contact',
metaDescription: 'Get in touch with the Olathe Club of the Deaf. We would love to hear from you!',
status: 'published',
lastUpdated: '2025-02-15',
createdBy: 'Admin User',
sections: 2
},
{
id: '6',
title: 'Privacy Policy',
slug: '/privacy-policy',
metaDescription: 'Our privacy policy details how we handle and protect your information.',
status: 'published',
lastUpdated: '2025-01-10',
createdBy: 'Admin User',
sections: 8
},
{
id: '7',
title: 'Donate',
slug: '/donate',
metaDescription: 'Support the Olathe Club of the Deaf through donations.',
status: 'published',
lastUpdated: '2025-03-05',
createdBy: 'Admin User',
sections: 4
},
{
id: '8',
title: 'Summer Programs',
slug: '/summer-programs',
metaDescription: 'Summer programs and activities for deaf youth in Olathe.',
status: 'draft',
lastUpdated: '2025-03-18',
createdBy: 'Admin User',
sections: 3
},
];
// Filter pages based on current filter settings
const filteredPages = allPages.filter(page => {
if (filter.status !== 'all' && page.status !== filter.status) return false;
if (filter.search) {
const searchLower = filter.search.toLowerCase();
if (!page.title.toLowerCase().includes(searchLower) &&
!page.slug.toLowerCase().includes(searchLower)) {
return false;
}
}
return true;
});
// Handle filter changes
const handleFilterChange = (newFilter: { status: string; search: string }) => {
setFilter(newFilter);
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
return (
<div>
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Content Management</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage website content, page sections, and metadata.
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
href="/admin/content/create"
className="btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create New Page
</Link>
</div>
</div>
{/* Filter component */}
<PageFilter onFilterChange={handleFilterChange} />
{/* Pages table */}
<div className="card p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Page
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
URL
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last Updated
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{filteredPages.map((page) => (
<tr key={page.id}>
<td className="px-6 py-4">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{page.title}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{page.sections} sections Created by {page.createdBy}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900 dark:text-white font-mono">{page.slug}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 line-clamp-1">{page.metaDescription}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{formatDate(page.lastUpdated)}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
page.status === 'published'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200'
}`}>
{page.status.charAt(0).toUpperCase() + page.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<Link href={page.slug} target="_blank" className="text-primary hover:text-primary-dark">
View
</Link>
<Link href={`/admin/content/${page.id}/edit`} className="text-primary hover:text-primary-dark">
Edit
</Link>
{page.status === 'published' ? (
<button
onClick={() => {
// In a real app, this would call an API
alert(`Unpublish page: ${page.title}`);
}}
className="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-300"
>
Unpublish
</button>
) : (
<button
onClick={() => {
// In a real app, this would call an API
alert(`Publish page: ${page.title}`);
}}
className="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300"
>
Publish
</button>
)}
{page.id !== '1' && page.id !== '2' && page.id !== '3' && page.id !== '4' && page.id !== '5' && (
<button
onClick={() => {
// In a real app, this would call an API
alert(`Delete page: ${page.title}`);
}}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Delete
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredPages.length === 0 && (
<div className="py-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No pages found matching the current filters.</p>
</div>
)}
</div>
{/* WYSIWYG Editor Preview */}
<div className="mt-12">
<h2 className="text-xl font-semibold mb-6">Page Editor Preview</h2>
<div className="card p-8 bg-gray-50 dark:bg-gray-800">
<div className="flex items-center justify-between mb-6">
<div className="w-full">
<input
type="text"
placeholder="Page Title"
className="w-full text-2xl font-bold bg-transparent border-0 border-b border-gray-300 dark:border-gray-600 focus:outline-none focus:border-primary dark:focus:border-primary px-0 py-2"
defaultValue="New Page Title"
/>
</div>
</div>
<div className="border border-gray-300 dark:border-gray-600 rounded-md mb-6">
{/* Mock toolbar */}
<div className="flex flex-wrap items-center p-2 gap-1 border-b border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700">
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h10M4 18h16" />
</svg>
</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
</svg>
</button>
<span className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-1"></span>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600 font-bold">B</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600 italic">I</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600 underline">U</button>
<span className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-1"></span>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</button>
<button className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
</button>
</div>
{/* Editor content */}
<div className="p-4 min-h-[200px] bg-white dark:bg-gray-700">
<p className="mb-4">This is a preview of the WYSIWYG editor. In the actual implementation, this would be a fully functional content editor with formatting options, image uploads, and more.</p>
<p>The editor would support various formatting options like <strong>bold</strong>, <em>italic</em>, and <u>underline</u> text, as well as lists, links, and images.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label htmlFor="slug" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
URL Path
</label>
<div className="mt-1 flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-sm">
/
</span>
<input
type="text"
id="slug"
className="focus:ring-primary focus:border-primary flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700"
placeholder="page-url-path"
defaultValue="new-page"
/>
</div>
</div>
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
id="status"
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm rounded-md"
defaultValue="draft"
>
<option value="published">Published</option>
<option value="draft">Draft</option>
</select>
</div>
</div>
<div className="mb-6">
<label htmlFor="meta-description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Meta Description (for SEO)
</label>
<textarea
id="meta-description"
rows={3}
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
placeholder="Brief description of the page for search engines (150-160 characters recommended)"
></textarea>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Write a compelling description to improve search engine rankings and click-through rates.
</p>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Save as Draft
</button>
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Publish Page
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,55 @@
'use client';
import React from 'react';
import AdminSidebar from '../../../components/admin/AdminSidebar';
export default function AdminDashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// For a real app, add auth check here to prevent unauthorized access
// const { data: session, status } = useSession()
// if (status === "loading") return <LoadingSpinner />
// if (status === "unauthenticated") redirect('/admin/login')
return (
<div className="h-screen flex">
{/* Sidebar */}
<div className="w-64 md:block hidden">
<AdminSidebar />
</div>
{/* Mobile sidebar (hidden by default) */}
<div className="md:hidden">
{/* We'd implement a mobile menu here */}
</div>
{/* Main content */}
<div className="flex-1 flex flex-col bg-gray-50 dark:bg-gray-900 overflow-y-auto">
{/* Header */}
<header className="bg-white dark:bg-gray-800 shadow-sm">
<div className="px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
Dashboard
</h1>
{/* User menu or mobile menu button would go here */}
<div className="flex items-center space-x-4">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Admin User
</div>
</div>
</div>
</div>
</header>
{/* Page content */}
<main className="flex-1 p-4 sm:p-6 lg:p-8">
{children}
</main>
</div>
</div>
);
}

View file

@ -0,0 +1,266 @@
import React from 'react';
import Link from 'next/link';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Admin Dashboard | Olathe Club of the Deaf',
description: 'Admin dashboard for the Olathe Club of the Deaf website',
};
// Stats Card Component
const StatCard = ({ title, value, icon, href }: { title: string; value: string; icon: React.ReactNode; href: string }) => (
<Link href={href} className="card p-6 hover:shadow-md transition-shadow">
<div className="flex items-center">
<div className="p-3 rounded-full bg-primary-light bg-opacity-20 text-primary">
{icon}
</div>
<div className="ml-5">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{title}</p>
<p className="text-2xl font-semibold text-gray-900 dark:text-white">{value}</p>
</div>
</div>
</Link>
);
// Simple icons
const MembersIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
const EventsIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
);
const ContactIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
const VideosIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
export default function AdminDashboardPage() {
// In a real app, these would be fetched from the API
const stats = {
members: "47",
activeMembers: "41",
events: "12",
videos: "8",
documents: "15",
unreadContacts: "3"
};
// Upcoming events - in a real app, these would be fetched from the API
const upcomingEvents = [
{
id: '1',
title: 'Monthly Social',
date: 'Apr 15, 2025',
location: 'Olathe Community Center',
status: 'Published'
},
{
id: '2',
title: 'ASL Workshop',
date: 'Apr 22, 2025',
location: 'OCD Meeting Room',
status: 'Draft'
},
{
id: '3',
title: 'Board Meeting',
date: 'May 5, 2025',
location: 'Zoom Meeting',
status: 'Published'
}
];
// Membership expirations - in a real app, these would be fetched from the API
const expiringMemberships = [
{
id: '101',
name: 'John Smith',
expiryDate: 'Apr 30, 2025',
type: 'Regular',
status: 'Expiring Soon'
},
{
id: '102',
name: 'Sarah Johnson',
expiryDate: 'May 15, 2025',
type: 'Family',
status: 'Expiring Soon'
},
{
id: '103',
name: 'Robert Lee',
expiryDate: 'Mar 28, 2025',
type: 'Regular',
status: 'Expired'
}
];
return (
<div>
<div className="mb-8">
<h2 className="text-lg font-semibold mb-6">Overview</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Total Members"
value={stats.members}
icon={<MembersIcon />}
href="/admin/members"
/>
<StatCard
title="Active Members"
value={stats.activeMembers}
icon={<MembersIcon />}
href="/admin/members/active"
/>
<StatCard
title="Upcoming Events"
value={stats.events}
icon={<EventsIcon />}
href="/admin/events"
/>
<StatCard
title="Unread Messages"
value={stats.unreadContacts}
icon={<ContactIcon />}
href="/admin/contact"
/>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Upcoming Events */}
<div className="card p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Upcoming Events</h3>
<Link href="/admin/events" className="text-primary hover:text-primary-dark text-sm font-medium">
View All
</Link>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{upcomingEvents.map((event) => (
<div key={event.id} className="py-3">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{event.title}</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">{event.date} {event.location}</p>
</div>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${
event.status === 'Published'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
}`}>
{event.status}
</span>
</div>
</div>
))}
</div>
<div className="mt-4">
<Link
href="/admin/events/create"
className="btn-primary inline-flex items-center text-sm font-medium py-2 px-4 rounded-md"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Event
</Link>
</div>
</div>
{/* Membership Expirations */}
<div className="card p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Membership Expirations</h3>
<Link href="/admin/members" className="text-primary hover:text-primary-dark text-sm font-medium">
View All
</Link>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{expiringMemberships.map((member) => (
<div key={member.id} className="py-3">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{member.name}</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">{member.expiryDate} {member.type}</p>
</div>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${
member.status === 'Expired'
? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200'
}`}>
{member.status}
</span>
</div>
</div>
))}
</div>
<div className="mt-4">
<Link
href="/admin/members/notify"
className="btn-primary inline-flex items-center text-sm font-medium py-2 px-4 rounded-md"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Send Renewal Notices
</Link>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Link href="/admin/events/create" className="card p-4 flex items-center hover:shadow-md transition-shadow">
<div className="mr-3 text-primary">
<EventsIcon />
</div>
<span className="text-sm font-medium">New Event</span>
</Link>
<Link href="/admin/members/add" className="card p-4 flex items-center hover:shadow-md transition-shadow">
<div className="mr-3 text-primary">
<MembersIcon />
</div>
<span className="text-sm font-medium">Add Member</span>
</Link>
<Link href="/admin/videos/upload" className="card p-4 flex items-center hover:shadow-md transition-shadow">
<div className="mr-3 text-primary">
<VideosIcon />
</div>
<span className="text-sm font-medium">Upload Video</span>
</Link>
<Link href="/admin/content/edit" className="card p-4 flex items-center hover:shadow-md transition-shadow">
<div className="mr-3 text-primary">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<span className="text-sm font-medium">Edit Pages</span>
</Link>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,491 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
// Document Filter Component
const DocumentFilter = ({
onFilterChange
}: {
onFilterChange: (filter: { category: string; isPublic: string; search: string }) => void
}) => {
const [category, setCategory] = useState('all');
const [isPublic, setIsPublic] = useState('all');
const [search, setSearch] = useState('');
const handleFilterChange = () => {
onFilterChange({ category, isPublic, search });
};
const handleReset = () => {
setCategory('all');
setIsPublic('all');
setSearch('');
onFilterChange({ category: 'all', isPublic: 'all', search: '' });
};
return (
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filter Documents</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Category
</label>
<select
id="category"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="all">All Categories</option>
<option value="minutes">Meeting Minutes</option>
<option value="bylaws">Bylaws</option>
<option value="forms">Forms</option>
<option value="reports">Reports</option>
<option value="newsletters">Newsletters</option>
</select>
</div>
<div>
<label htmlFor="isPublic" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Visibility
</label>
<select
id="isPublic"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={isPublic}
onChange={(e) => setIsPublic(e.target.value)}
>
<option value="all">All Documents</option>
<option value="public">Public Only</option>
<option value="private">Private Only</option>
</select>
</div>
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
id="search"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
placeholder="Search by title or description"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="button"
onClick={handleFilterChange}
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Apply Filters
</button>
</div>
</div>
);
};
export default function DocumentsPage() {
const [filter, setFilter] = useState({ category: 'all', isPublic: 'all', search: '' });
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
const [selectAll, setSelectAll] = useState(false);
// Mock documents data - would come from API in a real implementation
const allDocuments = [
{
id: '1',
title: 'Board Meeting Minutes - March 2025',
description: 'Minutes from the March 10, 2025 board meeting',
category: 'minutes',
fileUrl: '/documents/minutes-march-2025.pdf',
mimeType: 'application/pdf',
fileSize: '284 KB',
uploadDate: '2025-03-15',
lastModified: '2025-03-15',
isPublic: false,
tags: ['board', 'minutes', 'march', '2025']
},
{
id: '2',
title: 'OCD Bylaws 2025',
description: 'Updated bylaws for the Olathe Club of the Deaf',
category: 'bylaws',
fileUrl: '/documents/ocd-bylaws-2025.pdf',
mimeType: 'application/pdf',
fileSize: '567 KB',
uploadDate: '2025-01-05',
lastModified: '2025-01-05',
isPublic: true,
tags: ['bylaws', 'rules', 'organization']
},
{
id: '3',
title: 'Membership Application Form',
description: 'Form for new membership applications',
category: 'forms',
fileUrl: '/documents/membership-form.pdf',
mimeType: 'application/pdf',
fileSize: '125 KB',
uploadDate: '2024-12-10',
lastModified: '2025-01-15',
isPublic: true,
tags: ['membership', 'application', 'form']
},
{
id: '4',
title: 'Annual Financial Report 2024',
description: 'Financial report for the fiscal year 2024',
category: 'reports',
fileUrl: '/documents/financial-report-2024.pdf',
mimeType: 'application/pdf',
fileSize: '1.2 MB',
uploadDate: '2025-02-15',
lastModified: '2025-02-15',
isPublic: false,
tags: ['financial', 'annual', 'report', '2024']
},
{
id: '5',
title: 'Winter Newsletter 2025',
description: 'Newsletter for winter 2025 with club updates and events',
category: 'newsletters',
fileUrl: '/documents/newsletter-winter-2025.pdf',
mimeType: 'application/pdf',
fileSize: '3.4 MB',
uploadDate: '2025-01-10',
lastModified: '2025-01-10',
isPublic: true,
tags: ['newsletter', 'winter', '2025', 'events']
},
{
id: '6',
title: 'Event Proposal Form',
description: 'Form for members to propose new events',
category: 'forms',
fileUrl: '/documents/event-proposal-form.docx',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
fileSize: '68 KB',
uploadDate: '2024-11-20',
lastModified: '2024-11-20',
isPublic: true,
tags: ['events', 'proposal', 'form']
},
{
id: '7',
title: 'Board Meeting Minutes - February 2025',
description: 'Minutes from the February 12, 2025 board meeting',
category: 'minutes',
fileUrl: '/documents/minutes-february-2025.pdf',
mimeType: 'application/pdf',
fileSize: '312 KB',
uploadDate: '2025-02-18',
lastModified: '2025-02-18',
isPublic: false,
tags: ['board', 'minutes', 'february', '2025']
},
{
id: '8',
title: 'Member Handbook',
description: 'Comprehensive handbook for all OCD members',
category: 'bylaws',
fileUrl: '/documents/member-handbook.pdf',
mimeType: 'application/pdf',
fileSize: '4.2 MB',
uploadDate: '2025-01-05',
lastModified: '2025-01-05',
isPublic: true,
tags: ['handbook', 'members', 'rules', 'guidelines']
},
];
// Filter documents based on current filter settings
const filteredDocuments = allDocuments.filter(document => {
if (filter.category !== 'all' && document.category !== filter.category) return false;
if (filter.isPublic !== 'all') {
const isPublicBool = filter.isPublic === 'public';
if (document.isPublic !== isPublicBool) return false;
}
if (filter.search) {
const searchLower = filter.search.toLowerCase();
if (!document.title.toLowerCase().includes(searchLower) &&
!document.description.toLowerCase().includes(searchLower) &&
!document.tags.some(tag => tag.toLowerCase().includes(searchLower))) {
return false;
}
}
return true;
});
// Handle filter changes
const handleFilterChange = (newFilter: { category: string; isPublic: string; search: string }) => {
setFilter(newFilter);
};
// Handle bulk actions
const handleBulkAction = (action: string) => {
// In a real app, this would call an API
alert(`Perform ${action} on ${selectedDocuments.length} documents`);
};
// Handle select all checkbox
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
const checked = event.target.checked;
setSelectAll(checked);
if (checked) {
setSelectedDocuments(filteredDocuments.map(doc => doc.id));
} else {
setSelectedDocuments([]);
}
};
// Handle individual checkbox selection
const handleSelectDocument = (documentId: string, checked: boolean) => {
if (checked) {
setSelectedDocuments(prev => [...prev, documentId]);
} else {
setSelectedDocuments(prev => prev.filter(id => id !== documentId));
}
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
// Get file icon based on MIME type
const getFileIcon = (mimeType: string) => {
if (mimeType.includes('pdf')) {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
);
} else if (mimeType.includes('word') || mimeType.includes('document')) {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
);
} else if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
);
} else {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
);
}
};
return (
<div>
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Documents</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage bylaws, meeting minutes, forms, and other documents.
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
href="/admin/documents/upload"
className="btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Upload Document
</Link>
</div>
</div>
{/* Filter component */}
<DocumentFilter onFilterChange={handleFilterChange} />
{/* Bulk actions */}
{selectedDocuments.length > 0 && (
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-md mb-6 flex items-center justify-between">
<div className="text-sm font-medium text-blue-700 dark:text-blue-200">
{selectedDocuments.length} document{selectedDocuments.length > 1 ? 's' : ''} selected
</div>
<div className="flex space-x-3">
<button
type="button"
onClick={() => handleBulkAction('download')}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 dark:text-blue-100 dark:bg-blue-800 dark:hover:bg-blue-700"
>
Download
</button>
<button
type="button"
onClick={() => handleBulkAction('delete')}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
>
Delete Selected
</button>
</div>
</div>
)}
{/* Documents table */}
<div className="card p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400">
<div className="flex items-center">
<input
id="select-all"
type="checkbox"
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
checked={selectAll}
onChange={handleSelectAll}
/>
<label htmlFor="select-all" className="sr-only">Select All</label>
</div>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Document
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Category
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Size
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Dates
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Visibility
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{filteredDocuments.map((document) => (
<tr key={document.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<input
id={`select-${document.id}`}
type="checkbox"
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
checked={selectedDocuments.includes(document.id)}
onChange={(e) => handleSelectDocument(document.id, e.target.checked)}
/>
<label htmlFor={`select-${document.id}`} className="sr-only">Select {document.title}</label>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0 text-gray-500 dark:text-gray-400 mr-3">
{getFileIcon(document.mimeType)}
</div>
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{document.title}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{document.description}
</div>
<div className="mt-1 flex flex-wrap gap-1">
{document.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
>
{tag}
</span>
))}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white capitalize">{document.category}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{document.fileSize}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
Uploaded: {formatDate(document.uploadDate)}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
Modified: {formatDate(document.lastModified)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${document.isPublic ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200'}`}>
{document.isPublic ? 'Public' : 'Private'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<a
href={document.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary-dark"
>
View
</a>
<Link href={`/admin/documents/${document.id}/edit`} className="text-primary hover:text-primary-dark">
Edit
</Link>
<button
onClick={() => {
// In a real app, this would call an API
alert(`Delete document: ${document.title}`);
}}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredDocuments.length === 0 && (
<div className="py-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No documents found matching the current filters.</p>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,336 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
// Simple Filter Component
const EventFilter = ({
onFilterChange
}: {
onFilterChange: (filter: { status: string; category: string; search: string }) => void
}) => {
const [status, setStatus] = useState('all');
const [category, setCategory] = useState('all');
const [search, setSearch] = useState('');
const handleFilterChange = () => {
onFilterChange({ status, category, search });
};
const handleReset = () => {
setStatus('all');
setCategory('all');
setSearch('');
onFilterChange({ status: 'all', category: 'all', search: '' });
};
return (
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filter Events</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
id="status"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="published">Published</option>
<option value="draft">Draft</option>
<option value="archived">Archived</option>
</select>
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Category
</label>
<select
id="category"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="all">All Categories</option>
<option value="social">Social</option>
<option value="athletic">Athletic</option>
<option value="board">Board</option>
<option value="general">General</option>
</select>
</div>
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
id="search"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
placeholder="Search by title or location"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="button"
onClick={handleFilterChange}
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Apply Filters
</button>
</div>
</div>
);
};
export default function EventsPage() {
const [filter, setFilter] = useState({ status: 'all', category: 'all', search: '' });
// Mock events data - would come from API in a real implementation
const allEvents = [
{
id: '1',
title: 'Monthly Social',
date: 'Apr 15, 2025',
time: '6:00 PM - 9:00 PM',
location: 'Olathe Community Center',
category: 'social',
status: 'published',
registrationRequired: true,
registeredCount: 24
},
{
id: '2',
title: 'ASL Workshop',
date: 'Apr 22, 2025',
time: '10:00 AM - 12:00 PM',
location: 'OCD Meeting Room',
category: 'educational',
status: 'draft',
registrationRequired: true,
registeredCount: 12
},
{
id: '3',
title: 'Board Meeting',
date: 'May 5, 2025',
time: '7:00 PM - 8:30 PM',
location: 'Zoom Meeting',
category: 'board',
status: 'published',
registrationRequired: false,
registeredCount: 0
},
{
id: '4',
title: 'Summer Picnic',
date: 'Jun 12, 2025',
time: '12:00 PM - 4:00 PM',
location: 'Olathe Park',
category: 'social',
status: 'draft',
registrationRequired: true,
registeredCount: 0
},
{
id: '5',
title: 'Deaf Awareness Week Workshop',
date: 'Sep 23, 2025',
time: '6:30 PM - 8:30 PM',
location: 'Olathe Public Library',
category: 'educational',
status: 'published',
registrationRequired: true,
registeredCount: 8
},
{
id: '6',
title: 'Holiday Party',
date: 'Dec 18, 2025',
time: '7:00 PM - 10:00 PM',
location: 'Olathe Community Center',
category: 'social',
status: 'draft',
registrationRequired: true,
registeredCount: 0
},
];
// Filter events based on current filter settings
const filteredEvents = allEvents.filter(event => {
if (filter.status !== 'all' && event.status !== filter.status) return false;
if (filter.category !== 'all' && event.category !== filter.category) return false;
if (filter.search && !event.title.toLowerCase().includes(filter.search.toLowerCase()) &&
!event.location.toLowerCase().includes(filter.search.toLowerCase())) return false;
return true;
});
// Handle filter changes
const handleFilterChange = (newFilter: { status: string; category: string; search: string }) => {
setFilter(newFilter);
};
// Function to get status badge styles
const getStatusBadgeClasses = (status: string) => {
switch (status) {
case 'published':
return 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200';
case 'draft':
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
case 'archived':
return 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200';
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
}
};
// Function to get category badge styles
const getCategoryBadgeClasses = (category: string) => {
switch (category) {
case 'social':
return 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200';
case 'educational':
return 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-200';
case 'board':
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200';
case 'athletic':
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200';
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
}
};
return (
<div>
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Events</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage club events, registrations, and schedules.
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
href="/admin/events/create"
className="btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create Event
</Link>
</div>
</div>
{/* Filter component */}
<EventFilter onFilterChange={handleFilterChange} />
{/* Events table */}
<div className="card p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Event
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date & Time
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Location
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Registrations
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{filteredEvents.map((event) => (
<tr key={event.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{event.title}</div>
<div>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getCategoryBadgeClasses(event.category)}`}>
{event.category.charAt(0).toUpperCase() + event.category.slice(1)}
</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{event.date}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{event.time}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{event.location}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClasses(event.status)}`}>
{event.status.charAt(0).toUpperCase() + event.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{event.registrationRequired ? (
<span>{event.registeredCount} registered</span>
) : (
<span>Not required</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<Link href={`/admin/events/${event.id}`} className="text-primary hover:text-primary-dark">
View
</Link>
<Link href={`/admin/events/${event.id}/edit`} className="text-primary hover:text-primary-dark">
Edit
</Link>
<button
onClick={() => {
// Delete functionality would go here
alert(`Delete event: ${event.title}`);
}}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredEvents.length === 0 && (
<div className="py-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No events found matching the current filters.</p>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,19 @@
import React from 'react';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Admin Portal | Olathe Club of the Deaf',
description: 'Secure administration portal for the Olathe Club of the Deaf website',
};
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="admin-layout">
{children}
</div>
);
}

View file

@ -0,0 +1,152 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import type { Metadata } from 'next';
// Note: This can't be exported from a client component
// The metadata would need to be in a separate layout.tsx file
const metadata = {
title: 'Admin Login | Olathe Club of the Deaf',
description: 'Secure login for Olathe Club of the Deaf administrators',
};
const LoginPage = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
// This will be replaced with actual API call in the future
// For now, it's just a mock implementation
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Login failed');
}
// Login successful, redirect to admin dashboard
router.push('/admin/dashboard');
} catch (err) {
console.error('Login error:', err);
setError(err instanceof Error ? err.message : 'An unknown error occurred');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 flex items-center justify-center">
<div className="max-w-md w-full space-y-8">
<div>
<h1 className="mt-6 text-center text-3xl font-bold text-gray-900 dark:text-white">
OCD Admin Portal
</h1>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Secure login for authorized administrators
</p>
</div>
<div className="card p-8">
{error && (
<div className="mb-4 bg-red-100 dark:bg-red-900 border border-red-400 text-red-700 dark:text-red-100 px-4 py-3 rounded relative" role="alert">
<span className="block sm:inline">{error}</span>
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="username" className="sr-only">Username</label>
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
className="appearance-none rounded-t-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary focus:border-primary focus:z-10 sm:text-sm"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
aria-required="true"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-b-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary focus:border-primary focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
aria-required="true"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
Remember me
</label>
</div>
<div className="text-sm">
<Link
href="/admin/forgot-password"
className="text-primary hover:text-primary-dark"
>
Forgot your password?
</Link>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary ${loading ? 'opacity-70 cursor-not-allowed' : ''}`}
aria-busy={loading}
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
</form>
</div>
<div className="text-center mt-4">
<Link href="/" className="text-primary hover:text-primary-dark">
Return to public site
</Link>
</div>
</div>
</div>
);
};
export default LoginPage;

View file

@ -0,0 +1,456 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
// Member Filter Component
const MemberFilter = ({
onFilterChange
}: {
onFilterChange: (filter: { status: string; type: string; search: string }) => void
}) => {
const [status, setStatus] = useState('all');
const [type, setType] = useState('all');
const [search, setSearch] = useState('');
const handleFilterChange = () => {
onFilterChange({ status, type, search });
};
const handleReset = () => {
setStatus('all');
setType('all');
setSearch('');
onFilterChange({ status: 'all', type: 'all', search: '' });
};
return (
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filter Members</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
id="status"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
<option value="pending">Pending</option>
</select>
</div>
<div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Membership Type
</label>
<select
id="type"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="all">All Types</option>
<option value="regular">Regular</option>
<option value="family">Family</option>
<option value="lifetime">Lifetime</option>
<option value="honorary">Honorary</option>
</select>
</div>
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
id="search"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
placeholder="Search by name, email, or phone"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="button"
onClick={handleFilterChange}
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Apply Filters
</button>
</div>
</div>
);
};
// Bulk actions component
const BulkActions = ({ selectedMembers, onAction }: { selectedMembers: string[], onAction: (action: string) => void }) => {
if (selectedMembers.length === 0) return null;
return (
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-md mb-6 flex items-center justify-between">
<div className="text-sm font-medium text-blue-700 dark:text-blue-200">
{selectedMembers.length} member{selectedMembers.length > 1 ? 's' : ''} selected
</div>
<div className="flex space-x-3">
<button
type="button"
onClick={() => onAction('email')}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 dark:text-blue-100 dark:bg-blue-800 dark:hover:bg-blue-700"
>
Email Selected
</button>
<button
type="button"
onClick={() => onAction('renew')}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark"
>
Renew Selected
</button>
</div>
</div>
);
};
export default function MembersPage() {
const [filter, setFilter] = useState({ status: 'all', type: 'all', search: '' });
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
const [selectAll, setSelectAll] = useState(false);
// Mock members data - would come from API in a real implementation
const allMembers = [
{
id: '1',
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@example.com',
phone: '(913) 555-1234',
membershipType: 'regular',
joinDate: '2019-06-15',
expirationDate: '2025-06-15',
status: 'active',
lastRenewalDate: '2024-06-10',
},
{
id: '2',
firstName: 'Sarah',
lastName: 'Johnson',
email: 'sarah.j@example.com',
phone: '(913) 555-5678',
membershipType: 'family',
joinDate: '2021-03-22',
expirationDate: '2025-03-22',
status: 'active',
lastRenewalDate: '2024-03-15',
},
{
id: '3',
firstName: 'Robert',
lastName: 'Lee',
email: 'robert.lee@example.com',
phone: '(913) 555-9012',
membershipType: 'regular',
joinDate: '2020-11-05',
expirationDate: '2025-03-28',
status: 'expired',
lastRenewalDate: '2023-03-25',
},
{
id: '4',
firstName: 'Emily',
lastName: 'Davis',
email: 'emily.d@example.com',
phone: '(913) 555-3456',
membershipType: 'lifetime',
joinDate: '2018-09-30',
expirationDate: null,
status: 'active',
lastRenewalDate: null,
},
{
id: '5',
firstName: 'Michael',
lastName: 'Wilson',
email: 'michael.w@example.com',
phone: '(913) 555-7890',
membershipType: 'regular',
joinDate: '2022-01-15',
expirationDate: '2025-01-15',
status: 'active',
lastRenewalDate: '2024-01-10',
},
{
id: '6',
firstName: 'Jennifer',
lastName: 'Brown',
email: 'jennifer.b@example.com',
phone: '(913) 555-2345',
membershipType: 'family',
joinDate: '2021-07-20',
expirationDate: '2023-07-20',
status: 'expired',
lastRenewalDate: '2022-07-15',
},
{
id: '7',
firstName: 'William',
lastName: 'Taylor',
email: 'will.t@example.com',
phone: '(913) 555-6789',
membershipType: 'honorary',
joinDate: '2020-05-10',
expirationDate: null,
status: 'active',
lastRenewalDate: null,
},
{
id: '8',
firstName: 'Lisa',
lastName: 'Anderson',
email: 'lisa.a@example.com',
phone: '(913) 555-0123',
membershipType: 'regular',
joinDate: '2023-02-28',
expirationDate: '2024-02-28',
status: 'pending',
lastRenewalDate: null,
},
];
// Filter members based on current filter settings
const filteredMembers = allMembers.filter(member => {
if (filter.status !== 'all' && member.status !== filter.status) return false;
if (filter.type !== 'all' && member.membershipType !== filter.type) return false;
if (filter.search) {
const searchLower = filter.search.toLowerCase();
const fullName = `${member.firstName} ${member.lastName}`.toLowerCase();
if (!fullName.includes(searchLower) &&
!member.email.toLowerCase().includes(searchLower) &&
!member.phone.includes(filter.search)) {
return false;
}
}
return true;
});
// Handle filter changes
const handleFilterChange = (newFilter: { status: string; type: string; search: string }) => {
setFilter(newFilter);
};
// Handle bulk actions
const handleBulkAction = (action: string) => {
// In a real app, this would call an API
alert(`Perform ${action} on ${selectedMembers.length} members`);
};
// Handle select all checkbox
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
const checked = event.target.checked;
setSelectAll(checked);
if (checked) {
setSelectedMembers(filteredMembers.map(member => member.id));
} else {
setSelectedMembers([]);
}
};
// Handle individual checkbox selection
const handleSelectMember = (memberId: string, checked: boolean) => {
if (checked) {
setSelectedMembers(prev => [...prev, memberId]);
} else {
setSelectedMembers(prev => prev.filter(id => id !== memberId));
}
};
// Function to get status badge styles
const getStatusBadgeClasses = (status: string) => {
switch (status) {
case 'active':
return 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200';
case 'expired':
return 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200';
case 'pending':
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200';
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
}
};
// Format date for display
const formatDate = (dateString: string | null) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
return (
<div>
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Members</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage memberships, track expirations, and communicate with members.
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
href="/admin/members/add"
className="btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Member
</Link>
</div>
</div>
{/* Filter component */}
<MemberFilter onFilterChange={handleFilterChange} />
{/* Bulk actions */}
<BulkActions
selectedMembers={selectedMembers}
onAction={handleBulkAction}
/>
{/* Members table */}
<div className="card p-0 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400">
<div className="flex items-center">
<input
id="select-all"
type="checkbox"
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
checked={selectAll}
onChange={handleSelectAll}
/>
<label htmlFor="select-all" className="sr-only">Select All</label>
</div>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Member
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Contact
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Membership
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Dates
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{filteredMembers.map((member) => (
<tr key={member.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<input
id={`select-${member.id}`}
type="checkbox"
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
checked={selectedMembers.includes(member.id)}
onChange={(e) => handleSelectMember(member.id, e.target.checked)}
/>
<label htmlFor={`select-${member.id}`} className="sr-only">Select {member.firstName} {member.lastName}</label>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{member.firstName} {member.lastName}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{member.email}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{member.phone}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white capitalize">{member.membershipType}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
Joined: {formatDate(member.joinDate)}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
Expires: {formatDate(member.expirationDate)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClasses(member.status)}`}>
{member.status.charAt(0).toUpperCase() + member.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<Link href={`/admin/members/${member.id}`} className="text-primary hover:text-primary-dark">
View
</Link>
<Link href={`/admin/members/${member.id}/edit`} className="text-primary hover:text-primary-dark">
Edit
</Link>
<button
onClick={() => {
// In a real app, this would call an API
alert(`Send renewal notice to: ${member.firstName} ${member.lastName}`);
}}
className="text-primary hover:text-primary-dark"
>
Renew
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredMembers.length === 0 && (
<div className="py-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No members found matching the current filters.</p>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,9 @@
import { redirect } from 'next/navigation';
export default function AdminPage() {
// Redirect to login page
redirect('/admin/login');
// This won't be rendered, but is needed for TypeScript
return null;
}

View file

@ -0,0 +1,621 @@
'use client';
import React, { useState } from 'react';
export default function SettingsPage() {
// Mock settings data - would be loaded from API in real implementation
const [generalSettings, setGeneralSettings] = useState({
siteName: 'Olathe Club of the Deaf',
siteDescription: 'The official website for the Olathe Club of the Deaf, serving the deaf community in Olathe, Kansas.',
contactEmail: 'info@olathedeafclub.com',
notificationEmail: 'notifications@olathedeafclub.com',
facebookUrl: 'https://facebook.com/olathedeafclub',
instagramUrl: 'https://instagram.com/olathedeafclub',
address: '123 Main Street, Olathe, KS 66061',
googleMapsUrl: 'https://goo.gl/maps/exampleMapLink'
});
const [emailSettings, setEmailSettings] = useState({
smtpHost: 'smtp.gmail.com',
smtpPort: '587',
smtpUsername: 'system@olathedeafclub.com',
smtpPassword: '••••••••••••',
fromName: 'Olathe Club of the Deaf',
fromEmail: 'system@olathedeafclub.com',
replyToEmail: 'info@olathedeafclub.com'
});
const [membershipSettings, setMembershipSettings] = useState({
regularMembershipFee: '30',
familyMembershipFee: '45',
lifetimeMembershipFee: '500',
expirationReminderDays: '30',
automaticRenewalEnabled: false,
sendWelcomeEmail: true,
sendExpirationReminders: true,
sendExpiredNotifications: true,
notifyBoardOnExpirations: true
});
const [accessibilitySettings, setAccessibilitySettings] = useState({
highContrastModeEnabled: true,
reducedMotionEnabled: true,
defaultVideoSubtitles: true,
defaultFontSize: 'medium',
defaultASLVideoDuration: '3', // in minutes
transcriptRequiredForVideos: true,
autoTranslateEnabled: false
});
// Handle form submit
const handleSubmit = (e: React.FormEvent, section: string) => {
e.preventDefault();
// In a real app, this would save settings to the API
alert(`Saved ${section} settings`);
};
// Handle changes for each settings section
const handleGeneralChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setGeneralSettings(prev => ({ ...prev, [name]: value }));
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setEmailSettings(prev => ({ ...prev, [name]: value }));
};
const handleMembershipChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value, type } = e.target as HTMLInputElement;
const newValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
setMembershipSettings(prev => ({ ...prev, [name]: newValue }));
};
const handleAccessibilityChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value, type } = e.target as HTMLInputElement;
const newValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
setAccessibilitySettings(prev => ({ ...prev, [name]: newValue }));
};
// Test email connection
const testEmailConnection = async () => {
// In a real app, this would send a test email via API
alert('Test email sent successfully!');
};
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Configure website settings, email notifications, and accessibility options.
</p>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<ul className="flex flex-wrap -mb-px text-sm font-medium text-center">
<li className="mr-2">
<a href="#general" className="inline-block p-4 border-b-2 border-primary text-primary rounded-t-lg active">
General
</a>
</li>
<li className="mr-2">
<a href="#email" className="inline-block p-4 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 rounded-t-lg">
Email
</a>
</li>
<li className="mr-2">
<a href="#membership" className="inline-block p-4 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 rounded-t-lg">
Membership
</a>
</li>
<li>
<a href="#accessibility" className="inline-block p-4 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 rounded-t-lg">
Accessibility
</a>
</li>
</ul>
</div>
{/* Settings Sections */}
<div className="space-y-12">
{/* General Settings */}
<section id="general" className="card p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-6">General Settings</h2>
<form onSubmit={(e) => handleSubmit(e, 'general')}>
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<label htmlFor="siteName" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Site Name
</label>
<div className="mt-1">
<input
type="text"
name="siteName"
id="siteName"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.siteName}
onChange={handleGeneralChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="contactEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Contact Email
</label>
<div className="mt-1">
<input
type="email"
name="contactEmail"
id="contactEmail"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.contactEmail}
onChange={handleGeneralChange}
/>
</div>
</div>
<div className="sm:col-span-6">
<label htmlFor="siteDescription" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Site Description
</label>
<div className="mt-1">
<textarea
id="siteDescription"
name="siteDescription"
rows={3}
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.siteDescription}
onChange={handleGeneralChange}
/>
</div>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Brief description of your organization. This may be used in search results and social media shares.
</p>
</div>
<div className="sm:col-span-3">
<label htmlFor="notificationEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Notification Email
</label>
<div className="mt-1">
<input
type="email"
name="notificationEmail"
id="notificationEmail"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.notificationEmail}
onChange={handleGeneralChange}
/>
</div>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Notifications about system events will be sent to this email.
</p>
</div>
<div className="sm:col-span-3">
<label htmlFor="address" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Organization Address
</label>
<div className="mt-1">
<input
type="text"
name="address"
id="address"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.address}
onChange={handleGeneralChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="facebookUrl" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Facebook URL
</label>
<div className="mt-1">
<input
type="url"
name="facebookUrl"
id="facebookUrl"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.facebookUrl}
onChange={handleGeneralChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="instagramUrl" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Instagram URL
</label>
<div className="mt-1">
<input
type="url"
name="instagramUrl"
id="instagramUrl"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.instagramUrl}
onChange={handleGeneralChange}
/>
</div>
</div>
<div className="sm:col-span-6">
<label htmlFor="googleMapsUrl" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Google Maps URL
</label>
<div className="mt-1">
<input
type="url"
name="googleMapsUrl"
id="googleMapsUrl"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={generalSettings.googleMapsUrl}
onChange={handleGeneralChange}
/>
</div>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
URL to your Google Maps location to display on the contact page.
</p>
</div>
</div>
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="submit"
className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Save Settings
</button>
</div>
</div>
</form>
</section>
{/* Email Settings */}
<section id="email" className="card p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-6">Email Settings</h2>
<form onSubmit={(e) => handleSubmit(e, 'email')}>
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<label htmlFor="smtpHost" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
SMTP Host
</label>
<div className="mt-1">
<input
type="text"
name="smtpHost"
id="smtpHost"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.smtpHost}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="smtpPort" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
SMTP Port
</label>
<div className="mt-1">
<input
type="text"
name="smtpPort"
id="smtpPort"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.smtpPort}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="smtpUsername" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
SMTP Username
</label>
<div className="mt-1">
<input
type="text"
name="smtpUsername"
id="smtpUsername"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.smtpUsername}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="smtpPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
SMTP Password
</label>
<div className="mt-1">
<input
type="password"
name="smtpPassword"
id="smtpPassword"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.smtpPassword}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="fromName" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
From Name
</label>
<div className="mt-1">
<input
type="text"
name="fromName"
id="fromName"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.fromName}
onChange={handleEmailChange}
/>
</div>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
The name that will appear in the From field of emails.
</p>
</div>
<div className="sm:col-span-3">
<label htmlFor="fromEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
From Email
</label>
<div className="mt-1">
<input
type="email"
name="fromEmail"
id="fromEmail"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.fromEmail}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="replyToEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Reply-To Email
</label>
<div className="mt-1">
<input
type="email"
name="replyToEmail"
id="replyToEmail"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={emailSettings.replyToEmail}
onChange={handleEmailChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<button
type="button"
onClick={testEmailConnection}
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
<svg className="-ml-1 mr-2 h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Test Email Connection
</button>
</div>
</div>
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="submit"
className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Save Settings
</button>
</div>
</div>
</form>
</section>
{/* Membership Settings */}
<section id="membership" className="card p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-6">Membership Settings</h2>
<form onSubmit={(e) => handleSubmit(e, 'membership')}>
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-2">
<label htmlFor="regularMembershipFee" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Regular Membership Fee ($)
</label>
<div className="mt-1">
<input
type="text"
name="regularMembershipFee"
id="regularMembershipFee"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={membershipSettings.regularMembershipFee}
onChange={handleMembershipChange}
/>
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="familyMembershipFee" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Family Membership Fee ($)
</label>
<div className="mt-1">
<input
type="text"
name="familyMembershipFee"
id="familyMembershipFee"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={membershipSettings.familyMembershipFee}
onChange={handleMembershipChange}
/>
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="lifetimeMembershipFee" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Lifetime Membership Fee ($)
</label>
<div className="mt-1">
<input
type="text"
name="lifetimeMembershipFee"
id="lifetimeMembershipFee"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={membershipSettings.lifetimeMembershipFee}
onChange={handleMembershipChange}
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="expirationReminderDays" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Days Before Expiration to Send Reminder
</label>
<div className="mt-1">
<input
type="text"
name="expirationReminderDays"
id="expirationReminderDays"
className="shadow-sm focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded-md"
value={membershipSettings.expirationReminderDays}
onChange={handleMembershipChange}
/>
</div>
</div>
<div className="sm:col-span-6">
<div className="space-y-4">
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="automaticRenewalEnabled"
name="automaticRenewalEnabled"
type="checkbox"
className="focus:ring-primary h-4 w-4 text-primary border-gray-300 rounded"
checked={membershipSettings.automaticRenewalEnabled}
onChange={handleMembershipChange}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="automaticRenewalEnabled" className="font-medium text-gray-700 dark:text-gray-300">
Enable Automatic Renewal
</label>
<p className="text-gray-500 dark:text-gray-400">When enabled, members will be asked if they want to set up automatic renewal.</p>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="sendWelcomeEmail"
name="sendWelcomeEmail"
type="checkbox"
className="focus:ring-primary h-4 w-4 text-primary border-gray-300 rounded"
checked={membershipSettings.sendWelcomeEmail}
onChange={handleMembershipChange}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="sendWelcomeEmail" className="font-medium text-gray-700 dark:text-gray-300">
Send Welcome Email
</label>
<p className="text-gray-500 dark:text-gray-400">Send welcome email to new members when they join.</p>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="sendExpirationReminders"
name="sendExpirationReminders"
type="checkbox"
className="focus:ring-primary h-4 w-4 text-primary border-gray-300 rounded"
checked={membershipSettings.sendExpirationReminders}
onChange={handleMembershipChange}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="sendExpirationReminders" className="font-medium text-gray-700 dark:text-gray-300">
Send Expiration Reminders
</label>
<p className="text-gray-500 dark:text-gray-400">Send email reminders to members before their membership expires.</p>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="sendExpiredNotifications"
name="sendExpiredNotifications"
type="checkbox"
className="focus:ring-primary h-4 w-4 text-primary border-gray-300 rounded"
checked={membershipSettings.sendExpiredNotifications}
onChange={handleMembershipChange}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="sendExpiredNotifications" className="font-medium text-gray-700 dark:text-gray-300">
Send Expired Notifications
</label>
<p className="text-gray-500 dark:text-gray-400">Send notification to members when their membership has expired.</p>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="notifyBoardOnExpirations"
name="notifyBoardOnExpirations"
type="checkbox"
className="focus:ring-primary h-4 w-4 text-primary border-gray-300 rounded"
checked={membershipSettings.notifyBoardOnExpirations}
onChange={handleMembershipChange}
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="notifyBoardOnExpirations" className="font-medium text-gray-700 dark:text-gray-300">
Notify Board on Expirations
</label>
<p className="text-gray-500 dark:text-gray-400">Send notification to board members when memberships expire.</p>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="submit"
className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2

View file

@ -0,0 +1,326 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
// Video Filter Component
const VideoFilter = ({
onFilterChange
}: {
onFilterChange: (filter: { status: string; category: string; search: string }) => void
}) => {
const [status, setStatus] = useState('all');
const [category, setCategory] = useState('all');
const [search, setSearch] = useState('');
const handleFilterChange = () => {
onFilterChange({ status, category, search });
};
const handleReset = () => {
setStatus('all');
setCategory('all');
setSearch('');
onFilterChange({ status: 'all', category: 'all', search: '' });
};
return (
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Filter Videos</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
id="status"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="published">Published</option>
<option value="draft">Draft</option>
<option value="processing">Processing</option>
</select>
</div>
<div>
<label htmlFor="category" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Category
</label>
<select
id="category"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="all">All Categories</option>
<option value="announcements">Announcements</option>
<option value="events">Events</option>
<option value="tutorials">Tutorials</option>
<option value="stories">Stories</option>
</select>
</div>
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
id="search"
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
placeholder="Search by title or description"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleReset}
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Reset
</button>
<button
type="button"
onClick={handleFilterChange}
className="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
Apply Filters
</button>
</div>
</div>
);
};
export default function VideosPage() {
const [filter, setFilter] = useState({ status: 'all', category: 'all', search: '' });
// Mock videos data - would come from API in a real implementation
const allVideos = [
{
id: '1',
title: 'Welcome to OCD',
description: 'An introduction to the Olathe Club of the Deaf',
duration: '2:35',
uploadDate: '2025-01-15',
category: 'announcements',
status: 'published',
thumbnailUrl: '/images/video-thumb-1.jpg',
hasSubtitles: true,
hasTranscript: true
},
{
id: '2',
title: 'ASL Tutorial: Basic Greetings',
description: 'Learn basic greetings in American Sign Language',
duration: '4:12',
uploadDate: '2025-02-03',
category: 'tutorials',
status: 'published',
thumbnailUrl: '/images/video-thumb-2.jpg',
hasSubtitles: true,
hasTranscript: true
},
{
id: '3',
title: 'Summer Picnic 2024 Recap',
description: 'Highlights from our annual summer picnic',
duration: '3:45',
uploadDate: '2024-07-25',
category: 'events',
status: 'published',
thumbnailUrl: '/images/video-thumb-3.jpg',
hasSubtitles: true,
hasTranscript: true
},
{
id: '4',
title: 'Board Meeting Announcement - April 2025',
description: 'Important information about the upcoming board meeting',
duration: '1:30',
uploadDate: '2025-03-15',
category: 'announcements',
status: 'draft',
thumbnailUrl: '/images/video-thumb-4.jpg',
hasSubtitles: false,
hasTranscript: true
},
{
id: '5',
title: 'ASL Story: The Tortoise and the Hare',
description: 'A classic fable told in ASL',
duration: '6:20',
uploadDate: '2025-02-20',
category: 'stories',
status: 'published',
thumbnailUrl: '/images/video-thumb-5.jpg',
hasSubtitles: true,
hasTranscript: true
},
{
id: '6',
title: 'Membership Benefits Explained',
description: 'Learn about the benefits of becoming an OCD member',
duration: '3:10',
uploadDate: '2025-03-01',
category: 'announcements',
status: 'processing',
thumbnailUrl: '/images/video-thumb-6.jpg',
hasSubtitles: false,
hasTranscript: false
},
];
// Filter videos based on current filter settings
const filteredVideos = allVideos.filter(video => {
if (filter.status !== 'all' && video.status !== filter.status) return false;
if (filter.category !== 'all' && video.category !== filter.category) return false;
if (filter.search && !video.title.toLowerCase().includes(filter.search.toLowerCase()) &&
!video.description.toLowerCase().includes(filter.search.toLowerCase())) return false;
return true;
});
// Handle filter changes
const handleFilterChange = (newFilter: { status: string; category: string; search: string }) => {
setFilter(newFilter);
};
// Function to get status badge styles
const getStatusBadgeClasses = (status: string) => {
switch (status) {
case 'published':
return 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200';
case 'draft':
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
case 'processing':
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200';
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300';
}
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
// Video card component
const VideoCard = ({ video }: { video: typeof allVideos[0] }) => (
<div className="card overflow-hidden h-full flex flex-col">
{/* Thumbnail with duration */}
<div className="relative bg-gray-200 dark:bg-gray-700 aspect-video">
{/* In a real app, this would be an actual image */}
<div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="absolute bottom-2 right-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded">
{video.duration}
</div>
</div>
{/* Content */}
<div className="p-4 flex-1 flex flex-col">
<div className="flex items-start justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-white line-clamp-1">{video.title}</h3>
<span className={`ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getStatusBadgeClasses(video.status)}`}>
{video.status.charAt(0).toUpperCase() + video.status.slice(1)}
</span>
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">{video.description}</p>
<div className="mt-2 flex items-center text-xs text-gray-500 dark:text-gray-400">
<span>Uploaded: {formatDate(video.uploadDate)}</span>
</div>
{/* Accessibility status */}
<div className="mt-2 flex items-center space-x-2 text-xs">
<span className={`px-2 py-0.5 rounded-full ${video.hasSubtitles ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200'}`}>
{video.hasSubtitles ? 'Has Subtitles' : 'No Subtitles'}
</span>
<span className={`px-2 py-0.5 rounded-full ${video.hasTranscript ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200'}`}>
{video.hasTranscript ? 'Has Transcript' : 'No Transcript'}
</span>
</div>
{/* Category */}
<div className="mt-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200">
{video.category.charAt(0).toUpperCase() + video.category.slice(1)}
</span>
</div>
{/* Actions */}
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-between">
<Link href={`/admin/videos/${video.id}`} className="text-primary hover:text-primary-dark text-sm">
View
</Link>
<Link href={`/admin/videos/${video.id}/edit`} className="text-primary hover:text-primary-dark text-sm">
Edit
</Link>
<button
onClick={() => {
// Delete functionality would go here
alert(`Delete video: ${video.title}`);
}}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 text-sm"
>
Delete
</button>
</div>
</div>
</div>
);
return (
<div>
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Videos</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage ASL videos with subtitles and transcripts.
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
href="/admin/videos/upload"
className="btn-primary inline-flex items-center px-4 py-2 text-sm font-medium rounded-md"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Upload Video
</Link>
</div>
</div>
{/* Filter component */}
<VideoFilter onFilterChange={handleFilterChange} />
{/* Videos grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredVideos.map((video) => (
<VideoCard key={video.id} video={video} />
))}
</div>
{filteredVideos.length === 0 && (
<div className="card p-6 text-center">
<p className="text-gray-500 dark:text-gray-400">No videos found matching the current filters.</p>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
// Mock user data - in a real application, this would be fetched from a database
const MOCK_USER = {
id: '1',
username: 'admin',
// In a real app, this would be a hashed password, not plaintext
password: 'password123',
name: 'Admin User',
role: 'admin',
};
export async function POST(request: NextRequest) {
try {
// Parse the request body
const body = await request.json();
const { username, password } = body;
// Basic validation
if (!username || !password) {
return NextResponse.json(
{ message: 'Username and password are required' },
{ status: 400 }
);
}
// In a real app, you would:
// 1. Query the database for the user
// 2. Compare the hashed password
// 3. Create a JWT token
// For this demo, we're using a mock user and simple comparison
if (username === MOCK_USER.username && password === MOCK_USER.password) {
// Create session or JWT token here
// For now, we'll just return a successful response with mock user data
// In production, you would create a JWT token and set it as an HTTP-only cookie
// Create a new response
const response = NextResponse.json({
success: true,
user: {
id: MOCK_USER.id,
username: MOCK_USER.username,
name: MOCK_USER.name,
role: MOCK_USER.role,
},
});
// In a real app, set a secure HTTP-only cookie with the JWT
// response.cookies.set({
// name: 'auth_token',
// value: 'MOCK_JWT_TOKEN',
// httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
// maxAge: 60 * 60 * 24 * 7, // 1 week
// path: '/',
// });
return response;
}
// If credentials don't match
return NextResponse.json(
{ message: 'Invalid username or password' },
{ status: 401 }
);
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,131 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface NavItem {
name: string;
href: string;
icon: React.ReactNode;
}
// Simple icons for admin navigation
const DashboardIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16m-7 6h7" />
</svg>
);
const EventsIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
);
const MembersIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
const ContentIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
);
const DocumentsIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
);
const VideosIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
const SettingsIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
const LogoutIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
);
const AdminSidebar = () => {
const pathname = usePathname();
const navigation: NavItem[] = [
{ name: 'Dashboard', href: '/admin/dashboard', icon: <DashboardIcon /> },
{ name: 'Events', href: '/admin/events', icon: <EventsIcon /> },
{ name: 'Members', href: '/admin/members', icon: <MembersIcon /> },
{ name: 'Content', href: '/admin/content', icon: <ContentIcon /> },
{ name: 'Documents', href: '/admin/documents', icon: <DocumentsIcon /> },
{ name: 'Videos', href: '/admin/videos', icon: <VideosIcon /> },
{ name: 'Settings', href: '/admin/settings', icon: <SettingsIcon /> },
];
return (
<div className="h-full flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
{/* Logo and site title */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<Link href="/admin/dashboard" className="flex items-center">
<span className="text-xl font-semibold text-gray-900 dark:text-white">OCD Admin</span>
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4">
<div className="px-2 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.name}
href={item.href}
className={`group flex items-center px-4 py-2 text-sm font-medium rounded-md ${
isActive
? 'bg-primary text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
aria-current={isActive ? 'page' : undefined}
>
<span className={`mr-3 ${isActive ? 'text-white' : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-300'}`}>
{item.icon}
</span>
{item.name}
</Link>
);
})}
</div>
</nav>
{/* Logout button at bottom */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => {
// In a real app, call logout API
window.location.href = '/admin/login';
}}
className="w-full flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
>
<span className="mr-3 text-gray-500 dark:text-gray-400">
<LogoutIcon />
</span>
Logout
</button>
</div>
</div>
);
};
export default AdminSidebar;

View file

@ -20,12 +20,12 @@ The Olathe Club of the Deaf (OCD) website will serve as a digital hub for the de
- **Animation**: Framer Motion 11 for UI transitions
- **Routing**: Next.js App Router with nested layouts
- **Interactive Components**:
- React Simple Maps 4.0 for location visualization
- React Simple Maps 3.0.0 for location visualization
- Custom video player with WebVTT support
- React Intersection Observer 9.13 for scroll animations
- React Intersection Observer 9.16.0 for scroll animations
### Backend Architecture
- **Framework**: Node.js with Express 4.21
- **Framework**: Node.js with Express 4.18.2
- **API Design**: RESTful endpoints
- **Authentication**:
- JWT-based authentication for admin access