phase 6 ,7,9
This commit is contained in:
241
PHASE-5-7-9-VERIFICATION-REPORT.md
Normal file
241
PHASE-5-7-9-VERIFICATION-REPORT.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# PHASE 5-7-9 VERIFICATION REPORT
|
||||||
|
**Date**: Generated automatically
|
||||||
|
**Document**: `PHASE-5-7-9-SITES-RENDERER-INTEGRATION-UI.md`
|
||||||
|
|
||||||
|
## SUMMARY
|
||||||
|
|
||||||
|
**Overall Completion**: ~75% Complete
|
||||||
|
|
||||||
|
### ✅ COMPLETED SECTIONS
|
||||||
|
|
||||||
|
#### Phase 5: Sites Renderer & Bulk Generation (80% Complete)
|
||||||
|
- ✅ **Sites Container**: Docker Compose configured (`igny8_sites` service)
|
||||||
|
- ✅ **Sites Renderer Frontend**: Complete (`sites/src/`)
|
||||||
|
- ✅ **PublisherService**: Implemented with multi-destination support
|
||||||
|
- ✅ **SitesRendererAdapter**: Implemented
|
||||||
|
- ✅ **DeploymentService**: Implemented
|
||||||
|
- ✅ **PublishingRecord Model**: Implemented
|
||||||
|
- ✅ **DeploymentRecord Model**: Implemented
|
||||||
|
- ✅ **Publisher ViewSet**: Implemented with endpoints
|
||||||
|
- ✅ **Layout Renderer**: Implemented (`sites/src/utils/layoutRenderer.tsx`)
|
||||||
|
- ✅ **Template System**: Implemented (`sites/src/utils/templateEngine.tsx`)
|
||||||
|
- ✅ **File Access Integration**: Implemented (`sites/src/utils/fileAccess.ts`)
|
||||||
|
- ✅ **Site Definition Loader**: Implemented (`sites/src/loaders/loadSiteDefinition.ts`)
|
||||||
|
|
||||||
|
**MISSING**:
|
||||||
|
- ❌ **Bulk Page Generation**: `bulk_generate_pages()` method not found in `PageGenerationService`
|
||||||
|
- ❌ **Bulk Generation API**: `generate_all_pages` action not found in `SiteBlueprintViewSet`
|
||||||
|
- ❌ **Bulk Generation UI**: "Generate All Pages" button not implemented in Site Builder
|
||||||
|
- ❌ **Page Selection UI**: Checkbox selection for pages not implemented
|
||||||
|
- ❌ **Progress Tracking**: Bulk generation progress tracking not implemented
|
||||||
|
|
||||||
|
#### Phase 6: Site Integration & Multi-Destination Publishing (95% Complete)
|
||||||
|
- ✅ **SiteIntegration Model**: Implemented with all required fields
|
||||||
|
- ✅ **IntegrationService**: Implemented with CRUD operations
|
||||||
|
- ✅ **SyncService**: Implemented for two-way sync
|
||||||
|
- ✅ **ContentSyncService**: Implemented
|
||||||
|
- ✅ **BaseAdapter**: Implemented as abstract base class
|
||||||
|
- ✅ **WordPressAdapter**: Implemented
|
||||||
|
- ✅ **SitesRendererAdapter**: Implemented
|
||||||
|
- ✅ **ShopifyAdapter**: Implemented (skeleton)
|
||||||
|
- ✅ **PublisherService Multi-Destination**: Extended with `publish_to_multiple_destinations()`
|
||||||
|
- ✅ **Site Model Extensions**: `site_type` and `hosting_type` fields added
|
||||||
|
- ✅ **Integration ViewSet**: Implemented with all endpoints
|
||||||
|
- ✅ **Integration UI**: Updated with `SiteIntegrationsSection`
|
||||||
|
- ✅ **Publishing Settings UI**: Created (`frontend/src/pages/Settings/Publishing.tsx`)
|
||||||
|
- ✅ **PublishingRules Component**: Created
|
||||||
|
- ✅ **Site Management Dashboard (Core)**: Implemented (`Manage.tsx`)
|
||||||
|
- ✅ **Writer UI Integration**: Site Builder tasks filtering implemented
|
||||||
|
|
||||||
|
**MISSING**:
|
||||||
|
- ❌ **Site Content Editor (Core)**: `Editor.tsx` exists but may need enhancement
|
||||||
|
- ❌ **Post Editor**: Not found (`PostEditor.tsx`)
|
||||||
|
- ❌ **Page Manager (Core)**: `PageManager.tsx` exists but may need enhancement
|
||||||
|
- ❌ **Site Settings (Core)**: `Settings.tsx` exists but may need enhancement
|
||||||
|
|
||||||
|
#### Phase 7: UI Components & Prompt Management (70% Complete)
|
||||||
|
- ✅ **Component Library Structure**: Complete (12 blocks, 7 layouts, 6 templates)
|
||||||
|
- ✅ **All Block Components**: All 12 blocks implemented
|
||||||
|
- ✅ **All Layout Components**: All 7 layouts implemented
|
||||||
|
- ✅ **All Template Components**: All 6 templates implemented
|
||||||
|
- ✅ **Component Documentation**: README.md updated
|
||||||
|
- ✅ **Prompt Management UI**: Complete
|
||||||
|
- ✅ Backend: `site_structure_generation` added to `AIPrompt.PROMPT_TYPE_CHOICES`
|
||||||
|
- ✅ Migration: Created
|
||||||
|
- ✅ Frontend: Added to `PROMPT_TYPES`
|
||||||
|
- ✅ Site Builder section: Added to Prompts page
|
||||||
|
- ✅ Prompt editor: Implemented with variable documentation
|
||||||
|
- ✅ **Site List View**: Implemented with filters (`List.tsx`)
|
||||||
|
- ✅ **Site Dashboard (Advanced)**: Implemented (`Dashboard.tsx`)
|
||||||
|
|
||||||
|
**MISSING**:
|
||||||
|
- ❌ **Site Content Manager (Advanced)**: Not found (`Content.tsx`)
|
||||||
|
- ❌ **Post Editor (Advanced)**: Not found (`PostEditor.tsx`)
|
||||||
|
- ❌ **Page Manager (Advanced)**: `PageManager.tsx` exists but may need drag-drop, bulk actions
|
||||||
|
- ❌ **Site Settings (Advanced + SEO)**: `Settings.tsx` exists but may need SEO features
|
||||||
|
- ❌ **Site Preview**: Not found (`Preview.tsx`)
|
||||||
|
- ❌ **Layout Selector Component**: Not found (`LayoutSelector.tsx`)
|
||||||
|
- ❌ **Template Library Component**: Not found (`TemplateLibrary.tsx`)
|
||||||
|
- ❌ **Layout Preview Component**: Not found (`LayoutPreview.tsx`)
|
||||||
|
- ❌ **Template Customizer Component**: Not found (`TemplateCustomizer.tsx`)
|
||||||
|
- ❌ **Style Editor Component**: Not found (`StyleEditor.tsx`)
|
||||||
|
- ❌ **CMS Theme System**: Not found (`frontend/src/styles/cms/`)
|
||||||
|
- ❌ **Style Presets**: Not found
|
||||||
|
- ❌ **Color Schemes**: Not found
|
||||||
|
- ❌ **Typography System**: Not found
|
||||||
|
- ❌ **Component Styles**: Not found
|
||||||
|
- ❌ **Component Tests**: Not found
|
||||||
|
- ❌ **Component Storybook**: Not found (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DETAILED CHECKLIST
|
||||||
|
|
||||||
|
### Phase 5: Sites Renderer & Bulk Generation
|
||||||
|
|
||||||
|
#### Backend Tasks
|
||||||
|
- [x] Create PublisherService
|
||||||
|
- [x] Create SitesRendererAdapter
|
||||||
|
- [x] Create DeploymentService
|
||||||
|
- [x] Create PublishingRecord model
|
||||||
|
- [x] Create DeploymentRecord model
|
||||||
|
- [x] Create Publisher ViewSet
|
||||||
|
- [ ] Add `bulk_generate_pages()` to PageGenerationService
|
||||||
|
- [ ] Add `create_tasks_for_pages()` to PageGenerationService
|
||||||
|
- [ ] Add `generate_all_pages` action to SiteBlueprintViewSet
|
||||||
|
- [ ] Add `create_tasks` action to SiteBlueprintViewSet
|
||||||
|
- [x] Database migrations
|
||||||
|
|
||||||
|
#### Frontend Tasks
|
||||||
|
- [x] Create Sites container in docker-compose
|
||||||
|
- [x] Create sites renderer frontend
|
||||||
|
- [x] Create site definition loader
|
||||||
|
- [x] Create layout renderer
|
||||||
|
- [x] Create template system
|
||||||
|
- [x] Import shared components
|
||||||
|
- [x] Integrate file access
|
||||||
|
- [ ] Add "Generate All Pages" button to Site Builder
|
||||||
|
- [ ] Add page selection UI
|
||||||
|
- [ ] Add progress tracking
|
||||||
|
- [ ] Add bulk generation API methods
|
||||||
|
|
||||||
|
### Phase 6: Site Integration & Multi-Destination Publishing
|
||||||
|
|
||||||
|
#### Backend Tasks
|
||||||
|
- [x] Create SiteIntegration model
|
||||||
|
- [x] Create IntegrationService
|
||||||
|
- [x] Create SyncService
|
||||||
|
- [x] Create ContentSyncService
|
||||||
|
- [x] Create BaseAdapter
|
||||||
|
- [x] Refactor WordPressAdapter (implemented as new adapter)
|
||||||
|
- [x] Create SitesRendererAdapter
|
||||||
|
- [x] Create ShopifyAdapter (skeleton)
|
||||||
|
- [x] Extend PublisherService for multi-destination
|
||||||
|
- [x] Extend Site model (site_type, hosting_type)
|
||||||
|
- [x] Create Integration ViewSet
|
||||||
|
- [x] Database migrations
|
||||||
|
|
||||||
|
#### Frontend Tasks
|
||||||
|
- [x] Update Integration Settings page
|
||||||
|
- [x] Create Publishing Settings page
|
||||||
|
- [x] Create Site Management Dashboard (core)
|
||||||
|
- [x] Create Site Content Editor (core) - exists but may need enhancement
|
||||||
|
- [x] Create Page Manager (core) - exists but may need enhancement
|
||||||
|
- [x] Create Site Settings (core) - exists but may need enhancement
|
||||||
|
- [x] Create PlatformSelector component
|
||||||
|
- [x] Create IntegrationStatus component
|
||||||
|
- [x] Create PublishingRules component
|
||||||
|
- [x] Update Writer UI for Site Builder tasks
|
||||||
|
|
||||||
|
### Phase 7: UI Components & Prompt Management
|
||||||
|
|
||||||
|
#### Backend Tasks
|
||||||
|
- [x] Add site_structure_generation to AIPrompt.PROMPT_TYPE_CHOICES
|
||||||
|
- [x] Create migration for new prompt type
|
||||||
|
|
||||||
|
#### Frontend Tasks
|
||||||
|
- [x] Complete component library (blocks, layouts, templates)
|
||||||
|
- [x] Add site_structure_generation to PROMPT_TYPES
|
||||||
|
- [x] Add "Site Builder" section to Prompts page
|
||||||
|
- [x] Add prompt editor for site structure generation
|
||||||
|
- [x] Create Site List View
|
||||||
|
- [x] Create Site Dashboard (advanced)
|
||||||
|
- [ ] Create Site Content Manager (advanced)
|
||||||
|
- [ ] Create Post Editor (advanced)
|
||||||
|
- [ ] Create Page Manager (advanced) - exists but may need enhancement
|
||||||
|
- [ ] Create Site Settings (advanced + SEO) - exists but may need enhancement
|
||||||
|
- [ ] Create Site Preview
|
||||||
|
- [ ] Create Layout Selector
|
||||||
|
- [ ] Create Template Library
|
||||||
|
- [ ] Create Layout Preview
|
||||||
|
- [ ] Create Template Customizer
|
||||||
|
- [ ] Create Style Editor
|
||||||
|
- [ ] Create CMS Theme System
|
||||||
|
- [ ] Create Style Presets
|
||||||
|
- [ ] Create Color Schemes
|
||||||
|
- [ ] Create Typography System
|
||||||
|
- [ ] Create Component Styles
|
||||||
|
- [ ] Write component tests
|
||||||
|
- [ ] Write component documentation (README exists, but may need expansion)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL MISSING FEATURES
|
||||||
|
|
||||||
|
### High Priority
|
||||||
|
1. **Bulk Page Generation** (Phase 5)
|
||||||
|
- Backend: `bulk_generate_pages()` method
|
||||||
|
- Backend: `create_tasks_for_pages()` method
|
||||||
|
- Backend: API endpoints for bulk generation
|
||||||
|
- Frontend: UI for bulk generation in Site Builder
|
||||||
|
|
||||||
|
2. **Advanced Site Management** (Phase 7)
|
||||||
|
- Site Content Manager with search/filters
|
||||||
|
- Post Editor (full-featured)
|
||||||
|
- Site Preview (live preview with iframe)
|
||||||
|
- Advanced Page Manager (drag-drop, bulk actions)
|
||||||
|
- Advanced Site Settings (SEO, meta tags, Open Graph, schema)
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
3. **Layout & Template System UI** (Phase 7)
|
||||||
|
- Layout Selector component
|
||||||
|
- Template Library component
|
||||||
|
- Layout Preview component
|
||||||
|
- Template Customizer component
|
||||||
|
- Style Editor component
|
||||||
|
|
||||||
|
4. **CMS Styling System** (Phase 7)
|
||||||
|
- Theme system
|
||||||
|
- Style presets
|
||||||
|
- Color schemes
|
||||||
|
- Typography system
|
||||||
|
- Component styles
|
||||||
|
|
||||||
|
### Low Priority
|
||||||
|
5. **Testing & Documentation** (Phase 7)
|
||||||
|
- Component tests
|
||||||
|
- Component Storybook (optional)
|
||||||
|
- Expanded component documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RECOMMENDATIONS
|
||||||
|
|
||||||
|
1. **Complete Bulk Generation** (Phase 5): This is a core feature for Site Builder workflow
|
||||||
|
2. **Enhance Existing Components**: Review `Editor.tsx`, `PageManager.tsx`, `Settings.tsx` to ensure they meet "advanced" requirements
|
||||||
|
3. **Implement Site Preview**: Critical for user experience
|
||||||
|
4. **Add CMS Styling System**: Important for customization capabilities
|
||||||
|
5. **Create Layout/Template UI Components**: Needed for user-friendly site building
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CONCLUSION
|
||||||
|
|
||||||
|
The implementation is **~75% complete** with core infrastructure in place. The main gaps are:
|
||||||
|
- Bulk page generation functionality (Phase 5)
|
||||||
|
- Advanced site management features (Phase 7)
|
||||||
|
- Layout/template UI components (Phase 7)
|
||||||
|
- CMS styling system (Phase 7)
|
||||||
|
|
||||||
|
Most backend infrastructure is complete. The remaining work is primarily frontend UI components and advanced features.
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated manually for Phase 7: Prompt Management UI
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0007_add_module_enable_settings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aiprompt',
|
||||||
|
name='prompt_type',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('clustering', 'Clustering'),
|
||||||
|
('ideas', 'Ideas Generation'),
|
||||||
|
('content_generation', 'Content Generation'),
|
||||||
|
('image_prompt_extraction', 'Image Prompt Extraction'),
|
||||||
|
('image_prompt_template', 'Image Prompt Template'),
|
||||||
|
('negative_prompt', 'Negative Prompt'),
|
||||||
|
('site_structure_generation', 'Site Structure Generation'),
|
||||||
|
],
|
||||||
|
db_index=True,
|
||||||
|
max_length=50
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ class AIPrompt(AccountBaseModel):
|
|||||||
('image_prompt_extraction', 'Image Prompt Extraction'),
|
('image_prompt_extraction', 'Image Prompt Extraction'),
|
||||||
('image_prompt_template', 'Image Prompt Template'),
|
('image_prompt_template', 'Image Prompt Template'),
|
||||||
('negative_prompt', 'Negative Prompt'),
|
('negative_prompt', 'Negative Prompt'),
|
||||||
|
('site_structure_generation', 'Site Structure Generation'), # Phase 7: Site Builder prompts
|
||||||
]
|
]
|
||||||
|
|
||||||
prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True)
|
prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True)
|
||||||
|
|||||||
@@ -15,10 +15,41 @@ These components are designed to be framework-agnostic where possible, but curre
|
|||||||
|
|
||||||
```
|
```
|
||||||
shared/
|
shared/
|
||||||
├── blocks/ # Content blocks (HeroBlock, FeatureGridBlock, StatsPanel)
|
├── blocks/ # Content blocks (12 total)
|
||||||
├── layouts/ # Page layouts (DefaultLayout, MinimalLayout)
|
│ ├── HeroBlock.tsx
|
||||||
├── templates/ # Full page templates (MarketingTemplate, LandingTemplate)
|
│ ├── FeatureGridBlock.tsx
|
||||||
└── index.ts # Barrel exports
|
│ ├── StatsPanel.tsx
|
||||||
|
│ ├── ServicesBlock.tsx
|
||||||
|
│ ├── ProductsBlock.tsx
|
||||||
|
│ ├── TestimonialsBlock.tsx
|
||||||
|
│ ├── ContactFormBlock.tsx
|
||||||
|
│ ├── CTABlock.tsx
|
||||||
|
│ ├── ImageGalleryBlock.tsx
|
||||||
|
│ ├── VideoBlock.tsx
|
||||||
|
│ ├── TextBlock.tsx
|
||||||
|
│ ├── QuoteBlock.tsx
|
||||||
|
│ ├── blocks.css
|
||||||
|
│ └── index.ts
|
||||||
|
├── layouts/ # Page layouts (7 total)
|
||||||
|
│ ├── DefaultLayout.tsx
|
||||||
|
│ ├── MinimalLayout.tsx
|
||||||
|
│ ├── MagazineLayout.tsx
|
||||||
|
│ ├── EcommerceLayout.tsx
|
||||||
|
│ ├── PortfolioLayout.tsx
|
||||||
|
│ ├── BlogLayout.tsx
|
||||||
|
│ ├── CorporateLayout.tsx
|
||||||
|
│ ├── layouts.css
|
||||||
|
│ └── index.ts
|
||||||
|
├── templates/ # Full page templates (6 total)
|
||||||
|
│ ├── MarketingTemplate.tsx
|
||||||
|
│ ├── LandingTemplate.tsx
|
||||||
|
│ ├── BlogTemplate.tsx
|
||||||
|
│ ├── BusinessTemplate.tsx
|
||||||
|
│ ├── PortfolioTemplate.tsx
|
||||||
|
│ ├── EcommerceTemplate.tsx
|
||||||
|
│ └── index.ts
|
||||||
|
├── index.ts # Barrel exports
|
||||||
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -102,6 +133,108 @@ Statistics display component.
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `ServicesBlock`
|
||||||
|
Display services or offerings in a grid layout.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Section title
|
||||||
|
- `subtitle?: string` - Section subtitle
|
||||||
|
- `services: Array<{ title: string; description: string; icon?: ReactNode; imageUrl?: string }>` - Service items
|
||||||
|
- `columns?: 2 | 3 | 4` - Number of columns
|
||||||
|
- `variant?: 'default' | 'card' | 'minimal'` - Display variant
|
||||||
|
|
||||||
|
#### `ProductsBlock`
|
||||||
|
Display products in a grid or list layout.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Section title
|
||||||
|
- `subtitle?: string` - Section subtitle
|
||||||
|
- `products: Array<{ name: string; description?: string; price?: string; imageUrl?: string; ctaLabel?: string }>` - Product items
|
||||||
|
- `columns?: 2 | 3 | 4` - Number of columns
|
||||||
|
- `variant?: 'grid' | 'list' | 'carousel'` - Display variant
|
||||||
|
|
||||||
|
#### `TestimonialsBlock`
|
||||||
|
Display customer testimonials with ratings and author info.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Section title
|
||||||
|
- `subtitle?: string` - Section subtitle
|
||||||
|
- `testimonials: Array<{ quote: string; author: string; role?: string; company?: string; avatarUrl?: string; rating?: number }>` - Testimonial items
|
||||||
|
- `columns?: 1 | 2 | 3` - Number of columns
|
||||||
|
- `variant?: 'default' | 'card' | 'minimal'` - Display variant
|
||||||
|
|
||||||
|
#### `ContactFormBlock`
|
||||||
|
Contact form with customizable fields.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Form title
|
||||||
|
- `subtitle?: string` - Form subtitle
|
||||||
|
- `fields?: Array<{ name: string; label: string; type: 'text' | 'email' | 'tel' | 'textarea'; required?: boolean; placeholder?: string }>` - Form fields
|
||||||
|
- `submitLabel?: string` - Submit button label
|
||||||
|
- `onSubmit?: (data: Record<string, string>) => void` - Submit handler
|
||||||
|
|
||||||
|
#### `CTABlock`
|
||||||
|
Call-to-action section with primary and secondary actions.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title: string` - CTA title
|
||||||
|
- `subtitle?: string` - CTA subtitle
|
||||||
|
- `primaryCtaLabel?: string` - Primary button label
|
||||||
|
- `primaryCtaLink?: string` - Primary button link
|
||||||
|
- `onPrimaryCtaClick?: () => void` - Primary button click handler
|
||||||
|
- `secondaryCtaLabel?: string` - Secondary button label
|
||||||
|
- `secondaryCtaLink?: string` - Secondary button link
|
||||||
|
- `onSecondaryCtaClick?: () => void` - Secondary button click handler
|
||||||
|
- `backgroundImage?: string` - Background image URL
|
||||||
|
- `variant?: 'default' | 'centered' | 'split'` - Layout variant
|
||||||
|
|
||||||
|
#### `ImageGalleryBlock`
|
||||||
|
Image gallery with grid, masonry, or carousel layout.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Gallery title
|
||||||
|
- `subtitle?: string` - Gallery subtitle
|
||||||
|
- `images: Array<{ url: string; alt?: string; caption?: string; thumbnailUrl?: string }>` - Image items
|
||||||
|
- `columns?: 2 | 3 | 4` - Number of columns
|
||||||
|
- `variant?: 'grid' | 'masonry' | 'carousel'` - Display variant
|
||||||
|
- `lightbox?: boolean` - Enable lightbox on click
|
||||||
|
|
||||||
|
#### `VideoBlock`
|
||||||
|
Video player component supporting both video URLs and embed codes.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Video title
|
||||||
|
- `subtitle?: string` - Video subtitle
|
||||||
|
- `videoUrl?: string` - Video file URL
|
||||||
|
- `embedCode?: string` - HTML embed code (e.g., YouTube iframe)
|
||||||
|
- `thumbnailUrl?: string` - Video thumbnail/poster
|
||||||
|
- `autoplay?: boolean` - Autoplay video
|
||||||
|
- `controls?: boolean` - Show video controls
|
||||||
|
- `loop?: boolean` - Loop video
|
||||||
|
- `muted?: boolean` - Mute video
|
||||||
|
- `variant?: 'default' | 'fullwidth' | 'centered'` - Layout variant
|
||||||
|
|
||||||
|
#### `TextBlock`
|
||||||
|
Simple text content block with customizable alignment and width.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title?: string` - Block title
|
||||||
|
- `content: string | ReactNode` - Text content (HTML string or React nodes)
|
||||||
|
- `align?: 'left' | 'center' | 'right' | 'justify'` - Text alignment
|
||||||
|
- `variant?: 'default' | 'narrow' | 'wide' | 'fullwidth'` - Width variant
|
||||||
|
|
||||||
|
#### `QuoteBlock`
|
||||||
|
Quote/testimonial block with author information.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `quote: string` - Quote text
|
||||||
|
- `author?: string` - Author name
|
||||||
|
- `role?: string` - Author role
|
||||||
|
- `company?: string` - Author company
|
||||||
|
- `avatarUrl?: string` - Author avatar image
|
||||||
|
- `variant?: 'default' | 'large' | 'minimal' | 'card'` - Display variant
|
||||||
|
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
||||||
|
|
||||||
### Layouts
|
### Layouts
|
||||||
|
|
||||||
#### `DefaultLayout`
|
#### `DefaultLayout`
|
||||||
@@ -125,15 +258,64 @@ Minimal layout for focused content pages.
|
|||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
- `children: ReactNode` - Page content
|
- `children: ReactNode` - Page content
|
||||||
- `maxWidth?: string` - Maximum content width
|
- `background?: 'light' | 'dark'` - Background color
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```tsx
|
```tsx
|
||||||
<MinimalLayout maxWidth="800px">
|
<MinimalLayout background="light">
|
||||||
<ArticleContent />
|
<ArticleContent />
|
||||||
</MinimalLayout>
|
</MinimalLayout>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `MagazineLayout`
|
||||||
|
Magazine-style layout with featured section and sidebar.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `featured?: ReactNode` - Featured content section
|
||||||
|
- `sidebar?: ReactNode` - Sidebar content
|
||||||
|
- `mainContent: ReactNode` - Main content area
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
|
||||||
|
#### `EcommerceLayout`
|
||||||
|
E-commerce layout with navigation and product sidebar.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `navigation?: ReactNode` - Navigation menu
|
||||||
|
- `sidebar?: ReactNode` - Sidebar (filters, categories)
|
||||||
|
- `mainContent: ReactNode` - Main content (products)
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
|
||||||
|
#### `PortfolioLayout`
|
||||||
|
Portfolio layout optimized for showcasing work.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `mainContent: ReactNode` - Main content (gallery, projects)
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
- `fullWidth?: boolean` - Full-width layout option
|
||||||
|
|
||||||
|
#### `BlogLayout`
|
||||||
|
Blog layout with optional sidebar (left or right).
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `sidebar?: ReactNode` - Sidebar content
|
||||||
|
- `mainContent: ReactNode` - Main content (blog posts)
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
- `sidebarPosition?: 'left' | 'right'` - Sidebar position
|
||||||
|
|
||||||
|
#### `CorporateLayout`
|
||||||
|
Corporate/business layout with navigation and structured sections.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `navigation?: ReactNode` - Navigation menu
|
||||||
|
- `mainContent: ReactNode` - Main content
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
- `sidebar?: ReactNode` - Optional sidebar
|
||||||
|
|
||||||
### Templates
|
### Templates
|
||||||
|
|
||||||
#### `MarketingTemplate`
|
#### `MarketingTemplate`
|
||||||
@@ -169,6 +351,51 @@ Landing page template optimized for conversions.
|
|||||||
**Props:**
|
**Props:**
|
||||||
- Similar to `MarketingTemplate` with additional conversion-focused sections
|
- Similar to `MarketingTemplate` with additional conversion-focused sections
|
||||||
|
|
||||||
|
#### `BlogTemplate`
|
||||||
|
Blog page template with posts and sidebar.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `sidebar?: ReactNode` - Sidebar content
|
||||||
|
- `posts: ReactNode` - Blog posts content
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
- `sidebarPosition?: 'left' | 'right'` - Sidebar position
|
||||||
|
|
||||||
|
#### `BusinessTemplate`
|
||||||
|
Business/corporate page template.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `navigation?: ReactNode` - Navigation menu
|
||||||
|
- `hero?: ReactNode` - Hero section
|
||||||
|
- `features?: ReactNode` - Features section
|
||||||
|
- `services?: ReactNode` - Services section
|
||||||
|
- `testimonials?: ReactNode` - Testimonials section
|
||||||
|
- `cta?: ReactNode` - Call-to-action section
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
|
||||||
|
#### `PortfolioTemplate`
|
||||||
|
Portfolio showcase template.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `gallery?: ReactNode` - Portfolio gallery
|
||||||
|
- `about?: ReactNode` - About section
|
||||||
|
- `contact?: ReactNode` - Contact section
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
- `fullWidth?: boolean` - Full-width layout
|
||||||
|
|
||||||
|
#### `EcommerceTemplate`
|
||||||
|
E-commerce store template.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `header?: ReactNode` - Page header
|
||||||
|
- `navigation?: ReactNode` - Navigation menu
|
||||||
|
- `sidebar?: ReactNode` - Sidebar (filters)
|
||||||
|
- `products?: ReactNode` - Products grid
|
||||||
|
- `featured?: ReactNode` - Featured products
|
||||||
|
- `footer?: ReactNode` - Page footer
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
Components use CSS modules and shared CSS files:
|
Components use CSS modules and shared CSS files:
|
||||||
|
|||||||
76
frontend/src/components/shared/blocks/CTABlock.tsx
Normal file
76
frontend/src/components/shared/blocks/CTABlock.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface CTABlockProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
primaryCtaLabel?: string;
|
||||||
|
primaryCtaLink?: string;
|
||||||
|
onPrimaryCtaClick?: () => void;
|
||||||
|
secondaryCtaLabel?: string;
|
||||||
|
secondaryCtaLink?: string;
|
||||||
|
onSecondaryCtaClick?: () => void;
|
||||||
|
backgroundImage?: string;
|
||||||
|
variant?: 'default' | 'centered' | 'split';
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CTABlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
primaryCtaLabel,
|
||||||
|
primaryCtaLink,
|
||||||
|
onPrimaryCtaClick,
|
||||||
|
secondaryCtaLabel,
|
||||||
|
secondaryCtaLink,
|
||||||
|
onSecondaryCtaClick,
|
||||||
|
backgroundImage,
|
||||||
|
variant = 'default',
|
||||||
|
children
|
||||||
|
}: CTABlockProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`shared-cta shared-cta--${variant}`}
|
||||||
|
style={backgroundImage ? { backgroundImage: `url(${backgroundImage})` } : undefined}
|
||||||
|
>
|
||||||
|
<div className="shared-cta__content">
|
||||||
|
<h2 className="shared-cta__title">{title}</h2>
|
||||||
|
{subtitle && <p className="shared-cta__subtitle">{subtitle}</p>}
|
||||||
|
{children && <div className="shared-cta__children">{children}</div>}
|
||||||
|
<div className="shared-cta__actions">
|
||||||
|
{primaryCtaLabel && (
|
||||||
|
primaryCtaLink ? (
|
||||||
|
<a href={primaryCtaLink} className="shared-button shared-button--primary">
|
||||||
|
{primaryCtaLabel}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shared-button shared-button--primary"
|
||||||
|
onClick={onPrimaryCtaClick}
|
||||||
|
>
|
||||||
|
{primaryCtaLabel}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{secondaryCtaLabel && (
|
||||||
|
secondaryCtaLink ? (
|
||||||
|
<a href={secondaryCtaLink} className="shared-button shared-button--secondary">
|
||||||
|
{secondaryCtaLabel}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shared-button shared-button--secondary"
|
||||||
|
onClick={onSecondaryCtaClick}
|
||||||
|
>
|
||||||
|
{secondaryCtaLabel}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
83
frontend/src/components/shared/blocks/ContactFormBlock.tsx
Normal file
83
frontend/src/components/shared/blocks/ContactFormBlock.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface ContactFormBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
fields?: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: 'text' | 'email' | 'tel' | 'textarea';
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}>;
|
||||||
|
submitLabel?: string;
|
||||||
|
onSubmit?: (data: Record<string, string>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContactFormBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
fields = [
|
||||||
|
{ name: 'name', label: 'Name', type: 'text', required: true },
|
||||||
|
{ name: 'email', label: 'Email', type: 'email', required: true },
|
||||||
|
{ name: 'message', label: 'Message', type: 'textarea', required: true },
|
||||||
|
],
|
||||||
|
submitLabel = 'Submit',
|
||||||
|
onSubmit
|
||||||
|
}: ContactFormBlockProps) {
|
||||||
|
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const handleChange = (name: string, value: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit?.(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="shared-contact-form">
|
||||||
|
{title && <h2 className="shared-contact-form__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-contact-form__subtitle">{subtitle}</p>}
|
||||||
|
<form onSubmit={handleSubmit} className="shared-contact-form__form">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<div key={field.name} className="shared-contact-form__field">
|
||||||
|
<label htmlFor={field.name} className="shared-contact-form__label">
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="shared-contact-form__required">*</span>}
|
||||||
|
</label>
|
||||||
|
{field.type === 'textarea' ? (
|
||||||
|
<textarea
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
required={field.required}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
value={formData[field.name] || ''}
|
||||||
|
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||||
|
className="shared-contact-form__input"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type={field.type}
|
||||||
|
required={field.required}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
value={formData[field.name] || ''}
|
||||||
|
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||||
|
className="shared-contact-form__input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button type="submit" className="shared-button shared-button--primary">
|
||||||
|
{submitLabel}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
66
frontend/src/components/shared/blocks/ImageGalleryBlock.tsx
Normal file
66
frontend/src/components/shared/blocks/ImageGalleryBlock.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface ImageGalleryBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
images: Array<{
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
caption?: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
}>;
|
||||||
|
columns?: 2 | 3 | 4;
|
||||||
|
variant?: 'grid' | 'masonry' | 'carousel';
|
||||||
|
lightbox?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageGalleryBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
images,
|
||||||
|
columns = 3,
|
||||||
|
variant = 'grid',
|
||||||
|
lightbox = false
|
||||||
|
}: ImageGalleryBlockProps) {
|
||||||
|
const [selectedImage, setSelectedImage] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`shared-image-gallery shared-image-gallery--${variant}`}>
|
||||||
|
{title && <h2 className="shared-image-gallery__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-image-gallery__subtitle">{subtitle}</p>}
|
||||||
|
<div className={`shared-image-gallery__grid shared-image-gallery__grid--${columns}`}>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="shared-image-gallery__item"
|
||||||
|
onClick={() => lightbox && setSelectedImage(index)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={image.thumbnailUrl || image.url}
|
||||||
|
alt={image.alt || image.caption || `Image ${index + 1}`}
|
||||||
|
className="shared-image-gallery__image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{image.caption && (
|
||||||
|
<p className="shared-image-gallery__caption">{image.caption}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{lightbox && selectedImage !== null && (
|
||||||
|
<div
|
||||||
|
className="shared-image-gallery__lightbox"
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={images[selectedImage].url}
|
||||||
|
alt={images[selectedImage].alt || images[selectedImage].caption || 'Lightbox image'}
|
||||||
|
className="shared-image-gallery__lightbox-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
58
frontend/src/components/shared/blocks/ProductsBlock.tsx
Normal file
58
frontend/src/components/shared/blocks/ProductsBlock.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface ProductItem {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
price?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
ctaLabel?: string;
|
||||||
|
onCtaClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductsBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
products: ProductItem[];
|
||||||
|
columns?: 2 | 3 | 4;
|
||||||
|
variant?: 'grid' | 'list' | 'carousel';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductsBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
products,
|
||||||
|
columns = 3,
|
||||||
|
variant = 'grid'
|
||||||
|
}: ProductsBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-products shared-products--${variant}`}>
|
||||||
|
{title && <h2 className="shared-products__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-products__subtitle">{subtitle}</p>}
|
||||||
|
<div className={`shared-products__grid shared-products__grid--${columns}`}>
|
||||||
|
{products.map((product, index) => (
|
||||||
|
<div key={index} className="shared-products__item">
|
||||||
|
{product.imageUrl && (
|
||||||
|
<img src={product.imageUrl} alt={product.name} className="shared-products__image" />
|
||||||
|
)}
|
||||||
|
<h3 className="shared-products__name">{product.name}</h3>
|
||||||
|
{product.description && (
|
||||||
|
<p className="shared-products__description">{product.description}</p>
|
||||||
|
)}
|
||||||
|
{product.price && <p className="shared-products__price">{product.price}</p>}
|
||||||
|
{product.ctaLabel && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shared-button"
|
||||||
|
onClick={product.onCtaClick}
|
||||||
|
>
|
||||||
|
{product.ctaLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
53
frontend/src/components/shared/blocks/QuoteBlock.tsx
Normal file
53
frontend/src/components/shared/blocks/QuoteBlock.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface QuoteBlockProps {
|
||||||
|
quote: string;
|
||||||
|
author?: string;
|
||||||
|
role?: string;
|
||||||
|
company?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
variant?: 'default' | 'large' | 'minimal' | 'card';
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuoteBlock({
|
||||||
|
quote,
|
||||||
|
author,
|
||||||
|
role,
|
||||||
|
company,
|
||||||
|
avatarUrl,
|
||||||
|
variant = 'default',
|
||||||
|
align = 'left'
|
||||||
|
}: QuoteBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-quote-block shared-quote-block--${variant}`}>
|
||||||
|
<blockquote
|
||||||
|
className="shared-quote-block__quote"
|
||||||
|
style={{ textAlign: align }}
|
||||||
|
>
|
||||||
|
{quote}
|
||||||
|
</blockquote>
|
||||||
|
{author && (
|
||||||
|
<div className="shared-quote-block__author">
|
||||||
|
{avatarUrl && (
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={author}
|
||||||
|
className="shared-quote-block__avatar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="shared-quote-block__author-name">{author}</p>
|
||||||
|
{role && (
|
||||||
|
<p className="shared-quote-block__author-role">
|
||||||
|
{role}
|
||||||
|
{company && ` at ${company}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
45
frontend/src/components/shared/blocks/ServicesBlock.tsx
Normal file
45
frontend/src/components/shared/blocks/ServicesBlock.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface ServiceItem {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
imageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServicesBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
services: ServiceItem[];
|
||||||
|
columns?: 2 | 3 | 4;
|
||||||
|
variant?: 'default' | 'card' | 'minimal';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServicesBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
services,
|
||||||
|
columns = 3,
|
||||||
|
variant = 'default'
|
||||||
|
}: ServicesBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-services shared-services--${variant}`}>
|
||||||
|
{title && <h2 className="shared-services__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-services__subtitle">{subtitle}</p>}
|
||||||
|
<div className={`shared-services__grid shared-services__grid--${columns}`}>
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<div key={index} className="shared-services__item">
|
||||||
|
{service.icon && <div className="shared-services__icon">{service.icon}</div>}
|
||||||
|
{service.imageUrl && (
|
||||||
|
<img src={service.imageUrl} alt={service.title} className="shared-services__image" />
|
||||||
|
)}
|
||||||
|
<h3 className="shared-services__item-title">{service.title}</h3>
|
||||||
|
<p className="shared-services__item-description">{service.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
67
frontend/src/components/shared/blocks/TestimonialsBlock.tsx
Normal file
67
frontend/src/components/shared/blocks/TestimonialsBlock.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface TestimonialItem {
|
||||||
|
quote: string;
|
||||||
|
author: string;
|
||||||
|
role?: string;
|
||||||
|
company?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
rating?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestimonialsBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
testimonials: TestimonialItem[];
|
||||||
|
columns?: 1 | 2 | 3;
|
||||||
|
variant?: 'default' | 'card' | 'minimal';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestimonialsBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
testimonials,
|
||||||
|
columns = 3,
|
||||||
|
variant = 'default'
|
||||||
|
}: TestimonialsBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-testimonials shared-testimonials--${variant}`}>
|
||||||
|
{title && <h2 className="shared-testimonials__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-testimonials__subtitle">{subtitle}</p>}
|
||||||
|
<div className={`shared-testimonials__grid shared-testimonials__grid--${columns}`}>
|
||||||
|
{testimonials.map((testimonial, index) => (
|
||||||
|
<div key={index} className="shared-testimonials__item">
|
||||||
|
{testimonial.rating && (
|
||||||
|
<div className="shared-testimonials__rating">
|
||||||
|
{'★'.repeat(testimonial.rating)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<blockquote className="shared-testimonials__quote">
|
||||||
|
{testimonial.quote}
|
||||||
|
</blockquote>
|
||||||
|
<div className="shared-testimonials__author">
|
||||||
|
{testimonial.avatarUrl && (
|
||||||
|
<img
|
||||||
|
src={testimonial.avatarUrl}
|
||||||
|
alt={testimonial.author}
|
||||||
|
className="shared-testimonials__avatar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="shared-testimonials__author-name">{testimonial.author}</p>
|
||||||
|
{testimonial.role && (
|
||||||
|
<p className="shared-testimonials__author-role">
|
||||||
|
{testimonial.role}
|
||||||
|
{testimonial.company && ` at ${testimonial.company}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
35
frontend/src/components/shared/blocks/TextBlock.tsx
Normal file
35
frontend/src/components/shared/blocks/TextBlock.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface TextBlockProps {
|
||||||
|
title?: string;
|
||||||
|
content: string | ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right' | 'justify';
|
||||||
|
variant?: 'default' | 'narrow' | 'wide' | 'fullwidth';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextBlock({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
align = 'left',
|
||||||
|
variant = 'default',
|
||||||
|
className = ''
|
||||||
|
}: TextBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-text-block shared-text-block--${variant} ${className}`}>
|
||||||
|
{title && <h2 className="shared-text-block__title">{title}</h2>}
|
||||||
|
<div
|
||||||
|
className="shared-text-block__content"
|
||||||
|
style={{ textAlign: align }}
|
||||||
|
>
|
||||||
|
{typeof content === 'string' ? (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
55
frontend/src/components/shared/blocks/VideoBlock.tsx
Normal file
55
frontend/src/components/shared/blocks/VideoBlock.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import './blocks.css';
|
||||||
|
|
||||||
|
export interface VideoBlockProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
embedCode?: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
autoplay?: boolean;
|
||||||
|
controls?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
variant?: 'default' | 'fullwidth' | 'centered';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoBlock({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
videoUrl,
|
||||||
|
embedCode,
|
||||||
|
thumbnailUrl,
|
||||||
|
autoplay = false,
|
||||||
|
controls = true,
|
||||||
|
loop = false,
|
||||||
|
muted = false,
|
||||||
|
variant = 'default'
|
||||||
|
}: VideoBlockProps) {
|
||||||
|
return (
|
||||||
|
<section className={`shared-video shared-video--${variant}`}>
|
||||||
|
{title && <h2 className="shared-video__title">{title}</h2>}
|
||||||
|
{subtitle && <p className="shared-video__subtitle">{subtitle}</p>}
|
||||||
|
<div className="shared-video__container">
|
||||||
|
{embedCode ? (
|
||||||
|
<div
|
||||||
|
className="shared-video__embed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: embedCode }}
|
||||||
|
/>
|
||||||
|
) : videoUrl ? (
|
||||||
|
<video
|
||||||
|
src={videoUrl}
|
||||||
|
controls={controls}
|
||||||
|
autoPlay={autoplay}
|
||||||
|
loop={loop}
|
||||||
|
muted={muted}
|
||||||
|
poster={thumbnailUrl}
|
||||||
|
className="shared-video__player"
|
||||||
|
>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,5 +4,22 @@ export { FeatureGridBlock } from './FeatureGridBlock';
|
|||||||
export type { FeatureGridBlockProps } from './FeatureGridBlock';
|
export type { FeatureGridBlockProps } from './FeatureGridBlock';
|
||||||
export { StatsPanel } from './StatsPanel';
|
export { StatsPanel } from './StatsPanel';
|
||||||
export type { StatsPanelProps, StatItem } from './StatsPanel';
|
export type { StatsPanelProps, StatItem } from './StatsPanel';
|
||||||
|
export { ServicesBlock } from './ServicesBlock';
|
||||||
|
export type { ServicesBlockProps, ServiceItem } from './ServicesBlock';
|
||||||
|
export { ProductsBlock } from './ProductsBlock';
|
||||||
|
export type { ProductsBlockProps, ProductItem } from './ProductsBlock';
|
||||||
|
export { TestimonialsBlock } from './TestimonialsBlock';
|
||||||
|
export type { TestimonialsBlockProps, TestimonialItem } from './TestimonialsBlock';
|
||||||
|
export { ContactFormBlock } from './ContactFormBlock';
|
||||||
|
export type { ContactFormBlockProps } from './ContactFormBlock';
|
||||||
|
export { CTABlock } from './CTABlock';
|
||||||
|
export type { CTABlockProps } from './CTABlock';
|
||||||
|
export { ImageGalleryBlock } from './ImageGalleryBlock';
|
||||||
|
export type { ImageGalleryBlockProps } from './ImageGalleryBlock';
|
||||||
|
export { VideoBlock } from './VideoBlock';
|
||||||
|
export type { VideoBlockProps } from './VideoBlock';
|
||||||
|
export { TextBlock } from './TextBlock';
|
||||||
|
export type { TextBlockProps } from './TextBlock';
|
||||||
|
export { QuoteBlock } from './QuoteBlock';
|
||||||
|
export type { QuoteBlockProps } from './QuoteBlock';
|
||||||
|
|
||||||
|
|||||||
35
frontend/src/components/shared/layouts/BlogLayout.tsx
Normal file
35
frontend/src/components/shared/layouts/BlogLayout.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './layouts.css';
|
||||||
|
|
||||||
|
export interface BlogLayoutProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
mainContent: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
sidebarPosition?: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogLayout({
|
||||||
|
header,
|
||||||
|
sidebar,
|
||||||
|
mainContent,
|
||||||
|
footer,
|
||||||
|
sidebarPosition = 'right'
|
||||||
|
}: BlogLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className={`shared-layout shared-layout--blog shared-layout--sidebar-${sidebarPosition}`}>
|
||||||
|
{header && <header className="shared-layout__header">{header}</header>}
|
||||||
|
<div className="shared-layout__content">
|
||||||
|
{sidebar && sidebarPosition === 'left' && (
|
||||||
|
<aside className="shared-layout__sidebar">{sidebar}</aside>
|
||||||
|
)}
|
||||||
|
<main className="shared-layout__main">{mainContent}</main>
|
||||||
|
{sidebar && sidebarPosition === 'right' && (
|
||||||
|
<aside className="shared-layout__sidebar">{sidebar}</aside>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{footer && <footer className="shared-layout__footer">{footer}</footer>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
31
frontend/src/components/shared/layouts/CorporateLayout.tsx
Normal file
31
frontend/src/components/shared/layouts/CorporateLayout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './layouts.css';
|
||||||
|
|
||||||
|
export interface CorporateLayoutProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
navigation?: ReactNode;
|
||||||
|
mainContent: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CorporateLayout({
|
||||||
|
header,
|
||||||
|
navigation,
|
||||||
|
mainContent,
|
||||||
|
footer,
|
||||||
|
sidebar
|
||||||
|
}: CorporateLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="shared-layout shared-layout--corporate">
|
||||||
|
{header && <header className="shared-layout__header">{header}</header>}
|
||||||
|
{navigation && <nav className="shared-layout__navigation">{navigation}</nav>}
|
||||||
|
<div className="shared-layout__content">
|
||||||
|
<main className="shared-layout__main">{mainContent}</main>
|
||||||
|
{sidebar && <aside className="shared-layout__sidebar">{sidebar}</aside>}
|
||||||
|
</div>
|
||||||
|
{footer && <footer className="shared-layout__footer">{footer}</footer>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
31
frontend/src/components/shared/layouts/EcommerceLayout.tsx
Normal file
31
frontend/src/components/shared/layouts/EcommerceLayout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './layouts.css';
|
||||||
|
|
||||||
|
export interface EcommerceLayoutProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
navigation?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
mainContent: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EcommerceLayout({
|
||||||
|
header,
|
||||||
|
navigation,
|
||||||
|
sidebar,
|
||||||
|
mainContent,
|
||||||
|
footer
|
||||||
|
}: EcommerceLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="shared-layout shared-layout--ecommerce">
|
||||||
|
{header && <header className="shared-layout__header">{header}</header>}
|
||||||
|
{navigation && <nav className="shared-layout__navigation">{navigation}</nav>}
|
||||||
|
<div className="shared-layout__content">
|
||||||
|
{sidebar && <aside className="shared-layout__sidebar">{sidebar}</aside>}
|
||||||
|
<main className="shared-layout__main">{mainContent}</main>
|
||||||
|
</div>
|
||||||
|
{footer && <footer className="shared-layout__footer">{footer}</footer>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
31
frontend/src/components/shared/layouts/MagazineLayout.tsx
Normal file
31
frontend/src/components/shared/layouts/MagazineLayout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './layouts.css';
|
||||||
|
|
||||||
|
export interface MagazineLayoutProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
featured?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
mainContent: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MagazineLayout({
|
||||||
|
header,
|
||||||
|
featured,
|
||||||
|
sidebar,
|
||||||
|
mainContent,
|
||||||
|
footer
|
||||||
|
}: MagazineLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="shared-layout shared-layout--magazine">
|
||||||
|
{header && <header className="shared-layout__header">{header}</header>}
|
||||||
|
{featured && <section className="shared-layout__featured">{featured}</section>}
|
||||||
|
<div className="shared-layout__content">
|
||||||
|
<main className="shared-layout__main">{mainContent}</main>
|
||||||
|
{sidebar && <aside className="shared-layout__sidebar">{sidebar}</aside>}
|
||||||
|
</div>
|
||||||
|
{footer && <footer className="shared-layout__footer">{footer}</footer>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
25
frontend/src/components/shared/layouts/PortfolioLayout.tsx
Normal file
25
frontend/src/components/shared/layouts/PortfolioLayout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './layouts.css';
|
||||||
|
|
||||||
|
export interface PortfolioLayoutProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
mainContent: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortfolioLayout({
|
||||||
|
header,
|
||||||
|
mainContent,
|
||||||
|
footer,
|
||||||
|
fullWidth = false
|
||||||
|
}: PortfolioLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className={`shared-layout shared-layout--portfolio ${fullWidth ? 'shared-layout--fullwidth' : ''}`}>
|
||||||
|
{header && <header className="shared-layout__header">{header}</header>}
|
||||||
|
<main className="shared-layout__main">{mainContent}</main>
|
||||||
|
{footer && <footer className="shared-layout__footer">{footer}</footer>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,5 +2,14 @@ export { DefaultLayout } from './DefaultLayout';
|
|||||||
export type { DefaultLayoutProps } from './DefaultLayout';
|
export type { DefaultLayoutProps } from './DefaultLayout';
|
||||||
export { MinimalLayout } from './MinimalLayout';
|
export { MinimalLayout } from './MinimalLayout';
|
||||||
export type { MinimalLayoutProps } from './MinimalLayout';
|
export type { MinimalLayoutProps } from './MinimalLayout';
|
||||||
|
export { MagazineLayout } from './MagazineLayout';
|
||||||
|
export type { MagazineLayoutProps } from './MagazineLayout';
|
||||||
|
export { EcommerceLayout } from './EcommerceLayout';
|
||||||
|
export type { EcommerceLayoutProps } from './EcommerceLayout';
|
||||||
|
export { PortfolioLayout } from './PortfolioLayout';
|
||||||
|
export type { PortfolioLayoutProps } from './PortfolioLayout';
|
||||||
|
export { BlogLayout } from './BlogLayout';
|
||||||
|
export type { BlogLayoutProps } from './BlogLayout';
|
||||||
|
export { CorporateLayout } from './CorporateLayout';
|
||||||
|
export type { CorporateLayoutProps } from './CorporateLayout';
|
||||||
|
|
||||||
|
|||||||
29
frontend/src/components/shared/templates/BlogTemplate.tsx
Normal file
29
frontend/src/components/shared/templates/BlogTemplate.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { BlogLayout } from '../layouts';
|
||||||
|
|
||||||
|
export interface BlogTemplateProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
posts: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
sidebarPosition?: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogTemplate({
|
||||||
|
header,
|
||||||
|
sidebar,
|
||||||
|
posts,
|
||||||
|
footer,
|
||||||
|
sidebarPosition = 'right'
|
||||||
|
}: BlogTemplateProps) {
|
||||||
|
return (
|
||||||
|
<BlogLayout
|
||||||
|
header={header}
|
||||||
|
sidebar={sidebar}
|
||||||
|
mainContent={posts}
|
||||||
|
footer={footer}
|
||||||
|
sidebarPosition={sidebarPosition}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { CorporateLayout } from '../layouts';
|
||||||
|
|
||||||
|
export interface BusinessTemplateProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
navigation?: ReactNode;
|
||||||
|
hero?: ReactNode;
|
||||||
|
features?: ReactNode;
|
||||||
|
services?: ReactNode;
|
||||||
|
testimonials?: ReactNode;
|
||||||
|
cta?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BusinessTemplate({
|
||||||
|
header,
|
||||||
|
navigation,
|
||||||
|
hero,
|
||||||
|
features,
|
||||||
|
services,
|
||||||
|
testimonials,
|
||||||
|
cta,
|
||||||
|
footer
|
||||||
|
}: BusinessTemplateProps) {
|
||||||
|
return (
|
||||||
|
<CorporateLayout
|
||||||
|
header={header}
|
||||||
|
navigation={navigation}
|
||||||
|
mainContent={
|
||||||
|
<>
|
||||||
|
{hero}
|
||||||
|
{features}
|
||||||
|
{services}
|
||||||
|
{testimonials}
|
||||||
|
{cta}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={footer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { EcommerceLayout } from '../layouts';
|
||||||
|
|
||||||
|
export interface EcommerceTemplateProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
navigation?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
products?: ReactNode;
|
||||||
|
featured?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EcommerceTemplate({
|
||||||
|
header,
|
||||||
|
navigation,
|
||||||
|
sidebar,
|
||||||
|
products,
|
||||||
|
featured,
|
||||||
|
footer
|
||||||
|
}: EcommerceTemplateProps) {
|
||||||
|
return (
|
||||||
|
<EcommerceLayout
|
||||||
|
header={header}
|
||||||
|
navigation={navigation}
|
||||||
|
sidebar={sidebar}
|
||||||
|
mainContent={
|
||||||
|
<>
|
||||||
|
{featured}
|
||||||
|
{products}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={footer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { PortfolioLayout } from '../layouts';
|
||||||
|
|
||||||
|
export interface PortfolioTemplateProps {
|
||||||
|
header?: ReactNode;
|
||||||
|
gallery?: ReactNode;
|
||||||
|
about?: ReactNode;
|
||||||
|
contact?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortfolioTemplate({
|
||||||
|
header,
|
||||||
|
gallery,
|
||||||
|
about,
|
||||||
|
contact,
|
||||||
|
footer,
|
||||||
|
fullWidth = false
|
||||||
|
}: PortfolioTemplateProps) {
|
||||||
|
return (
|
||||||
|
<PortfolioLayout
|
||||||
|
header={header}
|
||||||
|
mainContent={
|
||||||
|
<>
|
||||||
|
{gallery}
|
||||||
|
{about}
|
||||||
|
{contact}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={footer}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,5 +2,12 @@ export { MarketingTemplate } from './MarketingTemplate';
|
|||||||
export type { MarketingTemplateProps } from './MarketingTemplate';
|
export type { MarketingTemplateProps } from './MarketingTemplate';
|
||||||
export { LandingTemplate } from './LandingTemplate';
|
export { LandingTemplate } from './LandingTemplate';
|
||||||
export type { LandingTemplateProps } from './LandingTemplate';
|
export type { LandingTemplateProps } from './LandingTemplate';
|
||||||
|
export { BlogTemplate } from './BlogTemplate';
|
||||||
|
export type { BlogTemplateProps } from './BlogTemplate';
|
||||||
|
export { BusinessTemplate } from './BusinessTemplate';
|
||||||
|
export type { BusinessTemplateProps } from './BusinessTemplate';
|
||||||
|
export { PortfolioTemplate } from './PortfolioTemplate';
|
||||||
|
export type { PortfolioTemplateProps } from './PortfolioTemplate';
|
||||||
|
export { EcommerceTemplate } from './EcommerceTemplate';
|
||||||
|
export type { EcommerceTemplateProps } from './EcommerceTemplate';
|
||||||
|
|
||||||
|
|||||||
304
frontend/src/pages/Sites/Dashboard.tsx
Normal file
304
frontend/src/pages/Sites/Dashboard.tsx
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* Site Dashboard (Advanced)
|
||||||
|
* Phase 7: UI Components & Prompt Management
|
||||||
|
* Site overview with statistics and analytics
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
EyeIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
PlugIcon,
|
||||||
|
TrendingUpIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
GlobeIcon
|
||||||
|
} from 'lucide-react';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
import { fetchAPI } from '../../services/api';
|
||||||
|
|
||||||
|
interface Site {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
site_type: string;
|
||||||
|
hosting_type: string;
|
||||||
|
status: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SiteStats {
|
||||||
|
total_pages: number;
|
||||||
|
published_pages: number;
|
||||||
|
draft_pages: number;
|
||||||
|
total_content: number;
|
||||||
|
published_content: number;
|
||||||
|
integrations_count: number;
|
||||||
|
deployments_count: number;
|
||||||
|
last_deployment?: string;
|
||||||
|
views_count?: number;
|
||||||
|
visitors_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SiteDashboard() {
|
||||||
|
const { siteId } = useParams<{ siteId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const toast = useToast();
|
||||||
|
const [site, setSite] = useState<Site | null>(null);
|
||||||
|
const [stats, setStats] = useState<SiteStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (siteId) {
|
||||||
|
loadSiteData();
|
||||||
|
}
|
||||||
|
}, [siteId]);
|
||||||
|
|
||||||
|
const loadSiteData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [siteData, statsData] = await Promise.all([
|
||||||
|
fetchAPI(`/v1/auth/sites/${siteId}/`),
|
||||||
|
fetchSiteStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (siteData) {
|
||||||
|
setSite(siteData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statsData) {
|
||||||
|
setStats(statsData);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(`Failed to load site data: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSiteStats = async (): Promise<SiteStats | null> => {
|
||||||
|
try {
|
||||||
|
// TODO: Create backend endpoint for site stats
|
||||||
|
// For now, return mock data structure
|
||||||
|
return {
|
||||||
|
total_pages: 0,
|
||||||
|
published_pages: 0,
|
||||||
|
draft_pages: 0,
|
||||||
|
total_content: 0,
|
||||||
|
published_content: 0,
|
||||||
|
integrations_count: 0,
|
||||||
|
deployments_count: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching site stats:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Site Dashboard" />
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-gray-500">Loading site dashboard...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Site Not Found" />
|
||||||
|
<Card className="p-12 text-center">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">Site not found</p>
|
||||||
|
<Button onClick={() => navigate('/sites')} variant="outline">
|
||||||
|
Back to Sites
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statCards = [
|
||||||
|
{
|
||||||
|
label: 'Total Pages',
|
||||||
|
value: stats?.total_pages || 0,
|
||||||
|
icon: <FileTextIcon className="w-5 h-5" />,
|
||||||
|
color: 'blue',
|
||||||
|
link: `/sites/${siteId}/pages`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Published Pages',
|
||||||
|
value: stats?.published_pages || 0,
|
||||||
|
icon: <GlobeIcon className="w-5 h-5" />,
|
||||||
|
color: 'green',
|
||||||
|
link: `/sites/${siteId}/pages?status=published`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Draft Pages',
|
||||||
|
value: stats?.draft_pages || 0,
|
||||||
|
icon: <FileTextIcon className="w-5 h-5" />,
|
||||||
|
color: 'amber',
|
||||||
|
link: `/sites/${siteId}/pages?status=draft`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Integrations',
|
||||||
|
value: stats?.integrations_count || 0,
|
||||||
|
icon: <PlugIcon className="w-5 h-5" />,
|
||||||
|
color: 'purple',
|
||||||
|
link: `/sites/${siteId}/integrations`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deployments',
|
||||||
|
value: stats?.deployments_count || 0,
|
||||||
|
icon: <TrendingUpIcon className="w-5 h-5" />,
|
||||||
|
color: 'teal',
|
||||||
|
link: `/sites/${siteId}/deployments`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Total Content',
|
||||||
|
value: stats?.total_content || 0,
|
||||||
|
icon: <FileTextIcon className="w-5 h-5" />,
|
||||||
|
color: 'indigo',
|
||||||
|
link: `/sites/${siteId}/content`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title={`${site.name} - Dashboard`} />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6 flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{site.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{site.slug} • {site.site_type} • {site.hosting_type}
|
||||||
|
</p>
|
||||||
|
{site.domain && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
||||||
|
<GlobeIcon className="w-4 h-4 inline mr-1" />
|
||||||
|
{site.domain}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/preview`)}
|
||||||
|
>
|
||||||
|
<EyeIcon className="w-4 h-4 mr-2" />
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/settings`)}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
|
{statCards.map((stat, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
className="p-4 hover:shadow-lg transition-shadow cursor-pointer"
|
||||||
|
onClick={() => stat.link && navigate(stat.link)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
|
{stat.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{stat.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`text-${stat.color}-500`}>
|
||||||
|
{stat.icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card className="p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
Quick Actions
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||||
|
className="justify-start"
|
||||||
|
>
|
||||||
|
<FileTextIcon className="w-4 h-4 mr-2" />
|
||||||
|
Manage Pages
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||||
|
className="justify-start"
|
||||||
|
>
|
||||||
|
<FileTextIcon className="w-4 h-4 mr-2" />
|
||||||
|
Manage Content
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/integrations`)}
|
||||||
|
className="justify-start"
|
||||||
|
>
|
||||||
|
<PlugIcon className="w-4 h-4 mr-2" />
|
||||||
|
Manage Integrations
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||||
|
className="justify-start"
|
||||||
|
>
|
||||||
|
<TrendingUpIcon className="w-4 h-4 mr-2" />
|
||||||
|
Deploy Site
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
Recent Activity
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stats?.last_deployment ? (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<CalendarIcon className="w-5 h-5 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
Last Deployment
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{new Date(stats.last_deployment).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
No recent activity
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
405
frontend/src/pages/Sites/List.tsx
Normal file
405
frontend/src/pages/Sites/List.tsx
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
/**
|
||||||
|
* Site List View
|
||||||
|
* Phase 7: UI Components & Prompt Management
|
||||||
|
* Advanced site list with filters and search
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon, FilterIcon, SearchIcon } from 'lucide-react';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import SelectDropdown from '../../components/form/SelectDropdown';
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
import { fetchAPI } from '../../services/api';
|
||||||
|
|
||||||
|
interface Site {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
site_type: string;
|
||||||
|
hosting_type: string;
|
||||||
|
status: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
page_count?: number;
|
||||||
|
integration_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SiteList() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const toast = useToast();
|
||||||
|
const [sites, setSites] = useState<Site[]>([]);
|
||||||
|
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [siteTypeFilter, setSiteTypeFilter] = useState('');
|
||||||
|
const [hostingTypeFilter, setHostingTypeFilter] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
|
const [integrationFilter, setIntegrationFilter] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSites();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyFilters();
|
||||||
|
}, [sites, searchTerm, siteTypeFilter, hostingTypeFilter, statusFilter, integrationFilter]);
|
||||||
|
|
||||||
|
const loadSites = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await fetchAPI('/v1/auth/sites/');
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
setSites(data);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(`Failed to load sites: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
let filtered = [...sites];
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchTerm) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(site) =>
|
||||||
|
site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
site.slug.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site type filter
|
||||||
|
if (siteTypeFilter) {
|
||||||
|
filtered = filtered.filter((site) => site.site_type === siteTypeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hosting type filter
|
||||||
|
if (hostingTypeFilter) {
|
||||||
|
filtered = filtered.filter((site) => site.hosting_type === hostingTypeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter) {
|
||||||
|
if (statusFilter === 'active') {
|
||||||
|
filtered = filtered.filter((site) => site.is_active);
|
||||||
|
} else if (statusFilter === 'inactive') {
|
||||||
|
filtered = filtered.filter((site) => !site.is_active);
|
||||||
|
} else {
|
||||||
|
filtered = filtered.filter((site) => site.status === statusFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration filter (has integrations or not)
|
||||||
|
if (integrationFilter === 'has_integrations') {
|
||||||
|
filtered = filtered.filter((site) => (site.integration_count || 0) > 0);
|
||||||
|
} else if (integrationFilter === 'no_integrations') {
|
||||||
|
filtered = filtered.filter((site) => (site.integration_count || 0) === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredSites(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSite = () => {
|
||||||
|
navigate('/site-builder');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (siteId: number) => {
|
||||||
|
navigate(`/sites/${siteId}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettings = (siteId: number) => {
|
||||||
|
navigate(`/sites/${siteId}/settings`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (siteId: number) => {
|
||||||
|
navigate(`/sites/${siteId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (siteId: number) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this site?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
toast.success('Site deleted successfully');
|
||||||
|
loadSites();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(`Failed to delete site: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setSearchTerm('');
|
||||||
|
setSiteTypeFilter('');
|
||||||
|
setHostingTypeFilter('');
|
||||||
|
setStatusFilter('');
|
||||||
|
setIntegrationFilter('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const SITE_TYPES = [
|
||||||
|
{ value: '', label: 'All Types' },
|
||||||
|
{ value: 'marketing', label: 'Marketing' },
|
||||||
|
{ value: 'ecommerce', label: 'Ecommerce' },
|
||||||
|
{ value: 'blog', label: 'Blog' },
|
||||||
|
{ value: 'portfolio', label: 'Portfolio' },
|
||||||
|
{ value: 'corporate', label: 'Corporate' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const HOSTING_TYPES = [
|
||||||
|
{ value: '', label: 'All Hosting' },
|
||||||
|
{ value: 'igny8_sites', label: 'IGNY8 Sites' },
|
||||||
|
{ value: 'wordpress', label: 'WordPress' },
|
||||||
|
{ value: 'shopify', label: 'Shopify' },
|
||||||
|
{ value: 'multi', label: 'Multi-Destination' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = [
|
||||||
|
{ value: '', label: 'All Status' },
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'inactive', label: 'Inactive' },
|
||||||
|
{ value: 'active', label: 'Active Status' },
|
||||||
|
{ value: 'inactive', label: 'Inactive Status' },
|
||||||
|
{ value: 'suspended', label: 'Suspended' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const INTEGRATION_OPTIONS = [
|
||||||
|
{ value: '', label: 'All Sites' },
|
||||||
|
{ value: 'has_integrations', label: 'Has Integrations' },
|
||||||
|
{ value: 'no_integrations', label: 'No Integrations' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Site List" />
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-gray-500">Loading sites...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Site List - IGNY8" />
|
||||||
|
|
||||||
|
<div className="mb-6 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Site List
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
View and manage all your sites with advanced filtering
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCreateSite} variant="primary">
|
||||||
|
<PlusIcon className="w-4 h-4 mr-2" />
|
||||||
|
Create New Site
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="p-4 mb-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<FilterIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Filters
|
||||||
|
</h2>
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search sites..."
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Site Type
|
||||||
|
</label>
|
||||||
|
<SelectDropdown
|
||||||
|
options={SITE_TYPES}
|
||||||
|
value={siteTypeFilter}
|
||||||
|
onChange={(e) => setSiteTypeFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hosting Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Hosting
|
||||||
|
</label>
|
||||||
|
<SelectDropdown
|
||||||
|
options={HOSTING_TYPES}
|
||||||
|
value={hostingTypeFilter}
|
||||||
|
onChange={(e) => setHostingTypeFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<SelectDropdown
|
||||||
|
options={STATUS_OPTIONS}
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Integrations
|
||||||
|
</label>
|
||||||
|
<SelectDropdown
|
||||||
|
options={INTEGRATION_OPTIONS}
|
||||||
|
value={integrationFilter}
|
||||||
|
onChange={(e) => setIntegrationFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Results Count */}
|
||||||
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Showing {filteredSites.length} of {sites.length} sites
|
||||||
|
{hasActiveFilters && ' (filtered)'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sites List */}
|
||||||
|
{filteredSites.length === 0 ? (
|
||||||
|
<Card className="p-12 text-center">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'}
|
||||||
|
</p>
|
||||||
|
{hasActiveFilters ? (
|
||||||
|
<Button onClick={clearFilters} variant="outline">
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleCreateSite} variant="primary">
|
||||||
|
Create Your First Site
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{filteredSites.map((site) => (
|
||||||
|
<Card key={site.id} className="p-4 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{site.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{site.slug}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 text-xs rounded ${
|
||||||
|
site.is_active
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||||
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{site.is_active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 text-xs">
|
||||||
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:bg-blue-200 rounded capitalize">
|
||||||
|
{site.site_type}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-1 bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 rounded capitalize">
|
||||||
|
{site.hosting_type}
|
||||||
|
</span>
|
||||||
|
{site.integration_count && site.integration_count > 0 && (
|
||||||
|
<span className="px-2 py-1 bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200 rounded">
|
||||||
|
{site.integration_count} integration{site.integration_count > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{site.page_count || 0} pages
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleView(site.id)}
|
||||||
|
title="View"
|
||||||
|
>
|
||||||
|
<EyeIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(site.id)}
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<EditIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSettings(site.id)}
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<SettingsIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(site.id)}
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,13 @@ const PROMPT_TYPES = [
|
|||||||
icon: '🚫',
|
icon: '🚫',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'site_structure_generation',
|
||||||
|
label: 'Site Structure Generation',
|
||||||
|
description: 'Generate site structure from business brief. Use [IGNY8_BUSINESS_BRIEF], [IGNY8_OBJECTIVES], [IGNY8_STYLE], and [IGNY8_SITE_INFO] to inject data.',
|
||||||
|
icon: '🏗️',
|
||||||
|
color: 'teal',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Prompts() {
|
export default function Prompts() {
|
||||||
@@ -426,6 +433,87 @@ export default function Prompts() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Site Builder Prompts Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-1">
|
||||||
|
Site Builder
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Configure AI prompt templates for site structure generation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site Structure Generation Prompt */}
|
||||||
|
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
{PROMPT_TYPES.filter(t => t.key === 'site_structure_generation').map((type) => {
|
||||||
|
const prompt = prompts[type.key] || {
|
||||||
|
prompt_type: type.key,
|
||||||
|
prompt_type_display: type.label,
|
||||||
|
prompt_value: '',
|
||||||
|
default_prompt: '',
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type.key}>
|
||||||
|
<div className="p-5 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{type.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||||||
|
{type.label}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{type.description}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-xs text-gray-500 dark:text-gray-500">
|
||||||
|
<p className="font-semibold mb-1">Available Variables:</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_BUSINESS_BRIEF]</code> - Business description and context</li>
|
||||||
|
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_OBJECTIVES]</code> - Site objectives and goals</li>
|
||||||
|
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_STYLE]</code> - Design style preferences</li>
|
||||||
|
<li><code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">[IGNY8_SITE_INFO]</code> - Site type and requirements</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<TextArea
|
||||||
|
value={prompt.prompt_value || ''}
|
||||||
|
onChange={(value) => handlePromptChange(type.key, value)}
|
||||||
|
rows={15}
|
||||||
|
placeholder="Enter prompt template for site structure generation..."
|
||||||
|
className="font-mono-custom text-sm"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-3 mt-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleSave(type.key)}
|
||||||
|
disabled={saving[type.key]}
|
||||||
|
className="flex-1"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleReset(type.key)}
|
||||||
|
disabled={saving[type.key]}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Reset to Default
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user