phase 6 ,7,9
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user