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

View file

@ -13,7 +13,7 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.0", "dotenv": "^16.4.0",
"express": "^4.21.0", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2", "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 ## What I'm Working On Now
- Implemented Phase 1 of the OCD website based on the planned implementation - Completing Phase 3 of the OCD website development plan
- Created key public-facing pages (Home, About, Contact) with full accessibility - Finalizing admin interface implementation
- Set up core components like Navigation, Footer, Hero, and ContactForm with WCAG 2.2 AA compliance - Setting up connections between frontend UI and backend APIs
- Established MongoDB schema validation and database models - Preparing for Phase 4 (accessibility features and refinement)
- Implemented JWT-based authentication middleware with RBAC - Adding API integration for CRUD operations
- Created Docker configuration with security best practices
## Recent Changes ## Recent Changes
- Created the complete directory structure following atomic design principles - Implemented comprehensive admin dashboard system:
- Set up core configuration files: - Created central dashboard with statistics and quick actions
- Docker Compose with multi-stage builds, resource limits, and security - Built out events management with filtering and CRUD UI
- Frontend/backend Dockerfiles with non-root users - Implemented members management with bulk actions and status tracking
- MongoDB initialization scripts with schema validation - Developed video management with accessibility indicators
- Next.js configuration with security headers - Added document repository with visibility controls
- Tailwind CSS with CSS-first @theme configuration - Created content management with WYSIWYG editor preview
- Nginx configuration with security headers - Added settings interface with site, email, and membership options
- Installed dependencies for both frontend and backend
- Implemented responsive Navigation component with keyboard accessibility - Major improvements to frontend architecture:
- Added Footer component with site map and accessibility options - Implemented responsive layouts for all admin interfaces
- Created Hero component with animations and responsive design - Used TypeScript for strongly-typed components
- Built Contact form with validation and ARIA attributes - Created reusable filter, table, and card components
- Implemented Home, About, and Contact pages with accessibility features - Maintained consistent UI patterns across admin sections
- Created Video Player component with transcript display options - Ensured all interfaces follow accessibility guidelines
- Set up JWT authentication middleware for admin dashboard
## Next Steps ## Next Steps
1. Complete remaining public-facing pages: 1. Complete backend API integrations:
- Events page with calendar component - Connect admin UI components to backend endpoints
- Membership information page - Add real data loading with loading states
- Donation page with Zeffy integration - Implement error handling for API requests
- Accessibility statement page - Set up client-side data validation
2. Begin admin dashboard implementation: 2. Implement remaining admin features:
- Create authentication flow - Add form handlers for CRUD operations
- Implement dashboard layout - Implement event recurrence functionality
- Set up protected routes - Create media upload components
- Create admin forms for content management - Add user notification system
3. Implement event management system 3. Deploy staging environment:
4. Set up video upload and management functionality - Set up Docker containers for testing
5. Create membership tracking system with expiration notifications - Configure environment variables
- Enable SSL certificates
- Test on multiple devices and browsers
## Special Considerations ## Special Considerations
- Maintain WCAG 2.2 AA compliance throughout all components and pages - Ensure strict authentication for all admin routes
- Focus on creating components that support deaf and DeafBlind users - Keep all forms accessible and keyboard navigable
- All videos must have thumbnails, captions, and full transcripts - Ensure admin interfaces work well on mobile devices
- Ensure keyboard navigation works properly throughout the site - All uploaded content must support WCAG 2.2 AA compliance
- Follow database schema validation using $jsonSchema for data integrity - Maintain clear separation between public and admin areas
- Apply security best practices in all aspects of the implementation - 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 1. **Provide Accessible Information**: Deliver critical information about the club's activities, mission, and resources in a fully accessible format.
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
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 ## Problems Solved
The OCD website addresses several key challenges:
### For Community Members ### 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 - **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.
- **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
### For Website Visitors - **Community Discovery**: Finding local deaf community events and resources can be challenging. The website centralizes information about club activities and resources.
- **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
## 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 ### For Club Administrators
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
#### Organization Administrator Journey - **Administrative Burden**: Tracking memberships, organizing events, and distributing information manually is time-consuming. The admin system automates these tasks.
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
#### Website Visitor Journey - **Technological Barriers**: Club leaders may not have technical expertise. The admin interface provides accessible tools for non-technical users.
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
### Key Features - **Content Updates**: Maintaining current information was historically difficult. The CMS allows for easy content updates.
#### Public-Facing Features - **Resource Distribution**: Sharing meeting minutes, bylaws, and other documents was inefficient. The document repository centralizes these resources.
- **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
#### Administration Features ### For Public Awareness
- **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
### User Types & Needs - **Deaf Awareness**: Many hearing people lack understanding of deaf culture. The website educates the broader community.
#### Deaf Community Members - **Resource Visibility**: Community resources for deaf individuals were not widely known. The website increases visibility of these resources.
- 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
#### DeafBlind Users ## Target Users
- Screen reader compatibility
- High contrast mode
- Simplified content structure
- Keyboard navigation support
- Text descriptions of all visual elements
- Reduced motion option
#### Organization Board Members ### Primary Users
- Secure access to administrative features
- Easy content management without technical skills
- Membership data visualization
- Export capabilities for reports
- Notification management
#### Potential Members & Community Supporters 1. **Current OCD Members**
- Clear information about the organization - Age range: 18-80+
- Simple donation process - Technical proficiency: Varies widely
- Easy-to-understand membership benefits - Need: Access event information, membership details, club updates
- Mobile-friendly experience
- Social sharing capabilities
## Special Considerations 2. **Club Leadership & Board Members**
- Role: Volunteer administrators
- Technical proficiency: Basic to intermediate
- Need: Manage content, track memberships, coordinate events
### Accessibility Requirements 3. **Prospective Members**
- WCAG 2.2 AA compliance as minimum standard - Deaf or hard-of-hearing individuals in Olathe area
- ASL videos must have captions and transcripts - May be new to the area or community
- All functionality must be keyboard accessible - Need: Learn about the club, membership benefits, upcoming events
- 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
### Content Management ### Secondary Users
- 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
### Technical Requirements 4. **Deaf Community Supporters**
- Fast loading times (especially on mobile) - Family members, interpreters, educators
- Secure authentication for admin features - Connection: Support deaf individuals or the community
- HTTPS encryption for all traffic - Need: Event information, volunteer opportunities, donation options
- Regular security updates
- Daily database backups
- Responsive design for all device sizes
- Cross-browser compatibility
- Email notification system for expirations
### Legal & Privacy 5. **Community Partners**
- Clear privacy policy - Local businesses, schools, government agencies
- Secure handling of member data - Relationship: Collaborate with OCD on initiatives
- GDPR-compliant data practices - Need: Contact information, organizational details
- Terms of service for website use
- Accessibility statement with contact information 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 # Project Progress: OCD Website
## What Works ## Completed Features
- 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
## What's Left to Build ### Phase 1: Project Setup & Infrastructure
The implementation has a solid foundation, but these key features still need to be built: - [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 ### Phase 2: Core Public Pages
2. Membership information and application pages - [x] Implement home page with featured events section
3. Admin dashboard and authentication flow - [x] Create About OCD section pages
4. Full event management system - [x] History page
5. Membership tracking and notification system - [x] Mission statement page
6. Document repository with meeting minutes and bylaws - [x] Board members page with photo grid
7. Full implementation of video management with upload functionality - [x] Implement bylaws page with document viewer
8. Integration with Zeffy for donations - [x] Create minutes archive with filtering and search
9. Email notification system implementation - [x] Develop responsive navigation system
10. Final deployment and testing - [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 ## In Progress Features
- **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
## Current Task ### Phase 4: API Integration & Backend Functionality
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. - [ ] 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 ## Architecture Patterns
### Frontend Architecture ### Component Architecture
- **Component Structure**: Atomic Design methodology - **Atomic Design Methodology**
- **Atoms**: Buttons, inputs, icons, typography elements - **Atoms**: Basic building blocks (buttons, inputs, icons)
- **Molecules**: Forms, cards, navigation items, video player - **Molecules**: Simple groups of UI elements (form fields, cards)
- **Organisms**: Navigation bar, page sections, footer - **Organisms**: Complex UI components (navigation, forms)
- **Templates**: Page layouts with content placeholders - **Templates**: Page layouts without specific content
- **Pages**: Complete views with actual content - **Pages**: Templates with real 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
### Backend Architecture - **File Organization**
- **API Design**: RESTful with resource-based endpoints - Components grouped by type (atoms, molecules, organisms)
- **Controller Pattern**: Route handlers separated from business logic - Pages in Next.js app directory structure
- **Repository Pattern**: Data access abstracted from controllers - Shared utilities in separate folders
- **Middleware Stack**: - Clear separation between frontend and backend
- Authentication/Authorization
- Request validation
- Error handling
- Logging
- CORS handling
- **Data Models**: MongoDB schemas with validation
- **Service Layer**: Business logic encapsulated in service modules
## 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 ### Data Flow Patterns
- File-based routing with nested layouts - **Unidirectional data flow**
- Server components for performance - Props down, events up
- Client components marked with 'use client' for interactivity - Context providers at appropriate levels
- Metadata API for SEO optimization - **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 ## Frontend Patterns
- 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
### Accessibility First ### Styling Approach
- WCAG 2.2 AA compliance as baseline requirement - **Utility-first CSS with Tailwind**
- Semantic HTML structure - Consistent design tokens through Tailwind config
- ARIA attributes used appropriately - Custom utilities for project-specific needs
- Keyboard navigation focus management - Component classes for common patterns
- Screen reader announcements for dynamic content - **CSS variables for theming**
- High contrast mode support - Core colors defined as CSS variables
- Reduced motion option - 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 ### Responsive Design
- Multi-stage builds for production optimization - **Mobile-first approach**
- Development environment consistency - Default styles for mobile
- Separate containers for frontend, backend, and database - Progressive enhancement for larger screens
- Volume mounting for source code in development - **Breakpoint system**
- Environment variable management - 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 ### Form Handling
- HTTP-only cookies for security - **Controlled components** for form inputs
- Role-based access control - **Field validation** with client-side validation
- Token refresh strategy - **Error handling** with inline error messages
- Secure routes with middleware - **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 ## Backend Patterns
```
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 Structure ### API Design
``` - **RESTful endpoints**
backend/ - Resource-based URLs
├── src/ - Appropriate HTTP methods
│ ├── config/ # Configuration settings - Consistent response format
│ ├── controllers/ # Request handlers - Status codes for error handling
│ ├── middleware/ # Express middleware - **Controller pattern**
│ ├── models/ # Data models and schemas - Route handlers for request parsing
│ ├── routes/ # API route definitions - Controllers for business logic
│ ├── services/ # Business logic - Services for reusable operations
│ └── utils/ # Helper functions - Models for data access
└── tests/ # Test suites
```
## Design Patterns ### Authentication
- **JWT-based authentication**
### Component Composition - JWTs stored in HTTP-only cookies
- Prefer composition over inheritance - Short expiration with refresh tokens
- Small, focused components with single responsibilities - CSRF protection with tokens
- Higher-order components for shared functionality - **Permission-based authorization**
- Render props for flexible component APIs - Role-based access control
- Resource ownership checks
### Custom Hooks - Fine-grained permissions
- Extract reusable stateful logic into custom hooks
- Naming convention: use[HookName]
- Keep hooks focused on specific concerns
### Error Handling ### Error Handling
- Try/catch blocks in async functions - **Centralized error handling middleware**
- Error boundaries for UI component failures - Consistent error format
- Consistent error response format in API - Appropriate status codes
- Detailed logging for debugging - Detailed error messages in development
- Sanitized messages in production
- **Try/catch blocks** for async operations
- **Validation errors** for input validation
### Dependency Injection ## Database Patterns
- Service dependencies provided via parameters
- Loose coupling between modules
- Facilitates testing with mocks
## 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 ### Query Patterns
- **Component Files**: PascalCase.tsx - **Repository pattern** for data access
- **Hook Files**: camelCase.ts - Encapsulated database operations
- **Utility Files**: camelCase.ts - Reusable query methods
- **CSS Modules**: camelCase.module.css - Consistent error handling
- **Component Props**: interface ComponentNameProps - **Pagination** for large collections
- Limit/skip approach
- Cursor-based pagination for efficiency
- **Filtering** with query parameters
- Dynamic filter building
- Validation of filter parameters
### Backend ## Testing Patterns
- **Controllers**: camelCase.controller.ts
- **Models**: PascalCase.model.ts
- **Routes**: camelCase.routes.ts
- **Services**: camelCase.service.ts
- **Middleware**: camelCase.middleware.ts
## Testing Strategy ### Unit Testing
- **Frontend**: - **Component testing** with React Testing Library
- Component tests with React Testing Library - Component rendering
- E2E tests with Playwright - User interactions
- Visual regression tests - State changes
- **Backend**: - **Hook testing** with custom test utilities
- Unit tests for business logic - **Service testing** for business logic
- Integration tests for API endpoints
- Mock database for testing
## CI/CD Pipeline (Planned) ### Integration Testing
- GitHub Actions for automated builds - **API endpoint testing**
- Linting and type checking - Request validation
- Automated testing - Response structure
- Docker image building - Error handling
- Deployment to production server - **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 # Technical Context: OCD Website
## Technologies Used ## Technology Stack
### Frontend ### Frontend
- **Framework**: Next.js 15.2.3 - Server-side rendering with React - **Framework**: Next.js 15.2.3 with React 19
- **Language**: TypeScript 5.8 - Type-safe development - **Language**: TypeScript 5.8
- **UI Components**: React 19.0.0 - Component-based architecture
- **Styling**: - **Styling**:
- Tailwind CSS 4.0.15 - Utility-first CSS framework - Tailwind CSS 4.0
- CSS Variables - Custom theming system - PostCSS 8.4 with Autoprefixer 10.4
- PostCSS 8.4 - CSS processing - CSS variables for theming
- Autoprefixer 10.4 - CSS vendor prefixing - **State Management**: React Context API
- **Animation**: Framer Motion 12.6.0 - Interactive animations - **Routing**: Next.js App Router
- **Routing**: Next.js App Router - File-based routing with layouts - **Animations**: Framer Motion 11
- **Icons**: Custom SVG icons - Accessibility-focused - **Icons**: React Icons 5.5.0
- **Forms**: Custom form components with validation - **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 ### Backend
- **Framework**: Node.js with Express 4.21 - **Framework**: Node.js with Express 4.18.2
- **Language**: TypeScript - Type-safe API development - **Language**: TypeScript 5.8
- **Database**: MongoDB - Document-based storage - **API Design**: RESTful endpoints
- **API**: RESTful endpoints - **Email Handling**: Nodemailer 6.9
- **Email**: Nodemailer 6.9 - Email notifications
- **Authentication**: JWT-based with HTTP-only cookies - **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 - **Package Manager**: pnpm 9.14
- **Containerization**: Docker & Docker Compose
- **Build Tool**: Vite 6.0
- **Linting**: ESLint 9 - **Linting**: ESLint 9
- **Build System**: Next.js built-in bundler - **Process Management**: Concurrently for multiple processes
- **Version Control**: Git - **TypeScript Execution**: tsx/ts-node
- **Container**: Docker & Docker Compose - **Hot Module Replacement**: Enabled
- **Configuration**: Environment variables with dotenv
- **Caching**: Redis (planned)
### Deployment ### Production Environment
- **Production Server**: Nginx reverse proxy - **Web Server**: Nginx for static assets and reverse proxy
- **Containers**: Docker multi-stage builds - **API Server**: Node.js
- **Database**: MongoDB Atlas (planned) - **Containerization**: Docker with multi-stage builds
- **Cache**: Redis (planned) - **SSL**: Let's Encrypt certificates
- **CI/CD**: GitHub Actions (planned) - **Monitoring**: Basic health checks
- **Logging**: Winston logger with structured format
## Development Environment ## Architecture Overview
### Local Setup ### Frontend Architecture
- Next.js development server (localhost:3000) - **Server Components**: Used for static pages (About, Bylaws)
- Express API server (localhost:4000) - **Client Components**: Used for interactive elements
- MongoDB container - **Rendering Strategy**:
- Tailwind CSS JIT compilation - SSR for critical pages (Home, Events)
- Hot module reloading - 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 ### Backend Architecture
- **docker-compose.yml**: Container orchestration - **API Layer**: RESTful endpoints organized by resource
- **frontend/next.config.js**: Next.js configuration - **Authentication Layer**: JWT middleware for protected routes
- **frontend/tailwind.config.js**: Tailwind customization - **Controller Layer**: Handles business logic
- **frontend/postcss.config.js**: CSS processing - **Model Layer**: Mongoose models with validation
- **backend/.env**: API environment variables - **Email Service**: Nodemailer for transactional emails
- **nginx/conf/default.conf**: Production web server - **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 ## Development Setup
- WCAG 2.2 AA compliance
- Screen reader compatibility ### 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 - Keyboard navigation support
- Screen reader considerations
- Color contrast compliance - Color contrast compliance
- Reduced motion support - Focus management
- DeafBlind/Blind user accessibility - Reduced motion option
- ASL video content with captions and transcripts - Video subtitle support
- Text resizing support
### Browser Support ## Security Measures
- Modern browsers (Chrome, Firefox, Safari, Edge)
- IE not supported
- Mobile browsers (iOS Safari, Chrome Android)
### Performance Targets - Authentication with JWTs and HTTP-only cookies
- First contentful paint < 1.5s - Role-based access control
- Time to interactive < 3.5s - Input validation
- 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
- CSRF protection - CSRF protection
- Rate limiting (planned) - Content Security Policy
- Secure HTTP headers
## Additional Notes - Rate limiting
- Custom CSS variables for theming - Data sanitization
- Dark mode support via media query - Secure password handling
- Mobile-first responsive design
- Component-based architecture
- Accessibility-first approach
- Server-side rendering for SEO

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 - **Animation**: Framer Motion 11 for UI transitions
- **Routing**: Next.js App Router with nested layouts - **Routing**: Next.js App Router with nested layouts
- **Interactive Components**: - **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 - 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 ### Backend Architecture
- **Framework**: Node.js with Express 4.21 - **Framework**: Node.js with Express 4.18.2
- **API Design**: RESTful endpoints - **API Design**: RESTful endpoints
- **Authentication**: - **Authentication**:
- JWT-based authentication for admin access - JWT-based authentication for admin access