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_template', 'Image Prompt Template'),
|
||||
('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)
|
||||
|
||||
@@ -15,10 +15,41 @@ These components are designed to be framework-agnostic where possible, but curre
|
||||
|
||||
```
|
||||
shared/
|
||||
├── blocks/ # Content blocks (HeroBlock, FeatureGridBlock, StatsPanel)
|
||||
├── layouts/ # Page layouts (DefaultLayout, MinimalLayout)
|
||||
├── templates/ # Full page templates (MarketingTemplate, LandingTemplate)
|
||||
└── index.ts # Barrel exports
|
||||
├── blocks/ # Content blocks (12 total)
|
||||
│ ├── HeroBlock.tsx
|
||||
│ ├── FeatureGridBlock.tsx
|
||||
│ ├── 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
|
||||
@@ -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
|
||||
|
||||
#### `DefaultLayout`
|
||||
@@ -125,15 +258,64 @@ Minimal layout for focused content pages.
|
||||
|
||||
**Props:**
|
||||
- `children: ReactNode` - Page content
|
||||
- `maxWidth?: string` - Maximum content width
|
||||
- `background?: 'light' | 'dark'` - Background color
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
<MinimalLayout maxWidth="800px">
|
||||
<MinimalLayout background="light">
|
||||
<ArticleContent />
|
||||
</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
|
||||
|
||||
#### `MarketingTemplate`
|
||||
@@ -169,6 +351,51 @@ Landing page template optimized for conversions.
|
||||
**Props:**
|
||||
- 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
|
||||
|
||||
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 { StatsPanel } 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 { MinimalLayout } 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 { LandingTemplate } 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: '🚫',
|
||||
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() {
|
||||
@@ -426,6 +433,87 @@ export default function Prompts() {
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user