componenets standardization 1
This commit is contained in:
183
DESIGN-GUIDE.md
Normal file
183
DESIGN-GUIDE.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# IGNY8 Design System Guide
|
||||||
|
|
||||||
|
> **Single Source of Truth for UI Components**
|
||||||
|
>
|
||||||
|
> This guide ensures consistent, maintainable frontend code across the entire application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
| Resource | Path | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| **Component System** | [docs/30-FRONTEND/COMPONENT-SYSTEM.md](docs/30-FRONTEND/COMPONENT-SYSTEM.md) | Full component reference with props, examples, and usage |
|
||||||
|
| **ESLint Plugin** | [frontend/eslint/](frontend/eslint/) | Custom rules enforcing design system |
|
||||||
|
| **Live Demo** | `/ui-elements` route | Interactive component showcase |
|
||||||
|
| **Design Tokens** | [frontend/src/styles/design-system.css](frontend/src/styles/design-system.css) | CSS variables and tokens |
|
||||||
|
| **Icons** | [frontend/src/icons/](frontend/src/icons/) | All SVG icons |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### 1. Use Components, Never Raw HTML
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ NEVER
|
||||||
|
<button className="...">Click</button>
|
||||||
|
<input type="text" className="..." />
|
||||||
|
<select className="...">...</select>
|
||||||
|
<textarea className="..."></textarea>
|
||||||
|
|
||||||
|
// ✅ ALWAYS
|
||||||
|
<Button variant="primary">Click</Button>
|
||||||
|
<InputField type="text" label="Name" />
|
||||||
|
<Select options={options} />
|
||||||
|
<TextArea rows={4} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Import Icons from Central Location
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ NEVER
|
||||||
|
import { XIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Trash } from 'lucide-react';
|
||||||
|
|
||||||
|
// ✅ ALWAYS
|
||||||
|
import { CloseIcon, TrashBinIcon } from '../../icons';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Consistent Sizing
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Icons in buttons/badges
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
|
||||||
|
// Standalone icons
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
|
||||||
|
// Large/header icons
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Quick Reference
|
||||||
|
|
||||||
|
| Need | Component | Import |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Action button | `Button` | `components/ui/button/Button` |
|
||||||
|
| Icon-only button | `IconButton` | `components/ui/button/IconButton` |
|
||||||
|
| Text input | `InputField` | `components/form/input/InputField` |
|
||||||
|
| Checkbox | `Checkbox` | `components/form/input/Checkbox` |
|
||||||
|
| Radio | `Radio` | `components/form/input/Radio` |
|
||||||
|
| Dropdown | `Select` | `components/form/Select` |
|
||||||
|
| Multi-line text | `TextArea` | `components/form/input/TextArea` |
|
||||||
|
| Toggle | `Switch` | `components/form/switch/Switch` |
|
||||||
|
| Status label | `Badge` | `components/ui/badge/Badge` |
|
||||||
|
| Container | `Card` | `components/ui/card/Card` |
|
||||||
|
| Popup | `Modal` | `components/ui/modal` |
|
||||||
|
| Loading | `Spinner` | `components/ui/spinner/Spinner` |
|
||||||
|
| Notification | `useToast` | `components/ui/toast/ToastContainer` |
|
||||||
|
|
||||||
|
**→ See [COMPONENT-SYSTEM.md](docs/30-FRONTEND/COMPONENT-SYSTEM.md) for full props and examples**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ESLint Enforcement
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
| Rule | Level | Action |
|
||||||
|
|------|-------|--------|
|
||||||
|
| `no-raw-button` | warn → error | Use `Button` or `IconButton` |
|
||||||
|
| `no-raw-input` | warn → error | Use `InputField`, `Checkbox`, `Radio` |
|
||||||
|
| `no-raw-select` | warn → error | Use `Select` or `SelectDropdown` |
|
||||||
|
| `no-raw-textarea` | warn → error | Use `TextArea` |
|
||||||
|
| `no-restricted-imports` | error | Block external icon libraries |
|
||||||
|
|
||||||
|
### Check Violations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Location
|
||||||
|
|
||||||
|
The custom ESLint plugin is at: `frontend/eslint/eslint-plugin-igny8-design-system.cjs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For AI Agents
|
||||||
|
|
||||||
|
When working on this codebase:
|
||||||
|
|
||||||
|
1. **Read first**: [docs/30-FRONTEND/COMPONENT-SYSTEM.md](docs/30-FRONTEND/COMPONENT-SYSTEM.md)
|
||||||
|
2. **Never use**: `<button>`, `<input>`, `<select>`, `<textarea>` tags
|
||||||
|
3. **Import icons from**: `src/icons` only
|
||||||
|
4. **Verify after changes**: `npm run lint`
|
||||||
|
5. **Reference pages**: Planner and Writer modules use correct patterns
|
||||||
|
|
||||||
|
### Correct Import Paths
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// From a page in src/pages/
|
||||||
|
import Button from '../components/ui/button/Button';
|
||||||
|
import IconButton from '../components/ui/button/IconButton';
|
||||||
|
import InputField from '../components/form/input/InputField';
|
||||||
|
import { PlusIcon, CloseIcon } from '../icons';
|
||||||
|
|
||||||
|
// From a component in src/components/
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import { PlusIcon } from '../../icons';
|
||||||
|
|
||||||
|
// From a nested component
|
||||||
|
// Adjust ../ based on depth
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── eslint/
|
||||||
|
│ └── eslint-plugin-igny8-design-system.cjs # Custom rules
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── ui/ # Display components
|
||||||
|
│ │ │ ├── button/ # Button, IconButton
|
||||||
|
│ │ │ ├── badge/ # Badge
|
||||||
|
│ │ │ ├── card/ # Card
|
||||||
|
│ │ │ ├── modal/ # Modal
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── form/ # Form components
|
||||||
|
│ │ ├── input/ # InputField, Checkbox, Radio, TextArea
|
||||||
|
│ │ ├── switch/ # Switch
|
||||||
|
│ │ ├── Select.tsx
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── icons/ # All SVG icons
|
||||||
|
│ │ └── index.ts # Export all icons
|
||||||
|
│ └── styles/
|
||||||
|
│ └── design-system.css # Design tokens
|
||||||
|
docs/
|
||||||
|
└── 30-FRONTEND/
|
||||||
|
└── COMPONENT-SYSTEM.md # Full component documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
When fixing violations:
|
||||||
|
|
||||||
|
- [ ] Replace `<button>` with `Button` or `IconButton`
|
||||||
|
- [ ] Replace `<input type="text/email/password/number">` with `InputField`
|
||||||
|
- [ ] Replace `<input type="checkbox">` with `Checkbox`
|
||||||
|
- [ ] Replace `<input type="radio">` with `Radio`
|
||||||
|
- [ ] Replace `<select>` with `Select` or `SelectDropdown`
|
||||||
|
- [ ] Replace `<textarea>` with `TextArea`
|
||||||
|
- [ ] Replace external icon imports with `src/icons`
|
||||||
|
- [ ] Run `npm run lint` to verify
|
||||||
|
- [ ] Run `npm run build` to confirm no errors
|
||||||
617
docs/30-FRONTEND/COMPONENT-SYSTEM.md
Normal file
617
docs/30-FRONTEND/COMPONENT-SYSTEM.md
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
# IGNY8 Frontend Component System
|
||||||
|
|
||||||
|
> **🔒 ENFORCED BY ESLINT** - Violations will trigger warnings/errors during build.
|
||||||
|
> This document is the single source of truth for all UI components.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
| Element | Component | Import Path |
|
||||||
|
|---------|-----------|-------------|
|
||||||
|
| Button | `Button` | `components/ui/button/Button` |
|
||||||
|
| Icon Button | `IconButton` | `components/ui/button/IconButton` |
|
||||||
|
| Text Input | `InputField` | `components/form/input/InputField` |
|
||||||
|
| Checkbox | `Checkbox` | `components/form/input/Checkbox` |
|
||||||
|
| Radio | `Radio` | `components/form/input/Radio` |
|
||||||
|
| File Upload | `FileInput` | `components/form/input/FileInput` |
|
||||||
|
| Textarea | `TextArea` | `components/form/input/TextArea` |
|
||||||
|
| Dropdown | `Select` | `components/form/Select` |
|
||||||
|
| Searchable Dropdown | `SelectDropdown` | `components/form/SelectDropdown` |
|
||||||
|
| Multi-Select | `MultiSelect` | `components/form/MultiSelect` |
|
||||||
|
| Toggle Switch | `Switch` | `components/form/switch/Switch` |
|
||||||
|
| Badge | `Badge` | `components/ui/badge/Badge` |
|
||||||
|
| Card | `Card` | `components/ui/card/Card` |
|
||||||
|
| Modal | `Modal` | `components/ui/modal` |
|
||||||
|
| Alert | `Alert` | `components/ui/alert/Alert` |
|
||||||
|
| Spinner | `Spinner` | `components/ui/spinner/Spinner` |
|
||||||
|
| Tabs | `Tabs` | `components/ui/tabs/Tabs` |
|
||||||
|
| Tooltip | `Tooltip` | `components/ui/tooltip/Tooltip` |
|
||||||
|
| Toast | `useToast` | `components/ui/toast/ToastContainer` |
|
||||||
|
| Icons | `*Icon` | `icons` (e.g., `../../icons`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. BUTTONS
|
||||||
|
|
||||||
|
### Button (Standard)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary" // primary | secondary | outline | ghost | gradient
|
||||||
|
tone="brand" // brand | success | warning | danger | neutral
|
||||||
|
size="md" // xs | sm | md | lg | xl | 2xl
|
||||||
|
shape="rounded" // rounded | pill
|
||||||
|
startIcon={<Icon />} // Icon before text
|
||||||
|
endIcon={<Icon />} // Icon after text
|
||||||
|
onClick={handler}
|
||||||
|
disabled={false}
|
||||||
|
fullWidth={false}
|
||||||
|
type="button" // button | submit | reset
|
||||||
|
className="" // Additional classes
|
||||||
|
>
|
||||||
|
Button Text
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
```tsx
|
||||||
|
// Primary action
|
||||||
|
<Button variant="primary" tone="brand">Save</Button>
|
||||||
|
|
||||||
|
// Success/confirm
|
||||||
|
<Button variant="primary" tone="success">Approve</Button>
|
||||||
|
|
||||||
|
// Danger/destructive
|
||||||
|
<Button variant="primary" tone="danger">Delete</Button>
|
||||||
|
|
||||||
|
// Secondary/cancel
|
||||||
|
<Button variant="outline" tone="neutral">Cancel</Button>
|
||||||
|
|
||||||
|
// With icon
|
||||||
|
<Button startIcon={<PlusIcon className="w-4 h-4" />}>Add Item</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### IconButton (Icon-only)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import IconButton from '../../components/ui/button/IconButton';
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />} // Required: Icon component
|
||||||
|
variant="ghost" // solid | outline | ghost
|
||||||
|
tone="neutral" // brand | success | warning | danger | neutral
|
||||||
|
size="sm" // xs | sm | md | lg
|
||||||
|
shape="rounded" // rounded | circle
|
||||||
|
onClick={handler}
|
||||||
|
disabled={false}
|
||||||
|
title="Close" // Required: Accessibility label
|
||||||
|
aria-label="Close" // Optional: Override aria-label
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
```tsx
|
||||||
|
// Close button
|
||||||
|
<IconButton icon={<CloseIcon />} variant="ghost" tone="neutral" title="Close" />
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
<IconButton icon={<TrashBinIcon />} variant="ghost" tone="danger" title="Delete" />
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
<IconButton icon={<PencilIcon />} variant="ghost" tone="brand" title="Edit" />
|
||||||
|
|
||||||
|
// Add button (circular)
|
||||||
|
<IconButton icon={<PlusIcon />} variant="solid" tone="brand" shape="circle" title="Add" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. FORM INPUTS
|
||||||
|
|
||||||
|
### InputField (Text/Number/Email/Password)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
|
||||||
|
<InputField
|
||||||
|
type="text" // text | number | email | password | date | time
|
||||||
|
label="Field Label"
|
||||||
|
placeholder="Enter value..."
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
id="field-id"
|
||||||
|
name="field-name"
|
||||||
|
disabled={false}
|
||||||
|
error={false} // Shows error styling
|
||||||
|
success={false} // Shows success styling
|
||||||
|
hint="Helper text" // Text below input
|
||||||
|
min="0" // For number inputs
|
||||||
|
max="100" // For number inputs
|
||||||
|
step={1} // For number inputs
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### TextArea (Multi-line)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import TextArea from '../../components/form/input/TextArea';
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
placeholder="Enter description..."
|
||||||
|
rows={4}
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => setValue(value)}
|
||||||
|
disabled={false}
|
||||||
|
error={false}
|
||||||
|
hint="Helper text"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checkbox
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Accept terms"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(checked) => setChecked(checked)}
|
||||||
|
id="checkbox-id"
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Radio
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Radio from '../../components/form/input/Radio';
|
||||||
|
|
||||||
|
<Radio
|
||||||
|
id="radio-option-1"
|
||||||
|
name="radio-group"
|
||||||
|
value="option1"
|
||||||
|
label="Option 1"
|
||||||
|
checked={selected === 'option1'}
|
||||||
|
onChange={(value) => setSelected(value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select (Dropdown)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'opt1', label: 'Option 1' },
|
||||||
|
{ value: 'opt2', label: 'Option 2' },
|
||||||
|
]}
|
||||||
|
placeholder="Select..."
|
||||||
|
defaultValue=""
|
||||||
|
onChange={(value) => setValue(value)}
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### SelectDropdown (Searchable)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import SelectDropdown from '../../components/form/SelectDropdown';
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
label="Select Item"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => setValue(value)}
|
||||||
|
placeholder="Search..."
|
||||||
|
searchable={true}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch (Toggle)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Switch from '../../components/form/switch/Switch';
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label="Enable feature"
|
||||||
|
checked={enabled} // Controlled mode
|
||||||
|
onChange={(checked) => setEnabled(checked)}
|
||||||
|
disabled={false}
|
||||||
|
color="blue" // blue | gray
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### FileInput
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import FileInput from '../../components/form/input/FileInput';
|
||||||
|
|
||||||
|
<FileInput
|
||||||
|
onChange={(files) => handleFiles(files)}
|
||||||
|
accept=".csv,.json"
|
||||||
|
multiple={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. DISPLAY COMPONENTS
|
||||||
|
|
||||||
|
### Badge
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
tone="success" // brand | success | warning | danger | info | neutral | purple | indigo | pink | teal | cyan | blue
|
||||||
|
variant="soft" // solid | soft | outline | light
|
||||||
|
size="sm" // xs | sm | md
|
||||||
|
startIcon={<Icon />}
|
||||||
|
endIcon={<Icon />}
|
||||||
|
>
|
||||||
|
Label
|
||||||
|
</Badge>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Badge Patterns:**
|
||||||
|
```tsx
|
||||||
|
<Badge tone="success" variant="soft">Active</Badge>
|
||||||
|
<Badge tone="warning" variant="soft">Pending</Badge>
|
||||||
|
<Badge tone="danger" variant="soft">Failed</Badge>
|
||||||
|
<Badge tone="info" variant="soft">Draft</Badge>
|
||||||
|
<Badge tone="neutral" variant="soft">Archived</Badge>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Card
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Card, CardTitle, CardContent, CardDescription, CardAction, CardIcon } from '../../components/ui/card/Card';
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="surface" // surface | panel | frosted | borderless | gradient
|
||||||
|
padding="md" // none | sm | md | lg
|
||||||
|
shadow="sm" // none | sm | md
|
||||||
|
>
|
||||||
|
<CardIcon><Icon /></CardIcon>
|
||||||
|
<CardTitle>Title</CardTitle>
|
||||||
|
<CardDescription>Description text</CardDescription>
|
||||||
|
<CardContent>Main content</CardContent>
|
||||||
|
<CardAction onClick={handler}>Action</CardAction>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alert
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Alert from '../../components/ui/alert/Alert';
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
variant="success" // success | error | warning | info
|
||||||
|
title="Alert Title"
|
||||||
|
message="Alert message text"
|
||||||
|
showLink={false}
|
||||||
|
linkHref="#"
|
||||||
|
linkText="Learn more"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Modal } from '../../components/ui/modal';
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
showCloseButton={true}
|
||||||
|
isFullscreen={false}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
Modal content
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spinner
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Spinner } from '../../components/ui/spinner/Spinner';
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
size="md" // sm | md | lg
|
||||||
|
color="primary" // primary | success | error | warning | info
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tooltip
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Tooltip } from '../../components/ui/tooltip/Tooltip';
|
||||||
|
|
||||||
|
<Tooltip text="Tooltip text" placement="top">
|
||||||
|
<span>Hover target</span>
|
||||||
|
</Tooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toast (Notifications)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Show notifications
|
||||||
|
toast.success('Success', 'Operation completed');
|
||||||
|
toast.error('Error', 'Something went wrong');
|
||||||
|
toast.warning('Warning', 'Please review');
|
||||||
|
toast.info('Info', 'Here is some information');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ICONS
|
||||||
|
|
||||||
|
### Importing Icons
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Always import from central icons folder
|
||||||
|
import { PlusIcon, CloseIcon, CheckCircleIcon } from '../../icons';
|
||||||
|
|
||||||
|
// Use consistent sizing
|
||||||
|
<PlusIcon className="w-4 h-4" /> // Small (in buttons, badges)
|
||||||
|
<PlusIcon className="w-5 h-5" /> // Medium (standalone)
|
||||||
|
<PlusIcon className="w-6 h-6" /> // Large (headers, features)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Icons
|
||||||
|
|
||||||
|
**Core Icons:**
|
||||||
|
- `PlusIcon`, `CloseIcon`, `CheckCircleIcon`, `AlertIcon`, `InfoIcon`, `ErrorIcon`
|
||||||
|
- `BoltIcon`, `ArrowUpIcon`, `ArrowDownIcon`, `ArrowRightIcon`, `ArrowLeftIcon`
|
||||||
|
- `PencilIcon`, `TrashBinIcon`, `DownloadIcon`, `CopyIcon`
|
||||||
|
- `EyeIcon`, `EyeCloseIcon`, `LockIcon`, `UserIcon`
|
||||||
|
- `FolderIcon`, `FileIcon`, `GridIcon`, `ListIcon`
|
||||||
|
- `ChevronDownIcon`, `ChevronUpIcon`, `ChevronLeftIcon`, `ChevronRightIcon`
|
||||||
|
- `AngleDownIcon`, `AngleUpIcon`, `AngleLeftIcon`, `AngleRightIcon`
|
||||||
|
|
||||||
|
**Module Icons:**
|
||||||
|
- `TaskIcon`, `PageIcon`, `TableIcon`, `CalendarIcon`
|
||||||
|
- `PlugInIcon`, `DocsIcon`, `MailIcon`, `ChatIcon`
|
||||||
|
- `PieChartIcon`, `BoxCubeIcon`, `GroupIcon`
|
||||||
|
- `ShootingStarIcon`, `DollarLineIcon`
|
||||||
|
|
||||||
|
**Aliases (for compatibility):**
|
||||||
|
- `TrashIcon` → `TrashBinIcon`
|
||||||
|
- `XIcon` / `XMarkIcon` → `CloseIcon`
|
||||||
|
- `SearchIcon` → `GridIcon`
|
||||||
|
- `SettingsIcon` → `BoxCubeIcon`
|
||||||
|
- `FilterIcon` → `ListIcon`
|
||||||
|
|
||||||
|
### Adding New Icons
|
||||||
|
|
||||||
|
1. Add SVG file to `src/icons/`
|
||||||
|
2. Export in `src/icons/index.ts`:
|
||||||
|
```ts
|
||||||
|
import { ReactComponent as NewIcon } from "./new-icon.svg?react";
|
||||||
|
export { NewIcon };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. FOLDER STRUCTURE
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # UI Components (display/interaction)
|
||||||
|
│ │ ├── accordion/
|
||||||
|
│ │ ├── alert/
|
||||||
|
│ │ │ ├── Alert.tsx
|
||||||
|
│ │ │ └── AlertModal.tsx
|
||||||
|
│ │ ├── avatar/
|
||||||
|
│ │ ├── badge/
|
||||||
|
│ │ │ └── Badge.tsx
|
||||||
|
│ │ ├── breadcrumb/
|
||||||
|
│ │ ├── button/
|
||||||
|
│ │ │ ├── Button.tsx # ← Standard button
|
||||||
|
│ │ │ ├── IconButton.tsx # ← Icon-only button
|
||||||
|
│ │ │ └── ButtonWithTooltip.tsx
|
||||||
|
│ │ ├── button-group/
|
||||||
|
│ │ ├── card/
|
||||||
|
│ │ ├── dataview/
|
||||||
|
│ │ ├── dropdown/
|
||||||
|
│ │ ├── list/
|
||||||
|
│ │ ├── modal/
|
||||||
|
│ │ ├── pagination/
|
||||||
|
│ │ ├── progress/
|
||||||
|
│ │ ├── ribbon/
|
||||||
|
│ │ ├── spinner/
|
||||||
|
│ │ ├── table/
|
||||||
|
│ │ ├── tabs/
|
||||||
|
│ │ ├── toast/
|
||||||
|
│ │ ├── tooltip/
|
||||||
|
│ │ └── videos/
|
||||||
|
│ │
|
||||||
|
│ └── form/ # Form Components (inputs)
|
||||||
|
│ ├── input/
|
||||||
|
│ │ ├── InputField.tsx # ← Text/number/email inputs
|
||||||
|
│ │ ├── Checkbox.tsx # ← Checkbox
|
||||||
|
│ │ ├── Radio.tsx # ← Radio button
|
||||||
|
│ │ ├── RadioSm.tsx # ← Small radio button
|
||||||
|
│ │ ├── FileInput.tsx # ← File upload
|
||||||
|
│ │ └── TextArea.tsx # ← Multi-line text
|
||||||
|
│ ├── switch/
|
||||||
|
│ │ └── Switch.tsx # ← Toggle switch
|
||||||
|
│ ├── Select.tsx # ← Dropdown select
|
||||||
|
│ ├── SelectDropdown.tsx # ← Searchable dropdown
|
||||||
|
│ ├── MultiSelect.tsx # ← Multi-select dropdown
|
||||||
|
│ ├── Label.tsx # ← Form labels
|
||||||
|
│ ├── Form.tsx # ← Form wrapper
|
||||||
|
│ └── date-picker.tsx # ← Date picker
|
||||||
|
│
|
||||||
|
├── icons/ # All SVG icons
|
||||||
|
│ ├── index.ts # ← Export all icons from here
|
||||||
|
│ ├── plus.svg
|
||||||
|
│ ├── close.svg
|
||||||
|
│ └── ... (50+ icons)
|
||||||
|
│
|
||||||
|
└── styles/
|
||||||
|
└── design-system.css # ← Global design tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ESLINT ENFORCEMENT
|
||||||
|
|
||||||
|
### Rules File Location
|
||||||
|
`eslint-plugin-igny8-design-system.cjs` (project root)
|
||||||
|
|
||||||
|
### Active Rules
|
||||||
|
|
||||||
|
| Rule | Severity | Description |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| `no-raw-button` | warn | Use `Button` or `IconButton` instead of `<button>` |
|
||||||
|
| `no-raw-input` | warn | Use `InputField`, `Checkbox`, `Radio`, `FileInput` instead of `<input>` |
|
||||||
|
| `no-raw-select` | warn | Use `Select` or `SelectDropdown` instead of `<select>` |
|
||||||
|
| `no-raw-textarea` | warn | Use `TextArea` instead of `<textarea>` |
|
||||||
|
| `no-restricted-imports` | error | Block imports from `@heroicons/*`, `lucide-react`, `@mui/icons-material` |
|
||||||
|
|
||||||
|
### Running Lint Check
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Violations
|
||||||
|
```bash
|
||||||
|
npm run lint 2>&1 | grep "igny8-design-system"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. MIGRATION EXAMPLES
|
||||||
|
|
||||||
|
### Raw Button → Button Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ BEFORE
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-brand-500 text-white rounded hover:bg-brand-600"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// ✅ AFTER
|
||||||
|
<Button variant="primary" tone="brand" onClick={handleClick}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw Button → IconButton
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ BEFORE
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<CloseIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// ✅ AFTER
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw Input → InputField
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ BEFORE
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border rounded px-3 py-2 focus:ring-2"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Enter name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
// ✅ AFTER
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
label="Name"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Enter name"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw Select → Select Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ BEFORE
|
||||||
|
<select
|
||||||
|
className="border rounded px-3 py-2"
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => setStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select status</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
// ✅ AFTER
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'inactive', label: 'Inactive' },
|
||||||
|
]}
|
||||||
|
placeholder="Select status"
|
||||||
|
defaultValue={status}
|
||||||
|
onChange={(value) => setStatus(value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Icon → Internal Icon
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ BEFORE
|
||||||
|
import { XIcon } from '@heroicons/react/24/outline';
|
||||||
|
<XIcon className="w-5 h-5" />
|
||||||
|
|
||||||
|
// ✅ AFTER
|
||||||
|
import { CloseIcon } from '../../icons';
|
||||||
|
<CloseIcon className="w-5 h-5" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. LIVE REFERENCE
|
||||||
|
|
||||||
|
View all components with live examples at: `/ui-elements`
|
||||||
|
|
||||||
|
This page shows every component with all prop variations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. AI AGENT INSTRUCTIONS
|
||||||
|
|
||||||
|
When working on this codebase, AI agents MUST:
|
||||||
|
|
||||||
|
1. **Never use raw HTML elements** (`<button>`, `<input>`, `<select>`, `<textarea>`)
|
||||||
|
2. **Import icons only from `src/icons`** - never from external libraries
|
||||||
|
3. **Follow the import paths** specified in this document
|
||||||
|
4. **Check ESLint** after making changes: `npm run lint`
|
||||||
|
5. **Reference this document** for correct component usage
|
||||||
|
6. **Use consistent icon sizing**: `className="w-4 h-4"` for small, `w-5 h-5` for medium
|
||||||
|
|
||||||
|
If a component doesn't exist, create it in `components/ui/` or `components/form/` first.
|
||||||
@@ -4,6 +4,10 @@ import reactHooks from 'eslint-plugin-react-hooks'
|
|||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
// Custom IGNY8 Design System plugin
|
||||||
|
// See: docs/30-FRONTEND/COMPONENT-SYSTEM.md for full documentation
|
||||||
|
import igny8DesignSystem from './eslint/eslint-plugin-igny8-design-system.cjs'
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ ignores: ['dist'] },
|
||||||
{
|
{
|
||||||
@@ -16,6 +20,7 @@ export default tseslint.config(
|
|||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
'react-refresh': reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
|
'igny8-design-system': igny8DesignSystem,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
@@ -23,6 +28,27 @@ export default tseslint.config(
|
|||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
// =====================================================
|
||||||
|
// IGNY8 DESIGN SYSTEM ENFORCEMENT
|
||||||
|
// These rules prevent inconsistent component usage
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
// Prevent direct imports from icon libraries - must use src/icons
|
||||||
|
'no-restricted-imports': ['error', {
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ['@heroicons/*', 'lucide-react', '@mui/icons-material', 'react-icons/*'],
|
||||||
|
message: 'Import icons from src/icons instead. Add new icons to src/icons/index.ts if needed.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
|
||||||
|
// IGNY8 Design System Rules - Set to 'warn' initially, change to 'error' after fixing existing code
|
||||||
|
// These will show warnings for all raw HTML elements that should use components
|
||||||
|
'igny8-design-system/no-raw-button': 'warn',
|
||||||
|
'igny8-design-system/no-raw-input': 'warn',
|
||||||
|
'igny8-design-system/no-raw-select': 'warn',
|
||||||
|
'igny8-design-system/no-raw-textarea': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
230
frontend/eslint/eslint-plugin-igny8-design-system.cjs
Normal file
230
frontend/eslint/eslint-plugin-igny8-design-system.cjs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* IGNY8 Design System ESLint Plugin
|
||||||
|
*
|
||||||
|
* This plugin enforces consistent component usage across the application.
|
||||||
|
* It prevents raw HTML elements and ensures all UI elements come from
|
||||||
|
* the central component library.
|
||||||
|
*
|
||||||
|
* DOCUMENTATION:
|
||||||
|
* - Full component reference: docs/30-FRONTEND/COMPONENT-SYSTEM.md
|
||||||
|
* - Design guide: DESIGN-GUIDE.md (root)
|
||||||
|
* - Live demo: /ui-elements route
|
||||||
|
*
|
||||||
|
* RULES:
|
||||||
|
* 1. no-raw-button - Use <Button> from components/ui/button/Button
|
||||||
|
* 2. no-raw-input - Use <InputField> from components/form/input/InputField
|
||||||
|
* 3. no-raw-select - Use <Select> from components/form/Select
|
||||||
|
* 4. no-raw-textarea - Use <TextArea> from components/form/input/TextArea
|
||||||
|
*
|
||||||
|
* USAGE: Import in eslint.config.js and enable rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
name: 'eslint-plugin-igny8-design-system',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
/**
|
||||||
|
* Disallow raw <button> elements
|
||||||
|
* Must use <Button> component from components/ui/button/Button
|
||||||
|
*/
|
||||||
|
'no-raw-button': {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow raw <button> elements. Use <Button> from components/ui/button/Button instead.',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
useButtonComponent: 'Use <Button> from components/ui/button/Button instead of raw <button>. For icon-only buttons, use <IconButton> from components/ui/button/IconButton.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
JSXOpeningElement(node) {
|
||||||
|
if (node.name.type === 'JSXIdentifier' && node.name.name === 'button') {
|
||||||
|
// Allow in specific files that need raw buttons (like the Button component itself)
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const allowedFiles = [
|
||||||
|
'Button.tsx',
|
||||||
|
'IconButton.tsx',
|
||||||
|
'ButtonGroup.tsx',
|
||||||
|
'Components.tsx', // Demo page
|
||||||
|
'UIElements.tsx', // Demo page
|
||||||
|
// Low-level UI components that need raw buttons
|
||||||
|
'Pagination.tsx',
|
||||||
|
'CompactPagination.tsx',
|
||||||
|
'AlertModal.tsx',
|
||||||
|
'Accordion.tsx',
|
||||||
|
'AccordionItem.tsx',
|
||||||
|
'Tabs.tsx',
|
||||||
|
'Tab.tsx',
|
||||||
|
'DropdownItem.tsx',
|
||||||
|
'SelectDropdown.tsx',
|
||||||
|
'MultiSelect.tsx',
|
||||||
|
'List.tsx',
|
||||||
|
'ListRadioItem.tsx',
|
||||||
|
'Dropdown.tsx',
|
||||||
|
// Form components
|
||||||
|
'PhoneInput.tsx',
|
||||||
|
'SearchModal.tsx',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedFiles.some(f => filename.endsWith(f))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useButtonComponent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disallow raw <input> elements
|
||||||
|
* Must use <InputField>, <Checkbox>, <Radio>, or <FileInput>
|
||||||
|
*/
|
||||||
|
'no-raw-input': {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow raw <input> elements. Use InputField, Checkbox, Radio, or FileInput from components/form/input.',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
useInputComponent: 'Use InputField, Checkbox, Radio, or FileInput from components/form/input instead of raw <input>.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
JSXOpeningElement(node) {
|
||||||
|
if (node.name.type === 'JSXIdentifier' && node.name.name === 'input') {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const allowedFiles = [
|
||||||
|
'InputField.tsx',
|
||||||
|
'Checkbox.tsx',
|
||||||
|
'Radio.tsx',
|
||||||
|
'RadioSm.tsx',
|
||||||
|
'FileInput.tsx',
|
||||||
|
'Switch.tsx',
|
||||||
|
'Components.tsx',
|
||||||
|
'UIElements.tsx',
|
||||||
|
'date-picker.tsx',
|
||||||
|
// Low-level UI components
|
||||||
|
'Pagination.tsx',
|
||||||
|
'CompactPagination.tsx',
|
||||||
|
'ListRadioItem.tsx',
|
||||||
|
'SearchModal.tsx',
|
||||||
|
'PhoneInput.tsx',
|
||||||
|
'SelectDropdown.tsx',
|
||||||
|
'MultiSelect.tsx',
|
||||||
|
'ConfigModal.tsx', // Complex automation config
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedFiles.some(f => filename.endsWith(f))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useInputComponent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disallow raw <select> elements
|
||||||
|
* Must use <Select> or <SelectDropdown> from components/form/
|
||||||
|
*/
|
||||||
|
'no-raw-select': {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow raw <select> elements. Use Select or SelectDropdown from components/form.',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
useSelectComponent: 'Use Select or SelectDropdown from components/form instead of raw <select>.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
JSXOpeningElement(node) {
|
||||||
|
if (node.name.type === 'JSXIdentifier' && node.name.name === 'select') {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const allowedFiles = [
|
||||||
|
'Select.tsx',
|
||||||
|
'SelectDropdown.tsx',
|
||||||
|
'MultiSelect.tsx',
|
||||||
|
'Components.tsx',
|
||||||
|
'UIElements.tsx',
|
||||||
|
'CompactPagination.tsx', // Has custom styling needs
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedFiles.some(f => filename.endsWith(f))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useSelectComponent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disallow raw <textarea> elements
|
||||||
|
* Must use <TextArea> from components/form/input/TextArea
|
||||||
|
*/
|
||||||
|
'no-raw-textarea': {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow raw <textarea> elements. Use TextArea from components/form/input/TextArea.',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
useTextAreaComponent: 'Use TextArea from components/form/input/TextArea instead of raw <textarea>.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
JSXOpeningElement(node) {
|
||||||
|
if (node.name.type === 'JSXIdentifier' && node.name.name === 'textarea') {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const allowedFiles = [
|
||||||
|
'TextArea.tsx',
|
||||||
|
'Components.tsx',
|
||||||
|
'UIElements.tsx',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowedFiles.some(f => filename.endsWith(f))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'useTextAreaComponent',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -116,6 +116,9 @@ const Help = lazy(() => import("./pages/Help/Help"));
|
|||||||
// Components - Lazy loaded
|
// Components - Lazy loaded
|
||||||
const Components = lazy(() => import("./pages/Components"));
|
const Components = lazy(() => import("./pages/Components"));
|
||||||
|
|
||||||
|
// UI Elements - Lazy loaded (Design System Reference)
|
||||||
|
const UIElements = lazy(() => import("./pages/UIElements"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -279,6 +282,9 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Components (Showcase Page) */}
|
{/* Components (Showcase Page) */}
|
||||||
<Route path="/components" element={<Components />} />
|
<Route path="/components" element={<Components />} />
|
||||||
|
|
||||||
|
{/* UI Elements (Design System - Non-indexable) */}
|
||||||
|
<Route path="/ui-elements" element={<UIElements />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Fallback Route */}
|
{/* Fallback Route */}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { automationService } from '../../services/automationService';
|
import { automationService } from '../../services/automationService';
|
||||||
import ComponentCard from '../common/ComponentCard';
|
import ComponentCard from '../common/ComponentCard';
|
||||||
|
import Select from '../form/Select';
|
||||||
|
|
||||||
interface ActivityLogProps {
|
interface ActivityLogProps {
|
||||||
runId: string;
|
runId: string;
|
||||||
@@ -39,16 +40,16 @@ const ActivityLog: React.FC<ActivityLogProps> = ({ runId }) => {
|
|||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-sm text-gray-600 dark:text-gray-400">Lines:</label>
|
<label className="text-sm text-gray-600 dark:text-gray-400">Lines:</label>
|
||||||
<select
|
<Select
|
||||||
value={lines}
|
options={[
|
||||||
onChange={(e) => setLines(parseInt(e.target.value))}
|
{ value: '50', label: '50' },
|
||||||
className="border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
{ value: '100', label: '100' },
|
||||||
>
|
{ value: '200', label: '200' },
|
||||||
<option value={50}>50</option>
|
{ value: '500', label: '500' },
|
||||||
<option value={100}>100</option>
|
]}
|
||||||
<option value={200}>200</option>
|
defaultValue={String(lines)}
|
||||||
<option value={500}>500</option>
|
onChange={(val) => setLines(parseInt(val))}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900 dark:bg-gray-950 text-success-400 p-4 rounded-lg font-mono text-xs overflow-auto max-h-96 border border-gray-700">
|
<div className="bg-gray-900 dark:bg-gray-950 text-success-400 p-4 rounded-lg font-mono text-xs overflow-auto max-h-96 border border-gray-700">
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import React, { useState } from 'react';
|
|||||||
import { AutomationConfig } from '../../services/automationService';
|
import { AutomationConfig } from '../../services/automationService';
|
||||||
import { Modal } from '../ui/modal';
|
import { Modal } from '../ui/modal';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import Checkbox from '../form/input/Checkbox';
|
||||||
|
import Select from '../form/Select';
|
||||||
|
import InputField from '../form/input/InputField';
|
||||||
|
|
||||||
interface ConfigModalProps {
|
interface ConfigModalProps {
|
||||||
config: AutomationConfig;
|
config: AutomationConfig;
|
||||||
@@ -51,17 +54,13 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Enable/Disable */}
|
{/* Enable/Disable */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="flex items-center">
|
<Checkbox
|
||||||
<input
|
label="Enable Automation"
|
||||||
type="checkbox"
|
checked={formData.is_enabled || false}
|
||||||
checked={formData.is_enabled || false}
|
onChange={(checked) =>
|
||||||
onChange={(e) =>
|
setFormData({ ...formData, is_enabled: checked })
|
||||||
setFormData({ ...formData, is_enabled: e.target.checked })
|
}
|
||||||
}
|
/>
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<span className="font-semibold">Enable Automation</span>
|
|
||||||
</label>
|
|
||||||
<p className="text-sm text-gray-600 ml-6">
|
<p className="text-sm text-gray-600 ml-6">
|
||||||
When enabled, automation will run on the configured schedule
|
When enabled, automation will run on the configured schedule
|
||||||
</p>
|
</p>
|
||||||
@@ -70,36 +69,33 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
{/* Frequency */}
|
{/* Frequency */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block font-semibold mb-1">Frequency</label>
|
<label className="block font-semibold mb-1">Frequency</label>
|
||||||
<select
|
<Select
|
||||||
value={formData.frequency || 'daily'}
|
options={[
|
||||||
onChange={(e) =>
|
{ value: 'daily', label: 'Daily' },
|
||||||
|
{ value: 'weekly', label: 'Weekly (Mondays)' },
|
||||||
|
{ value: 'monthly', label: 'Monthly (1st of month)' },
|
||||||
|
]}
|
||||||
|
defaultValue={formData.frequency || 'daily'}
|
||||||
|
onChange={(val) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
frequency: e.target.value as 'daily' | 'weekly' | 'monthly',
|
frequency: val as 'daily' | 'weekly' | 'monthly',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="border rounded px-3 py-2 w-full"
|
/>
|
||||||
>
|
|
||||||
<option value="daily">Daily</option>
|
|
||||||
<option value="weekly">Weekly (Mondays)</option>
|
|
||||||
<option value="monthly">Monthly (1st of month)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scheduled Time */}
|
{/* Scheduled Time */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block font-semibold mb-1">Scheduled Time</label>
|
<InputField
|
||||||
<input
|
label="Scheduled Time"
|
||||||
type="time"
|
type="time"
|
||||||
value={formData.scheduled_time || '02:00'}
|
value={formData.scheduled_time || '02:00'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, scheduled_time: e.target.value })
|
setFormData({ ...formData, scheduled_time: e.target.value })
|
||||||
}
|
}
|
||||||
className="border rounded px-3 py-2 w-full"
|
hint="Time of day to run automation (24-hour format)"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
Time of day to run automation (24-hour format)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Batch Sizes */}
|
{/* Batch Sizes */}
|
||||||
@@ -111,10 +107,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 1: Keywords → Clusters
|
label="Stage 1: Keywords → Clusters"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_1_batch_size || 20}
|
value={formData.stage_1_batch_size || 20}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -123,17 +117,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_1_batch_size: parseInt(e.target.value),
|
stage_1_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={100}
|
max="100"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 2: Clusters → Ideas
|
label="Stage 2: Clusters → Ideas"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_2_batch_size || 1}
|
value={formData.stage_2_batch_size || 1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -142,17 +133,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_2_batch_size: parseInt(e.target.value),
|
stage_2_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={10}
|
max="10"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 3: Ideas → Tasks
|
label="Stage 3: Ideas → Tasks"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_3_batch_size || 20}
|
value={formData.stage_3_batch_size || 20}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -161,17 +149,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_3_batch_size: parseInt(e.target.value),
|
stage_3_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={100}
|
max="100"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 4: Tasks → Content
|
label="Stage 4: Tasks → Content"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_4_batch_size || 1}
|
value={formData.stage_4_batch_size || 1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -180,17 +165,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_4_batch_size: parseInt(e.target.value),
|
stage_4_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={10}
|
max="10"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 5: Content → Image Prompts
|
label="Stage 5: Content → Image Prompts"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_5_batch_size || 1}
|
value={formData.stage_5_batch_size || 1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -199,17 +181,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_5_batch_size: parseInt(e.target.value),
|
stage_5_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={10}
|
max="10"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Stage 6: Image Prompts → Images
|
label="Stage 6: Image Prompts → Images"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.stage_6_batch_size || 1}
|
value={formData.stage_6_batch_size || 1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -218,9 +197,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
stage_6_batch_size: parseInt(e.target.value),
|
stage_6_batch_size: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={1}
|
min="1"
|
||||||
max={10}
|
max="10"
|
||||||
className="border rounded px-3 py-2 w-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,10 +213,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Within-Stage Delay (seconds)
|
label="Within-Stage Delay (seconds)"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.within_stage_delay || 3}
|
value={formData.within_stage_delay || 3}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -247,20 +223,15 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
within_stage_delay: parseInt(e.target.value),
|
within_stage_delay: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={0}
|
min="0"
|
||||||
max={30}
|
max="30"
|
||||||
className="border rounded px-3 py-2 w-full"
|
hint="Delay between batches within a stage"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Delay between batches within a stage
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<InputField
|
||||||
Between-Stage Delay (seconds)
|
label="Between-Stage Delay (seconds)"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.between_stage_delay || 5}
|
value={formData.between_stage_delay || 5}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -269,13 +240,10 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
|||||||
between_stage_delay: parseInt(e.target.value),
|
between_stage_delay: parseInt(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
min={0}
|
min="0"
|
||||||
max={60}
|
max="60"
|
||||||
className="border rounded px-3 py-2 w-full"
|
hint="Delay between stage transitions"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Delay between stage transitions
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import { useToast } from '../ui/toast/ToastContainer';
|
import { useToast } from '../ui/toast/ToastContainer';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
import {
|
import {
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PauseIcon,
|
PauseIcon,
|
||||||
@@ -243,12 +244,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
<div className="bg-error-50 dark:bg-error-900/20 border-2 border-error-500 rounded-lg p-4 mb-6">
|
<div className="bg-error-50 dark:bg-error-900/20 border-2 border-error-500 rounded-lg p-4 mb-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-error-700 dark:text-error-300 text-sm">{error}</p>
|
<p className="text-error-700 dark:text-error-300 text-sm">{error}</p>
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<XMarkIcon className="w-5 h-5" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
|
title="Close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-error-500 hover:text-error-700 dark:hover:text-error-300"
|
/>
|
||||||
>
|
|
||||||
<XMarkIcon className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -564,13 +567,15 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* Debug table toggle + table for stage data */}
|
{/* Debug table toggle + table for stage data */}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="xs"
|
||||||
onClick={() => setShowDebugTable(!showDebugTable)}
|
onClick={() => setShowDebugTable(!showDebugTable)}
|
||||||
className="text-xs text-gray-600 hover:underline"
|
|
||||||
>
|
>
|
||||||
{showDebugTable ? 'Hide' : 'Show'} debug table
|
{showDebugTable ? 'Hide' : 'Show'} debug table
|
||||||
</button>
|
</Button>
|
||||||
{showDebugTable && (
|
{showDebugTable && (
|
||||||
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
|
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
|
||||||
<div className="text-sm font-semibold mb-2">Stage Data</div>
|
<div className="text-sm font-semibold mb-2">Stage Data</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
||||||
import { useToast } from '../ui/toast/ToastContainer';
|
import { useToast } from '../ui/toast/ToastContainer';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
import {
|
import {
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PauseIcon,
|
PauseIcon,
|
||||||
@@ -228,13 +229,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Close Button - Top Right */}
|
{/* Close Button - Top Right */}
|
||||||
<button
|
<IconButton
|
||||||
onClick={onClose}
|
icon={<XMarkIcon className="w-5 h-5" />}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
title="Close"
|
title="Close"
|
||||||
>
|
onClick={onClose}
|
||||||
<XMarkIcon className="w-5 h-5" />
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Section */}
|
{/* Progress Section */}
|
||||||
|
|||||||
@@ -59,27 +59,31 @@ export default function UserAddressCard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
startIcon={
|
||||||
|
<svg
|
||||||
|
className="fill-current"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||||
|
fill=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg
|
|
||||||
className="fill-current"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||||
|
|||||||
@@ -67,27 +67,31 @@ export default function UserInfoCard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
startIcon={
|
||||||
|
<svg
|
||||||
|
className="fill-current"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||||
|
fill=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg
|
|
||||||
className="fill-current"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||||
|
|||||||
@@ -119,27 +119,31 @@ export default function UserMetaCard() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
startIcon={
|
||||||
|
<svg
|
||||||
|
className="fill-current"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||||
|
fill=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg
|
|
||||||
className="fill-current"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Button } from '../ui/button/Button';
|
import { Button } from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
import { Dropdown } from '../ui/dropdown/Dropdown';
|
import { Dropdown } from '../ui/dropdown/Dropdown';
|
||||||
import {
|
import {
|
||||||
MoreDotIcon,
|
MoreDotIcon,
|
||||||
@@ -66,14 +67,15 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<IconButton
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
aria-label="more actions"
|
icon={<MoreDotIcon className="h-5 w-5" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="more actions"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="dropdown-toggle flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
/>
|
||||||
>
|
|
||||||
<MoreDotIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -86,61 +88,76 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
|||||||
{/* WordPress Publishing - Only show if images are ready */}
|
{/* WordPress Publishing - Only show if images are ready */}
|
||||||
{canPublishToWordPress && (
|
{canPublishToWordPress && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={handlePublishClick}
|
onClick={handlePublishClick}
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
|
||||||
|
className="w-full justify-start px-4 py-2"
|
||||||
>
|
>
|
||||||
<PaperPlaneIcon className="h-4 w-4" />
|
Publish to Site
|
||||||
<span>Publish to Site</span>
|
</Button>
|
||||||
</button>
|
|
||||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Action */}
|
{/* Edit Action */}
|
||||||
{onEdit && (
|
{onEdit && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleMenuAction(onEdit)}
|
onClick={() => handleMenuAction(onEdit)}
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
startIcon={<PencilIcon className="h-4 w-4" />}
|
||||||
|
className="w-full justify-start px-4 py-2"
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-4 w-4" />
|
Edit
|
||||||
<span>Edit</span>
|
</Button>
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Generate Image Action */}
|
{/* Generate Image Action */}
|
||||||
{onGenerateImage && (
|
{onGenerateImage && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleMenuAction(onGenerateImage)}
|
onClick={() => handleMenuAction(onGenerateImage)}
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
startIcon={<FileIcon className="h-4 w-4" />}
|
||||||
|
className="w-full justify-start px-4 py-2"
|
||||||
>
|
>
|
||||||
<FileIcon className="h-4 w-4" />
|
Generate Image Prompts
|
||||||
<span>Generate Image Prompts</span>
|
</Button>
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Export Action */}
|
{/* Export Action */}
|
||||||
{onExport && (
|
{onExport && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleMenuAction(onExport)}
|
onClick={() => handleMenuAction(onExport)}
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
startIcon={<DownloadIcon className="h-4 w-4" />}
|
||||||
|
className="w-full justify-start px-4 py-2"
|
||||||
>
|
>
|
||||||
<DownloadIcon className="h-4 w-4" />
|
Export
|
||||||
<span>Export</span>
|
</Button>
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Action */}
|
{/* Delete Action */}
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
<>
|
<>
|
||||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleMenuAction(onDelete)}
|
onClick={() => handleMenuAction(onDelete)}
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-error-600 hover:bg-error-50 dark:text-error-400 dark:hover:bg-error-500/10"
|
startIcon={<TrashBinIcon className="h-4 w-4" />}
|
||||||
|
className="w-full justify-start px-4 py-2"
|
||||||
>
|
>
|
||||||
<TrashBinIcon className="h-4 w-4" />
|
Delete
|
||||||
<span>Delete</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function SignInForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
|
||||||
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
@@ -140,8 +140,8 @@ export default function SignInForm() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Sign in with Google
|
Sign in with Google
|
||||||
</button>
|
</Button>
|
||||||
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||||
<svg
|
<svg
|
||||||
width="21"
|
width="21"
|
||||||
className="fill-current"
|
className="fill-current"
|
||||||
@@ -153,7 +153,7 @@ export default function SignInForm() {
|
|||||||
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
|
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
|
||||||
</svg>
|
</svg>
|
||||||
Sign in with X
|
Sign in with X
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative py-3 sm:py-5">
|
<div className="relative py-3 sm:py-5">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
@@ -246,21 +246,27 @@ export default function SignInForm() {
|
|||||||
You're trying to login as: <strong>{sessionConflict.requestedUser.email}</strong>
|
You're trying to login as: <strong>{sessionConflict.requestedUser.email}</strong>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
tone="warning"
|
||||||
|
size="sm"
|
||||||
onClick={handleForceLogout}
|
onClick={handleForceLogout}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-warning-600 rounded-lg hover:bg-warning-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{loading ? 'Logging out...' : 'Logout Previous & Continue'}
|
{loading ? 'Logging out...' : 'Logout Previous & Continue'}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
tone="warning"
|
||||||
|
size="sm"
|
||||||
onClick={() => setSessionConflict(null)}
|
onClick={() => setSessionConflict(null)}
|
||||||
className="flex-1 px-4 py-2 text-sm font-medium text-warning-700 bg-warning-100 rounded-lg hover:bg-warning-200 dark:bg-warning-900/40 dark:text-warning-300 dark:hover:bg-warning-900/60 transition-colors"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
|||||||
import Label from "../form/Label";
|
import Label from "../form/Label";
|
||||||
import Input from "../form/input/InputField";
|
import Input from "../form/input/InputField";
|
||||||
import Checkbox from "../form/input/Checkbox";
|
import Checkbox from "../form/input/Checkbox";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
import { useAuthStore } from "../../store/authStore";
|
import { useAuthStore } from "../../store/authStore";
|
||||||
|
|
||||||
export default function SignUpForm({ planDetails: planDetailsProp, planLoading: planLoadingProp }: { planDetails?: any; planLoading?: boolean }) {
|
export default function SignUpForm({ planDetails: planDetailsProp, planLoading: planLoadingProp }: { planDetails?: any; planLoading?: boolean }) {
|
||||||
@@ -134,7 +135,7 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
|
||||||
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
@@ -160,8 +161,8 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Sign up with Google
|
Sign up with Google
|
||||||
</button>
|
</Button>
|
||||||
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
|
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||||
<svg
|
<svg
|
||||||
width="21"
|
width="21"
|
||||||
className="fill-current"
|
className="fill-current"
|
||||||
@@ -173,7 +174,7 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
|||||||
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
|
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
|
||||||
</svg>
|
</svg>
|
||||||
Sign up with X
|
Sign up with X
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative py-3 sm:py-5">
|
<div className="relative py-3 sm:py-5">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
@@ -294,13 +295,16 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
|||||||
</div>
|
</div>
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
|
fullWidth
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
{loading ? "Creating your account..." : "Start Free Trial"}
|
{loading ? "Creating your account..." : "Start Free Trial"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Label from '../form/Label';
|
|||||||
import Input from '../form/input/InputField';
|
import Input from '../form/input/InputField';
|
||||||
import Checkbox from '../form/input/Checkbox';
|
import Checkbox from '../form/input/Checkbox';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import SelectDropdown from '../form/SelectDropdown';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
|
||||||
interface PaymentMethodConfig {
|
interface PaymentMethodConfig {
|
||||||
@@ -321,21 +322,21 @@ export default function SignUpFormSimplified({ planDetails: planDetailsProp, pla
|
|||||||
<Label>
|
<Label>
|
||||||
Country<span className="text-error-500">*</span>
|
Country<span className="text-error-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<SelectDropdown
|
||||||
name="billingCountry"
|
options={[
|
||||||
|
{ value: 'US', label: 'United States' },
|
||||||
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
|
{ value: 'CA', label: 'Canada' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'DE', label: 'Germany' },
|
||||||
|
{ value: 'FR', label: 'France' },
|
||||||
|
]}
|
||||||
|
placeholder="Select country"
|
||||||
value={formData.billingCountry}
|
value={formData.billingCountry}
|
||||||
onChange={handleChange}
|
onChange={(val) => setFormData({ ...formData, billingCountry: val })}
|
||||||
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
|
/>
|
||||||
>
|
|
||||||
<option value="US">United States</option>
|
|
||||||
<option value="GB">United Kingdom</option>
|
|
||||||
<option value="IN">India</option>
|
|
||||||
<option value="PK">Pakistan</option>
|
|
||||||
<option value="CA">Canada</option>
|
|
||||||
<option value="AU">Australia</option>
|
|
||||||
<option value="DE">Germany</option>
|
|
||||||
<option value="FR">France</option>
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
Payment methods will be filtered by your country
|
Payment methods will be filtered by your country
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -284,24 +284,28 @@ export default function SignUpFormUnified({
|
|||||||
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-28'
|
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-28'
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
||||||
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annually')}
|
onClick={() => setBillingPeriod('annually')}
|
||||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
||||||
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Annually
|
Annually
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-6 flex items-center justify-center">
|
<div className="h-6 flex items-center justify-center">
|
||||||
<span className={`inline-flex items-center gap-1.5 text-xs text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-2 py-1 rounded-full transition-opacity duration-200 ${
|
<span className={`inline-flex items-center gap-1.5 text-xs text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-2 py-1 rounded-full transition-opacity duration-200 ${
|
||||||
@@ -345,8 +349,11 @@ export default function SignUpFormUnified({
|
|||||||
const isFree = parseFloat(String(plan.price || 0)) === 0;
|
const isFree = parseFloat(String(plan.price || 0)) === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={plan.id}
|
key={plan.id}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={() => onPlanSelect(plan)}
|
onClick={() => onPlanSelect(plan)}
|
||||||
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
@@ -366,7 +373,7 @@ export default function SignUpFormUnified({
|
|||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{billingPeriod === 'annually' && !isFree ? '/year' : '/month'}
|
{billingPeriod === 'annually' && !isFree ? '/year' : '/month'}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -550,24 +557,28 @@ export default function SignUpFormUnified({
|
|||||||
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-32'
|
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-32'
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
||||||
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annually')}
|
onClick={() => setBillingPeriod('annually')}
|
||||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
||||||
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Annually
|
Annually
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-7 flex items-center justify-center">
|
<div className="h-7 flex items-center justify-center">
|
||||||
<p className={`inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 text-sm font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full transition-opacity duration-200 ${
|
<p className={`inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 text-sm font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full transition-opacity duration-200 ${
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Modal } from '../ui/modal';
|
|||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import Label from '../form/Label';
|
import Label from '../form/Label';
|
||||||
import Input from '../form/input/InputField';
|
import Input from '../form/input/InputField';
|
||||||
|
import TextArea from '../form/input/TextArea';
|
||||||
import { Loader2Icon, UploadIcon, XIcon, CheckCircleIcon } from '../../icons';
|
import { Loader2Icon, UploadIcon, XIcon, CheckCircleIcon } from '../../icons';
|
||||||
import { API_BASE_URL } from '../../services/api';
|
import { API_BASE_URL } from '../../services/api';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
@@ -245,15 +246,12 @@ export default function PaymentConfirmationModal({
|
|||||||
{/* Additional Notes */}
|
{/* Additional Notes */}
|
||||||
<div>
|
<div>
|
||||||
<Label>Additional Notes (Optional)</Label>
|
<Label>Additional Notes (Optional)</Label>
|
||||||
<textarea
|
<TextArea
|
||||||
id="manual_notes"
|
|
||||||
name="manual_notes"
|
|
||||||
value={formData.manual_notes}
|
|
||||||
onChange={(e) => setFormData({ ...formData, manual_notes: e.target.value })}
|
|
||||||
placeholder="Any additional information about the payment..."
|
placeholder="Any additional information about the payment..."
|
||||||
rows={3}
|
rows={3}
|
||||||
|
value={formData.manual_notes}
|
||||||
|
onChange={(val) => setFormData({ ...formData, manual_notes: val })}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-4 py-3 text-sm border border-gray-300 rounded-lg focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:focus:border-brand-400"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
|
||||||
const ChartTab: React.FC = () => {
|
const ChartTab: React.FC = () => {
|
||||||
const [selected, setSelected] = useState<
|
const [selected, setSelected] = useState<
|
||||||
@@ -12,32 +13,35 @@ const ChartTab: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setSelected("optionOne")}
|
onClick={() => setSelected("optionOne")}
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||||
"optionOne"
|
"optionOne"
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setSelected("optionTwo")}
|
onClick={() => setSelected("optionTwo")}
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||||
"optionTwo"
|
"optionTwo"
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
Quarterly
|
Quarterly
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setSelected("optionThree")}
|
onClick={() => setSelected("optionThree")}
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||||
"optionThree"
|
"optionThree"
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
Annually
|
Annually
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
export interface ContentImageData {
|
export interface ContentImageData {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -73,12 +74,15 @@ export default function ContentImageCell({ image, maxPromptLength = 100, showPro
|
|||||||
<p className="text-gray-700 dark:text-gray-300">
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
{displayPrompt}
|
{displayPrompt}
|
||||||
{shouldTruncate && (
|
{shouldTruncate && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="brand"
|
||||||
|
size="xs"
|
||||||
onClick={() => setShowFullPrompt(!showFullPrompt)}
|
onClick={() => setShowFullPrompt(!showFullPrompt)}
|
||||||
className="ml-1 text-brand-500 hover:text-brand-600 text-xs"
|
className="ml-1 p-0 h-auto text-xs"
|
||||||
>
|
>
|
||||||
{showFullPrompt ? 'Show less' : 'Show more'}
|
{showFullPrompt ? 'Show less' : 'Show more'}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import { Modal } from '../ui/modal';
|
import { Modal } from '../ui/modal';
|
||||||
import { CloseIcon } from '../../icons';
|
import { CloseIcon } from '../../icons';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
|
|
||||||
interface ContentViewerModalProps {
|
interface ContentViewerModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -30,12 +32,14 @@ export default function ContentViewerModal({
|
|||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<CloseIcon className="w-6 h-6" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="Close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
|
/>
|
||||||
>
|
|
||||||
<CloseIcon className="w-6 h-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
@@ -48,12 +52,14 @@ export default function ContentViewerModal({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Modal } from '../ui/modal';
|
|||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import SelectDropdown from '../form/SelectDropdown';
|
import SelectDropdown from '../form/SelectDropdown';
|
||||||
import Label from '../form/Label';
|
import Label from '../form/Label';
|
||||||
|
import InputField from '../form/input/InputField';
|
||||||
|
import TextArea from '../form/input/TextArea';
|
||||||
|
|
||||||
export interface FormField {
|
export interface FormField {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -69,17 +71,15 @@ export default function FormModal({
|
|||||||
<>
|
<>
|
||||||
{fields.find(f => f.key === 'keyword') && (
|
{fields.find(f => f.key === 'keyword') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Label className="mb-2">
|
||||||
{fields.find(f => f.key === 'keyword')!.label}
|
{fields.find(f => f.key === 'keyword')!.label}
|
||||||
{fields.find(f => f.key === 'keyword')!.required && <span className="text-error-500 ml-1">*</span>}
|
{fields.find(f => f.key === 'keyword')!.required && <span className="text-error-500 ml-1">*</span>}
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={fields.find(f => f.key === 'keyword')!.value || ''}
|
value={fields.find(f => f.key === 'keyword')!.value || ''}
|
||||||
onChange={(e) => fields.find(f => f.key === 'keyword')!.onChange(e.target.value)}
|
onChange={(e) => fields.find(f => f.key === 'keyword')!.onChange(e.target.value)}
|
||||||
placeholder={fields.find(f => f.key === 'keyword')!.placeholder}
|
placeholder={fields.find(f => f.key === 'keyword')!.placeholder}
|
||||||
required={fields.find(f => f.key === 'keyword')!.required}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -87,20 +87,18 @@ export default function FormModal({
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{fields.find(f => f.key === 'volume') && (
|
{fields.find(f => f.key === 'volume') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Label className="mb-2">
|
||||||
{fields.find(f => f.key === 'volume')!.label}
|
{fields.find(f => f.key === 'volume')!.label}
|
||||||
{fields.find(f => f.key === 'volume')!.required && <span className="text-error-500 ml-1">*</span>}
|
{fields.find(f => f.key === 'volume')!.required && <span className="text-error-500 ml-1">*</span>}
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={fields.find(f => f.key === 'volume')!.value || ''}
|
value={fields.find(f => f.key === 'volume')!.value || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
||||||
fields.find(f => f.key === 'volume')!.onChange(value);
|
fields.find(f => f.key === 'volume')!.onChange(value);
|
||||||
}}
|
}}
|
||||||
placeholder={fields.find(f => f.key === 'volume')!.placeholder}
|
placeholder={fields.find(f => f.key === 'volume')!.placeholder}
|
||||||
required={fields.find(f => f.key === 'volume')!.required}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -121,18 +119,14 @@ export default function FormModal({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={difficultyField.value || ''}
|
value={difficultyField.value || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
||||||
difficultyField.onChange(value);
|
difficultyField.onChange(value);
|
||||||
}}
|
}}
|
||||||
placeholder={difficultyField.placeholder}
|
placeholder={difficultyField.placeholder}
|
||||||
required={difficultyField.required}
|
|
||||||
min={difficultyField.min}
|
|
||||||
max={difficultyField.max}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -161,36 +155,30 @@ export default function FormModal({
|
|||||||
if (field.type === 'textarea') {
|
if (field.type === 'textarea') {
|
||||||
return (
|
return (
|
||||||
<div key={`${field.key}-${idx}`}>
|
<div key={`${field.key}-${idx}`}>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Label className="mb-2">
|
||||||
{field.label}
|
{field.label}
|
||||||
{field.required && <span className="text-error-500 ml-1">*</span>}
|
{field.required && <span className="text-error-500 ml-1">*</span>}
|
||||||
</label>
|
</Label>
|
||||||
<textarea
|
<TextArea
|
||||||
rows={field.rows || 4}
|
rows={field.rows || 4}
|
||||||
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={field.value || ''}
|
value={field.value || ''}
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(val) => field.onChange(val)}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
required={field.required}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={`${field.key}-${idx}`}>
|
<div key={`${field.key}-${idx}`}>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Label className="mb-2">
|
||||||
{field.label}
|
{field.label}
|
||||||
{field.required && <span className="text-error-500 ml-1">*</span>}
|
{field.required && <span className="text-error-500 ml-1">*</span>}
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<InputField
|
||||||
type={field.type}
|
type={field.type}
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={field.value || ''}
|
value={field.value || ''}
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
required={field.required}
|
|
||||||
min={field.min}
|
|
||||||
max={field.max}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useErrorHandler } from '../../hooks/useErrorHandler';
|
import { useErrorHandler } from '../../hooks/useErrorHandler';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
export default function GlobalErrorDisplay() {
|
export default function GlobalErrorDisplay() {
|
||||||
const { errors, clearError, clearAllErrors } = useErrorHandler('GlobalErrorDisplay');
|
const { errors, clearError, clearAllErrors } = useErrorHandler('GlobalErrorDisplay');
|
||||||
@@ -42,23 +44,27 @@ export default function GlobalErrorDisplay() {
|
|||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<span className="text-xl leading-none">×</span>}
|
||||||
onClick={() => clearError(index)}
|
onClick={() => clearError(index)}
|
||||||
className="text-error-600 dark:text-error-400 hover:text-error-800 dark:hover:text-error-200 text-xl leading-none"
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
aria-label="Dismiss error"
|
aria-label="Dismiss error"
|
||||||
>
|
/>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{errors.length > 1 && (
|
{errors.length > 1 && (
|
||||||
<button
|
<Button
|
||||||
onClick={clearAllErrors}
|
onClick={clearAllErrors}
|
||||||
className="w-full px-3 py-2 text-xs bg-error-600 text-white rounded hover:bg-error-700"
|
variant="primary"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
Clear All Errors
|
Clear All Errors
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { ReactNode, useState, useEffect } from 'react';
|
import { ReactNode, useState, useEffect } from 'react';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import TextArea from '../form/input/TextArea';
|
||||||
|
import Select from '../form/Select';
|
||||||
|
import Label from '../form/Label';
|
||||||
import { useToast } from '../ui/toast/ToastContainer';
|
import { useToast } from '../ui/toast/ToastContainer';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
|
|
||||||
@@ -292,14 +295,13 @@ export default function ImageGenerationCard({
|
|||||||
|
|
||||||
{/* Prompt Description - Full Width */}
|
{/* Prompt Description - Full Width */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<Label className="mb-2">
|
||||||
Prompt Description *
|
Prompt Description *
|
||||||
</label>
|
</Label>
|
||||||
<textarea
|
<TextArea
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(val) => setPrompt(val)}
|
||||||
rows={6}
|
rows={6}
|
||||||
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
|
||||||
placeholder="Describe the visual elements, style, mood, and composition you want in the image..."
|
placeholder="Describe the visual elements, style, mood, and composition you want in the image..."
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
@@ -309,14 +311,13 @@ export default function ImageGenerationCard({
|
|||||||
|
|
||||||
{/* Negative Prompt - Small */}
|
{/* Negative Prompt - Small */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<Label className="mb-2">
|
||||||
Negative Prompt
|
Negative Prompt
|
||||||
</label>
|
</Label>
|
||||||
<textarea
|
<TextArea
|
||||||
value={negativePrompt}
|
value={negativePrompt}
|
||||||
onChange={(e) => setNegativePrompt(e.target.value)}
|
onChange={(val) => setNegativePrompt(val)}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
|
||||||
placeholder="Describe what you DON'T want in the image..."
|
placeholder="Describe what you DON'T want in the image..."
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
@@ -328,56 +329,38 @@ export default function ImageGenerationCard({
|
|||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{/* Image Type */}
|
{/* Image Type */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<Label className="mb-2">
|
||||||
Image Type
|
Image Type
|
||||||
</label>
|
</Label>
|
||||||
<select
|
<Select
|
||||||
value={imageType}
|
options={typeOptions}
|
||||||
onChange={(e) => setImageType(e.target.value)}
|
defaultValue={imageType}
|
||||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
onChange={(val) => setImageType(val)}
|
||||||
>
|
/>
|
||||||
{typeOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Size */}
|
{/* Image Size */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<Label className="mb-2">
|
||||||
Image Size
|
Image Size
|
||||||
</label>
|
</Label>
|
||||||
<select
|
<Select
|
||||||
value={imageSize}
|
options={sizeOptions}
|
||||||
onChange={(e) => setImageSize(e.target.value)}
|
defaultValue={imageSize}
|
||||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
onChange={(val) => setImageSize(val)}
|
||||||
>
|
/>
|
||||||
{sizeOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Format */}
|
{/* Image Format */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<Label className="mb-2">
|
||||||
Image Format
|
Image Format
|
||||||
</label>
|
</Label>
|
||||||
<select
|
<Select
|
||||||
value={imageFormat}
|
options={formatOptions}
|
||||||
onChange={(e) => setImageFormat(e.target.value)}
|
defaultValue={imageFormat}
|
||||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
onChange={(val) => setImageFormat(val)}
|
||||||
>
|
/>
|
||||||
{formatOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import { Modal } from '../ui/modal';
|
import { Modal } from '../ui/modal';
|
||||||
import { FileIcon, TimeIcon, CheckCircleIcon, ErrorIcon } from '../../icons';
|
import { FileIcon, TimeIcon, CheckCircleIcon, ErrorIcon } from '../../icons';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
export interface ImageQueueItem {
|
export interface ImageQueueItem {
|
||||||
imageId: number | null;
|
imageId: number | null;
|
||||||
@@ -593,12 +594,14 @@ export default function ImageQueueModal({
|
|||||||
{completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''} of {totalImages} total
|
{completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''} of {totalImages} total
|
||||||
</div>
|
</div>
|
||||||
{allDone && (
|
{allDone && (
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode, useEffect, useState } from 'react';
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
interface ImageResultCardProps {
|
interface ImageResultCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -195,11 +196,13 @@ export default function ImageResultCard({
|
|||||||
</svg>
|
</svg>
|
||||||
View Original
|
View Original
|
||||||
</a>
|
</a>
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(imageData.url);
|
navigator.clipboard.writeText(imageData.url);
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -211,12 +214,13 @@ export default function ImageResultCard({
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
className="mr-2"
|
||||||
>
|
>
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||||
</svg>
|
</svg>
|
||||||
Copy URL
|
Copy URL
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Modal } from '../ui/modal';
|
import { Modal } from '../ui/modal';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
interface SearchModalProps {
|
interface SearchModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -87,11 +88,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
<Modal isOpen={isOpen} onClose={onClose} className="sm:max-w-lg">
|
<Modal isOpen={isOpen} onClose={onClose} className="sm:max-w-lg">
|
||||||
<div className="p-0">
|
<div className="p-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 z-10">
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
{/* Using native input for ref and onKeyDown support - styled to match design system */}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -99,9 +101,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Search pages..."
|
placeholder="Search pages..."
|
||||||
className="w-full pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 bg-transparent focus:outline-none dark:text-white"
|
className="h-9 w-full rounded-lg border appearance-none px-3 py-2 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800 pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 rounded-none border-x-0 border-t-0"
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-gray-400 hidden sm:block">
|
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-gray-400 hidden sm:block z-10">
|
||||||
ESC to close
|
ESC to close
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,10 +115,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredResults.map((result, index) => (
|
filteredResults.map((result, index) => (
|
||||||
<button
|
<Button
|
||||||
key={result.path}
|
key={result.path}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={() => handleSelect(result)}
|
onClick={() => handleSelect(result)}
|
||||||
className={`w-full px-4 py-3 flex items-center gap-3 text-left transition-colors ${
|
className={`w-full px-4 py-3 flex items-center gap-3 text-left justify-start rounded-none ${
|
||||||
index === selectedIndex
|
index === selectedIndex
|
||||||
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400'
|
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400'
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'
|
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
@@ -126,7 +130,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="font-medium">{result.title}</span>
|
<span className="font-medium">{result.title}</span>
|
||||||
</button>
|
</Button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { GridIcon, ListIcon, TableIcon } from "../../icons";
|
import { GridIcon, ListIcon, TableIcon } from "../../icons";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
|
||||||
export type ViewType = "table" | "kanban" | "list";
|
export type ViewType = "table" | "kanban" | "list";
|
||||||
|
|
||||||
@@ -23,19 +24,18 @@ const ViewToggle: React.FC<ViewToggleProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={`inline-flex items-center gap-1 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900 ${className}`}>
|
<div className={`inline-flex items-center gap-1 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900 ${className}`}>
|
||||||
{views.map((view) => (
|
{views.map((view) => (
|
||||||
<button
|
<Button
|
||||||
key={view.type}
|
key={view.type}
|
||||||
onClick={() => onViewChange(view.type)}
|
onClick={() => onViewChange(view.type)}
|
||||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
variant={currentView === view.type ? "secondary" : "ghost"}
|
||||||
currentView === view.type
|
tone="neutral"
|
||||||
? "bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm"
|
size="sm"
|
||||||
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
className={currentView === view.type ? "shadow-sm" : ""}
|
||||||
}`}
|
|
||||||
title={view.label}
|
title={view.label}
|
||||||
|
startIcon={view.icon}
|
||||||
>
|
>
|
||||||
{view.icon}
|
|
||||||
<span className="hidden sm:inline">{view.label}</span>
|
<span className="hidden sm:inline">{view.label}</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { SourceBadge, ContentSource } from './SourceBadge';
|
import { SourceBadge, ContentSource } from './SourceBadge';
|
||||||
|
import InputField from '../form/input/InputField';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
interface ContentFilterProps {
|
interface ContentFilterProps {
|
||||||
onFilterChange: (filters: FilterState) => void;
|
onFilterChange: (filters: FilterState) => void;
|
||||||
@@ -34,12 +36,11 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
|
|||||||
<div className={`space-y-4 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div>
|
<div>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search content..."
|
placeholder="Search content..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-600 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,28 +48,24 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Source</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Source</label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant={filters.source === 'all' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleSourceChange('all')}
|
onClick={() => handleSourceChange('all')}
|
||||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
|
||||||
filters.source === 'all'
|
|
||||||
? 'bg-brand-500 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</button>
|
</Button>
|
||||||
{(['igny8', 'wordpress', 'shopify', 'custom'] as ContentSource[]).map((source) => (
|
{(['igny8', 'wordpress', 'shopify', 'custom'] as ContentSource[]).map((source) => (
|
||||||
<button
|
<Button
|
||||||
key={source}
|
key={source}
|
||||||
|
variant={filters.source === source ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleSourceChange(source)}
|
onClick={() => handleSourceChange(source)}
|
||||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
|
||||||
filters.source === source
|
|
||||||
? 'bg-brand-500 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<SourceBadge source={source} />
|
<SourceBadge source={source} />
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
FileIcon,
|
FileIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
|
||||||
export interface AIOperation {
|
export interface AIOperation {
|
||||||
type: 'clustering' | 'ideas' | 'content' | 'images';
|
type: 'clustering' | 'ideas' | 'content' | 'images';
|
||||||
@@ -73,31 +74,33 @@ export default function AIOperationsWidget({ data, onPeriodChange, loading }: AI
|
|||||||
|
|
||||||
{/* Period Dropdown */}
|
{/* Period Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="xs"
|
||||||
|
endIcon={<ChevronDownIcon className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />}
|
||||||
>
|
>
|
||||||
{currentPeriod.label}
|
{currentPeriod.label}
|
||||||
<ChevronDownIcon className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
|
</Button>
|
||||||
</button>
|
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<div className="absolute right-0 mt-1 w-24 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
<div className="absolute right-0 mt-1 w-24 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
||||||
{periods.map((period) => (
|
{periods.map((period) => (
|
||||||
<button
|
<Button
|
||||||
key={period.value}
|
key={period.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPeriodChange?.(period.value);
|
onPeriodChange?.(period.value);
|
||||||
setIsDropdownOpen(false);
|
setIsDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
variant="ghost"
|
||||||
data.period === period.value
|
tone={data.period === period.value ? 'brand' : 'neutral'}
|
||||||
? 'text-brand-600 dark:text-brand-400 font-medium'
|
size="xs"
|
||||||
: 'text-gray-600 dark:text-gray-400'
|
fullWidth
|
||||||
}`}
|
className="justify-start"
|
||||||
>
|
>
|
||||||
{period.label}
|
{period.label}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
|
|
||||||
export interface AttentionItem {
|
export interface AttentionItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -80,9 +82,17 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
|||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
className="w-full flex items-center justify-between px-5 py-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-800 rounded-t-xl hover:bg-warning-100 dark:hover:bg-warning-900/30 transition-colors"
|
variant="ghost"
|
||||||
|
tone="warning"
|
||||||
|
fullWidth
|
||||||
|
className="flex items-center justify-between px-5 py-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-800 rounded-t-xl hover:bg-warning-100 dark:hover:bg-warning-900/30"
|
||||||
|
endIcon={isCollapsed ? (
|
||||||
|
<ChevronDownIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronUpIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<AlertIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
<AlertIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
||||||
@@ -90,12 +100,7 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
|||||||
Needs Attention ({items.length})
|
Needs Attention ({items.length})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isCollapsed ? (
|
</Button>
|
||||||
<ChevronDownIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronUpIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
@@ -127,12 +132,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
|||||||
{item.actionLabel} →
|
{item.actionLabel} →
|
||||||
</Link>
|
</Link>
|
||||||
) : item.onAction ? (
|
) : item.onAction ? (
|
||||||
<button
|
<Button
|
||||||
onClick={item.onAction}
|
onClick={item.onAction}
|
||||||
className="text-sm font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
variant="ghost"
|
||||||
|
tone="brand"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{item.actionLabel}
|
{item.actionLabel}
|
||||||
</button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{item.secondaryActionHref && (
|
{item.secondaryActionHref && (
|
||||||
<Link
|
<Link
|
||||||
@@ -145,12 +152,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<CloseIcon className="w-4 h-4" />}
|
||||||
onClick={() => onDismiss(item.id)}
|
onClick={() => onDismiss(item.id)}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
variant="ghost"
|
||||||
>
|
tone="neutral"
|
||||||
<CloseIcon className="w-4 h-4" />
|
size="sm"
|
||||||
</button>
|
aria-label="Dismiss"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useBillingStore } from '../../store/billingStore';
|
import { useBillingStore } from '../../store/billingStore';
|
||||||
import ComponentCard from '../common/ComponentCard';
|
import ComponentCard from '../common/ComponentCard';
|
||||||
|
import Select from '../form/Select';
|
||||||
|
|
||||||
export default function UsageChartWidget() {
|
export default function UsageChartWidget() {
|
||||||
const { usageSummary, loading, loadUsageSummary } = useBillingStore();
|
const { usageSummary, loading, loadUsageSummary } = useBillingStore();
|
||||||
@@ -40,15 +41,15 @@ export default function UsageChartWidget() {
|
|||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-end items-center mb-4">
|
<div className="flex justify-end items-center mb-4">
|
||||||
<select
|
<Select
|
||||||
className="h-9 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
|
options={[
|
||||||
value={dateRange}
|
{ value: 'week', label: 'Last 7 Days' },
|
||||||
onChange={(e) => setDateRange(e.target.value as 'week' | 'month' | 'year')}
|
{ value: 'month', label: 'This Month' },
|
||||||
>
|
{ value: 'year', label: 'This Year' },
|
||||||
<option value="week">Last 7 Days</option>
|
]}
|
||||||
<option value="month">This Month</option>
|
defaultValue={dateRange}
|
||||||
<option value="year">This Year</option>
|
onChange={(val) => setDateRange(val as 'week' | 'month' | 'year')}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Card } from '../ui/card/Card';
|
import { Card } from '../ui/card/Card';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
import { ChevronRightIcon } from '@heroicons/react/24/solid';
|
import { ChevronRightIcon } from '@heroicons/react/24/solid';
|
||||||
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
|
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
|
||||||
import { WORKFLOW_COLORS } from '../../config/colors.config';
|
import { WORKFLOW_COLORS } from '../../config/colors.config';
|
||||||
@@ -55,17 +56,15 @@ function TimeFilterButtons({ value, onChange }: TimeFilterProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
|
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<button
|
<Button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => onChange(option.value)}
|
onClick={() => onChange(option.value)}
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-md transition-all ${
|
variant={value === option.value ? 'primary' : 'ghost'}
|
||||||
value === option.value
|
tone={value === option.value ? 'brand' : 'neutral'}
|
||||||
? 'bg-brand-500 text-white shadow-sm'
|
size="xs"
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
|
|||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||||
import { MoreDotIcon } from "../../icons";
|
import { MoreDotIcon } from "../../icons";
|
||||||
import CountryMap from "./CountryMap";
|
import CountryMap from "./CountryMap";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
|
||||||
export default function DemographicCard() {
|
export default function DemographicCard() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -26,9 +27,9 @@ export default function DemographicCard() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<button className="dropdown-toggle" onClick={toggleDropdown}>
|
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
||||||
</button>
|
</IconButton>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={closeDropdown}
|
onClose={closeDropdown}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
|
|||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||||
import { MoreDotIcon } from "../../icons";
|
import { MoreDotIcon } from "../../icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
|
||||||
export default function MonthlySalesChart() {
|
export default function MonthlySalesChart() {
|
||||||
const options: ApexOptions = {
|
const options: ApexOptions = {
|
||||||
@@ -107,9 +108,9 @@ export default function MonthlySalesChart() {
|
|||||||
Monthly Sales
|
Monthly Sales
|
||||||
</h3>
|
</h3>
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<button className="dropdown-toggle" onClick={toggleDropdown}>
|
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
||||||
</button>
|
</IconButton>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={closeDropdown}
|
onClose={closeDropdown}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||||
import { MoreDotIcon } from "../../icons";
|
import { MoreDotIcon } from "../../icons";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
|
||||||
export default function MonthlyTarget() {
|
export default function MonthlyTarget() {
|
||||||
const series = [75.55];
|
const series = [75.55];
|
||||||
@@ -76,9 +77,9 @@ export default function MonthlyTarget() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<button className="dropdown-toggle" onClick={toggleDropdown}>
|
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
|
||||||
</button>
|
</IconButton>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={closeDropdown}
|
onClose={closeDropdown}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "../ui/table";
|
} from "../ui/table";
|
||||||
import Badge from "../ui/badge/Badge";
|
import Badge from "../ui/badge/Badge";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
|
||||||
// Define the TypeScript interface for the table rows
|
// Define the TypeScript interface for the table rows
|
||||||
interface Product {
|
interface Product {
|
||||||
@@ -79,7 +80,7 @@ export default function RecentOrders() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
<Button variant="outline" size="sm">
|
||||||
<svg
|
<svg
|
||||||
className="stroke-current fill-white dark:fill-gray-800"
|
className="stroke-current fill-white dark:fill-gray-800"
|
||||||
width="20"
|
width="20"
|
||||||
@@ -116,10 +117,10 @@ export default function RecentOrders() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Filter
|
Filter
|
||||||
</button>
|
</Button>
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
<Button variant="outline" size="sm">
|
||||||
See all
|
See all
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-full overflow-x-auto">
|
<div className="max-w-full overflow-x-auto">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Input from './input/InputField';
|
import Input from './input/InputField';
|
||||||
|
import TextArea from './input/TextArea';
|
||||||
import SelectDropdown from './SelectDropdown';
|
import SelectDropdown from './SelectDropdown';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
@@ -97,19 +98,13 @@ export default function FormFieldRenderer({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
) : field.type === 'textarea' ? (
|
) : field.type === 'textarea' ? (
|
||||||
<textarea
|
<TextArea
|
||||||
id={fieldId}
|
|
||||||
className={`w-full rounded-lg border ${
|
|
||||||
error
|
|
||||||
? 'border-error-500'
|
|
||||||
: 'border-gray-300 dark:border-gray-700'
|
|
||||||
} bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(field.key, e.target.value)}
|
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
required={field.required}
|
|
||||||
rows={field.rows || 4}
|
rows={field.rows || 4}
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => onChange(field.key, val)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
error={!!error}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ThemeToggleButton } from "../common/ThemeToggleButton";
|
|||||||
import NotificationDropdown from "./NotificationDropdown";
|
import NotificationDropdown from "./NotificationDropdown";
|
||||||
import UserDropdown from "./UserDropdown";
|
import UserDropdown from "./UserDropdown";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
import InputField from "../form/input/InputField";
|
||||||
|
|
||||||
// Define the interface for the props
|
// Define the interface for the props
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -20,63 +22,54 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
|
|||||||
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
|
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
|
||||||
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
|
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
|
||||||
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
|
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
|
||||||
<button
|
<IconButton
|
||||||
className="block w-10 h-10 text-gray-500 lg:hidden dark:text-gray-400"
|
icon={
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 16 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="Toggle menu"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
className="lg:hidden"
|
||||||
{/* Hamburger Icon */}
|
/>
|
||||||
<svg
|
<IconButton
|
||||||
className={`block`}
|
icon={
|
||||||
width="16"
|
<svg
|
||||||
height="12"
|
width="16"
|
||||||
viewBox="0 0 16 12"
|
height="12"
|
||||||
fill="none"
|
viewBox="0 0 16 12"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="none"
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path
|
>
|
||||||
fillRule="evenodd"
|
<path
|
||||||
clipRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
clipRule="evenodd"
|
||||||
fill="currentColor"
|
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||||
/>
|
fill="currentColor"
|
||||||
</svg>
|
/>
|
||||||
<svg
|
</svg>
|
||||||
className="hidden"
|
}
|
||||||
width="24"
|
variant="outline"
|
||||||
height="24"
|
tone="neutral"
|
||||||
viewBox="0 0 24 24"
|
size="sm"
|
||||||
fill="none"
|
title="Toggle sidebar"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/* Cross Icon */}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="items-center justify-center hidden w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 lg:flex dark:text-gray-400 lg:h-11 lg:w-11 lg:border"
|
className="hidden lg:flex"
|
||||||
>
|
/>
|
||||||
<svg
|
|
||||||
className="hidden fill-current lg:block"
|
|
||||||
width="16"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 16 12"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Link to="/" className="lg:hidden">
|
<Link to="/" className="lg:hidden">
|
||||||
<img
|
<img
|
||||||
@@ -91,30 +84,35 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
<IconButton
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="More options"
|
||||||
onClick={toggleApplicationMenu}
|
onClick={toggleApplicationMenu}
|
||||||
className="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
|
className="lg:hidden"
|
||||||
>
|
/>
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<form action="https://formbold.com/s/unique_form_id" method="POST">
|
<form action="https://formbold.com/s/unique_form_id" method="POST">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button className="absolute -translate-y-1/2 left-4 top-1/2">
|
<span className="absolute -translate-y-1/2 left-4 top-1/2 pointer-events-none">
|
||||||
<svg
|
<svg
|
||||||
className="fill-gray-500 dark:fill-gray-400"
|
className="fill-gray-500 dark:fill-gray-400"
|
||||||
width="20"
|
width="20"
|
||||||
@@ -130,17 +128,17 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
|
|||||||
fill=""
|
fill=""
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</span>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search or type command..."
|
placeholder="Search or type command..."
|
||||||
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
|
className="pl-12 pr-14 xl:w-[430px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
<span className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400 pointer-events-none">
|
||||||
<span> ⌘ </span>
|
<span> ⌘ </span>
|
||||||
<span> K </span>
|
<span> K </span>
|
||||||
</button>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
import {
|
import {
|
||||||
useNotificationStore,
|
useNotificationStore,
|
||||||
formatNotificationTime,
|
formatNotificationTime,
|
||||||
@@ -126,9 +128,10 @@ export default function NotificationDropdown() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<IconButton
|
||||||
ref={buttonRef}
|
ref={buttonRef as React.RefObject<HTMLButtonElement>}
|
||||||
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full dropdown-toggle hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
variant="outline"
|
||||||
|
className="relative dropdown-toggle h-11 w-11"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
||||||
>
|
>
|
||||||
@@ -153,7 +156,7 @@ export default function NotificationDropdown() {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</IconButton>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -174,16 +177,21 @@ export default function NotificationDropdown() {
|
|||||||
</h5>
|
</h5>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={markAllAsRead}
|
onClick={markAllAsRead}
|
||||||
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
variant="ghost"
|
||||||
|
tone="brand"
|
||||||
|
size="xs"
|
||||||
|
className="text-xs"
|
||||||
>
|
>
|
||||||
Mark all read
|
Mark all read
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
||||||
aria-label="Close notifications"
|
aria-label="Close notifications"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -200,7 +208,7 @@ export default function NotificationDropdown() {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { useState, useRef } from "react";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
import {
|
import {
|
||||||
useNotificationStore,
|
useNotificationStore,
|
||||||
formatNotificationTime,
|
formatNotificationTime,
|
||||||
@@ -97,34 +99,38 @@ export default function NotificationDropdown() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<IconButton
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full dropdown-toggle hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
icon={
|
||||||
|
<svg
|
||||||
|
className="fill-current"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
shape="circle"
|
||||||
|
className="relative dropdown-toggle h-11 w-11"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
||||||
>
|
/>
|
||||||
{/* Notification badge */}
|
{/* Notification badge */}
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-warning-500 text-[10px] font-semibold text-white">
|
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-warning-500 text-[10px] font-semibold text-white">
|
||||||
{unreadCount > 9 ? '9+' : unreadCount}
|
{unreadCount > 9 ? '9+' : unreadCount}
|
||||||
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
|
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<svg
|
|
||||||
className="fill-current"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -145,18 +151,23 @@ export default function NotificationDropdown() {
|
|||||||
</h5>
|
</h5>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<button
|
<Button
|
||||||
onClick={markAllAsRead}
|
onClick={markAllAsRead}
|
||||||
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
|
variant="ghost"
|
||||||
|
tone="brand"
|
||||||
|
size="xs"
|
||||||
|
className="text-xs"
|
||||||
>
|
>
|
||||||
Mark all read
|
Mark all read
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<IconButton
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
aria-label="Close notifications"
|
aria-label="Close notifications"
|
||||||
>
|
icon={
|
||||||
<svg
|
<svg
|
||||||
className="fill-current"
|
className="fill-current"
|
||||||
width="20"
|
width="20"
|
||||||
@@ -171,7 +182,8 @@ export default function NotificationDropdown() {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
|||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useAuthStore } from "../../store/authStore";
|
import { useAuthStore } from "../../store/authStore";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
|
||||||
export default function UserDropdown() {
|
export default function UserDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -26,9 +27,11 @@ export default function UserDropdown() {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<Button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
className="flex items-center text-gray-700 dropdown-toggle dark:text-gray-400"
|
className="flex items-center text-gray-700 dropdown-toggle dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<span className="mr-3 overflow-hidden rounded-full h-11 w-11 bg-brand-500 flex items-center justify-center">
|
<span className="mr-3 overflow-hidden rounded-full h-11 w-11 bg-brand-500 flex items-center justify-center">
|
||||||
@@ -62,7 +65,7 @@ export default function UserDropdown() {
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -162,8 +165,10 @@ export default function UserDropdown() {
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<Button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
|
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -182,7 +187,7 @@ export default function UserDropdown() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Sign out
|
Sign out
|
||||||
</button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
|
import InputField from '../form/input/InputField';
|
||||||
import {
|
import {
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
@@ -297,12 +298,10 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
|
|||||||
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
|
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
Site Name
|
Site Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
|
||||||
value={siteName}
|
value={siteName}
|
||||||
onChange={(e) => setSiteName(e.target.value)}
|
onChange={(e) => setSiteName(e.target.value)}
|
||||||
placeholder="Enter site name"
|
placeholder="Enter site name"
|
||||||
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-brand-500 focus:ring-2 focus:ring-brand-200 dark:focus:ring-brand-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -311,12 +310,10 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
|
|||||||
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
|
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
Website Address
|
Website Address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
|
||||||
value={websiteAddress}
|
value={websiteAddress}
|
||||||
onChange={(e) => setWebsiteAddress(e.target.value)}
|
onChange={(e) => setWebsiteAddress(e.target.value)}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-brand-500 focus:ring-2 focus:ring-brand-200 dark:focus:ring-brand-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -93,18 +93,17 @@ export default function Step1Welcome({ onNext, onSkip }: Step1WelcomeProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
className="text-gray-500"
|
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="gap-2"
|
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Let's Get Started
|
Let's Get Started
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -278,20 +278,19 @@ export default function Step2AddSite({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="gap-2"
|
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleCreateSite}
|
onClick={handleCreateSite}
|
||||||
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
|
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
|
||||||
className="gap-2"
|
endIcon={!isCreating ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
||||||
>
|
>
|
||||||
{isCreating ? 'Creating...' : 'Create Site'}
|
{isCreating ? 'Creating...' : 'Create Site'}
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -245,24 +245,13 @@ export default function Step3ConnectIntegration({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={isTesting}
|
disabled={isTesting}
|
||||||
className="gap-1"
|
startIcon={
|
||||||
|
isTesting ? <TimeIcon className="w-4 h-4 animate-spin" /> :
|
||||||
|
testResult === 'success' ? <CheckCircleIcon className="w-4 h-4" /> :
|
||||||
|
<TimeIcon className="w-4 h-4" />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isTesting ? (
|
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
|
||||||
<>
|
|
||||||
<TimeIcon className="w-4 h-4 animate-spin" />
|
|
||||||
Testing...
|
|
||||||
</>
|
|
||||||
) : testResult === 'success' ? (
|
|
||||||
<>
|
|
||||||
<CheckCircleIcon className="w-4 h-4" />
|
|
||||||
Connected
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TimeIcon className="w-4 h-4" />
|
|
||||||
Test Connection
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -278,15 +267,16 @@ export default function Step3ConnectIntegration({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="gap-2"
|
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
@@ -294,10 +284,9 @@ export default function Step3ConnectIntegration({
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="gap-2"
|
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
|
import IconButton from '../../ui/button/IconButton';
|
||||||
|
import InputField from '../../form/input/InputField';
|
||||||
import { Card } from '../../ui/card';
|
import { Card } from '../../ui/card';
|
||||||
import Badge from '../../ui/badge/Badge';
|
import Badge from '../../ui/badge/Badge';
|
||||||
import Alert from '../../ui/alert/Alert';
|
import Alert from '../../ui/alert/Alert';
|
||||||
@@ -158,14 +160,11 @@ export default function Step4AddKeywords({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
placeholder="Enter a keyword..."
|
placeholder="Enter a keyword..."
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -199,12 +198,14 @@ export default function Step4AddKeywords({
|
|||||||
className="gap-1 pr-1"
|
className="gap-1 pr-1"
|
||||||
>
|
>
|
||||||
{keyword}
|
{keyword}
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<CloseIcon className="w-3 h-3" />}
|
||||||
onClick={() => handleRemoveKeyword(keyword)}
|
onClick={() => handleRemoveKeyword(keyword)}
|
||||||
className="ml-1 p-0.5 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
|
variant="ghost"
|
||||||
>
|
size="xs"
|
||||||
<CloseIcon className="w-3 h-3" />
|
className="ml-1"
|
||||||
</button>
|
aria-label="Remove keyword"
|
||||||
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,12 +215,14 @@ export default function Step4AddKeywords({
|
|||||||
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
|
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
|
||||||
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
|
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
|
||||||
{keywords.length > 0 && (
|
{keywords.length > 0 && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => setKeywords([])}
|
onClick={() => setKeywords([])}
|
||||||
className="text-error-500 hover:text-error-600"
|
className="text-error-500 hover:text-error-600"
|
||||||
>
|
>
|
||||||
Clear all
|
Clear all
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,15 +254,16 @@ export default function Step4AddKeywords({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="gap-2"
|
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
@@ -268,10 +272,9 @@ export default function Step4AddKeywords({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleSubmitKeywords}
|
onClick={handleSubmitKeywords}
|
||||||
disabled={isAdding || keywords.length === 0}
|
disabled={isAdding || keywords.length === 0}
|
||||||
className="gap-2"
|
endIcon={!isAdding ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
||||||
>
|
>
|
||||||
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
|
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,10 +157,10 @@ export default function Step5Complete({
|
|||||||
size="lg"
|
size="lg"
|
||||||
onClick={onComplete}
|
onClick={onComplete}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-2 w-full"
|
fullWidth
|
||||||
|
endIcon={!isLoading ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Loading...' : 'Go to Dashboard'}
|
{isLoading ? 'Loading...' : 'Go to Dashboard'}
|
||||||
<ArrowRightIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import './blocks.css';
|
import './blocks.css';
|
||||||
|
import Button from '../../ui/button/Button';
|
||||||
|
|
||||||
export interface CTABlockProps {
|
export interface CTABlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -44,13 +45,12 @@ export function CTABlock({
|
|||||||
{primaryCtaLabel}
|
{primaryCtaLabel}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="primary"
|
||||||
className="shared-button shared-button--primary"
|
|
||||||
onClick={onPrimaryCtaClick}
|
onClick={onPrimaryCtaClick}
|
||||||
>
|
>
|
||||||
{primaryCtaLabel}
|
{primaryCtaLabel}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{secondaryCtaLabel && (
|
{secondaryCtaLabel && (
|
||||||
@@ -59,13 +59,12 @@ export function CTABlock({
|
|||||||
{secondaryCtaLabel}
|
{secondaryCtaLabel}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="secondary"
|
||||||
className="shared-button shared-button--secondary"
|
|
||||||
onClick={onSecondaryCtaClick}
|
onClick={onSecondaryCtaClick}
|
||||||
>
|
>
|
||||||
{secondaryCtaLabel}
|
{secondaryCtaLabel}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import './blocks.css';
|
import './blocks.css';
|
||||||
|
import InputField from '../../form/input/InputField';
|
||||||
|
import TextArea from '../../form/input/TextArea';
|
||||||
|
import Button from '../../ui/button/Button';
|
||||||
|
|
||||||
export interface ContactFormBlockProps {
|
export interface ContactFormBlockProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -49,33 +52,25 @@ export function ContactFormBlock({
|
|||||||
{field.required && <span className="shared-contact-form__required">*</span>}
|
{field.required && <span className="shared-contact-form__required">*</span>}
|
||||||
</label>
|
</label>
|
||||||
{field.type === 'textarea' ? (
|
{field.type === 'textarea' ? (
|
||||||
<textarea
|
<TextArea
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
required={field.required}
|
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
value={formData[field.name] || ''}
|
value={formData[field.name] || ''}
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
onChange={(val) => handleChange(field.name, val)}
|
||||||
className="shared-contact-form__input"
|
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<InputField
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type={field.type}
|
type={field.type}
|
||||||
required={field.required}
|
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
value={formData[field.name] || ''}
|
value={formData[field.name] || ''}
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||||
className="shared-contact-form__input"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button type="submit" className="shared-button shared-button--primary">
|
<Button type="submit" variant="primary">
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import './blocks.css';
|
import './blocks.css';
|
||||||
|
import Button from '../../ui/button/Button';
|
||||||
|
|
||||||
export interface HeroBlockProps {
|
export interface HeroBlockProps {
|
||||||
eyebrow?: string;
|
eyebrow?: string;
|
||||||
@@ -18,9 +19,9 @@ export function HeroBlock({ eyebrow, title, subtitle, ctaLabel, onCtaClick, supp
|
|||||||
{subtitle && <p className="shared-hero__subtitle">{subtitle}</p>}
|
{subtitle && <p className="shared-hero__subtitle">{subtitle}</p>}
|
||||||
{supportingContent && <div className="shared-hero__support">{supportingContent}</div>}
|
{supportingContent && <div className="shared-hero__support">{supportingContent}</div>}
|
||||||
{ctaLabel && (
|
{ctaLabel && (
|
||||||
<button type="button" className="shared-button" onClick={onCtaClick}>
|
<Button variant="primary" onClick={onCtaClick}>
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import './blocks.css';
|
import './blocks.css';
|
||||||
|
import Button from '../../ui/button/Button';
|
||||||
|
|
||||||
export interface ProductItem {
|
export interface ProductItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -41,13 +42,12 @@ export function ProductsBlock({
|
|||||||
)}
|
)}
|
||||||
{product.price && <p className="shared-products__price">{product.price}</p>}
|
{product.price && <p className="shared-products__price">{product.price}</p>}
|
||||||
{product.ctaLabel && (
|
{product.ctaLabel && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="primary"
|
||||||
className="shared-button"
|
|
||||||
onClick={product.onCtaClick}
|
onClick={product.onCtaClick}
|
||||||
>
|
>
|
||||||
{product.ctaLabel}
|
{product.ctaLabel}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
// import { fetchSiteProgress, SiteProgress } from '../../services/api';
|
// import { fetchSiteProgress, SiteProgress } from '../../services/api';
|
||||||
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from '../../icons';
|
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from '../../icons';
|
||||||
|
|
||||||
@@ -326,7 +327,7 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
|||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<AlertCircleIcon className="w-4 h-4 text-warning-600 dark:text-warning-400 mt-0.5 flex-shrink-0" />
|
<AlertCircleIcon className="w-4 h-4 text-warning-600 dark:text-warning-400 mt-0.5 flex-shrink-0" />
|
||||||
<div className="text-xs text-warning-800 dark:text-warning-300">
|
<div className="text-xs text-warning-800 dark:text-warning-300">
|
||||||
Some data may be outdated. <button onClick={handleRetry} className="underline font-medium">Refresh</button>
|
Some data may be outdated. <Button variant="ghost" size="xs" onClick={handleRetry} className="underline font-medium p-0 h-auto">Refresh</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
|
|||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
import Label from '../../form/Label';
|
import Label from '../../form/Label';
|
||||||
import TextArea from '../../form/input/TextArea';
|
import TextArea from '../../form/input/TextArea';
|
||||||
|
import InputField from '../../form/input/InputField';
|
||||||
import { useToast } from '../../ui/toast/ToastContainer';
|
import { useToast } from '../../ui/toast/ToastContainer';
|
||||||
|
|
||||||
export interface StyleSettings {
|
export interface StyleSettings {
|
||||||
@@ -106,53 +107,41 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'css' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('css')}
|
onClick={() => setActiveTab('css')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'css'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<CodeIcon className="w-4 h-4 inline mr-2" />
|
<CodeIcon className="w-4 h-4 mr-2" />
|
||||||
Custom CSS
|
Custom CSS
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('colors')}
|
onClick={() => setActiveTab('colors')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'colors'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<PaletteIcon className="w-4 h-4 inline mr-2" />
|
<PaletteIcon className="w-4 h-4 mr-2" />
|
||||||
Colors
|
Colors
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('typography')}
|
onClick={() => setActiveTab('typography')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'typography'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<TypeIcon className="w-4 h-4 inline mr-2" />
|
<TypeIcon className="w-4 h-4 mr-2" />
|
||||||
Typography
|
Typography
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('spacing')}
|
onClick={() => setActiveTab('spacing')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'spacing'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Spacing
|
Spacing
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,18 +174,18 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
|||||||
<div key={colorKey}>
|
<div key={colorKey}>
|
||||||
<Label>{colorKey.charAt(0).toUpperCase() + colorKey.slice(1)} Color</Label>
|
<Label>{colorKey.charAt(0).toUpperCase() + colorKey.slice(1)} Color</Label>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
<input
|
<InputField
|
||||||
type="color"
|
type="color"
|
||||||
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || '#000000'}
|
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || '#000000'}
|
||||||
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
||||||
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
|
className="w-16 h-10"
|
||||||
/>
|
/>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || ''}
|
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || ''}
|
||||||
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,45 +200,45 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Font Family</Label>
|
<Label>Font Family</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.typography?.fontFamily || ''}
|
value={styleSettings.typography?.fontFamily || ''}
|
||||||
onChange={(e) => handleTypographyChange('fontFamily', e.target.value)}
|
onChange={(e) => handleTypographyChange('fontFamily', e.target.value)}
|
||||||
placeholder="Arial, sans-serif"
|
placeholder="Arial, sans-serif"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Heading Font</Label>
|
<Label>Heading Font</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.typography?.headingFont || ''}
|
value={styleSettings.typography?.headingFont || ''}
|
||||||
onChange={(e) => handleTypographyChange('headingFont', e.target.value)}
|
onChange={(e) => handleTypographyChange('headingFont', e.target.value)}
|
||||||
placeholder="Georgia, serif"
|
placeholder="Georgia, serif"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Base Font Size</Label>
|
<Label>Base Font Size</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.typography?.fontSize || ''}
|
value={styleSettings.typography?.fontSize || ''}
|
||||||
onChange={(e) => handleTypographyChange('fontSize', e.target.value)}
|
onChange={(e) => handleTypographyChange('fontSize', e.target.value)}
|
||||||
placeholder="16px"
|
placeholder="16px"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Line Height</Label>
|
<Label>Line Height</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.typography?.lineHeight || ''}
|
value={styleSettings.typography?.lineHeight || ''}
|
||||||
onChange={(e) => handleTypographyChange('lineHeight', e.target.value)}
|
onChange={(e) => handleTypographyChange('lineHeight', e.target.value)}
|
||||||
placeholder="1.5"
|
placeholder="1.5"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,12 +251,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Base Spacing Unit</Label>
|
<Label>Base Spacing Unit</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.spacing?.base || ''}
|
value={styleSettings.spacing?.base || ''}
|
||||||
onChange={(e) => handleSpacingChange('base', e.target.value)}
|
onChange={(e) => handleSpacingChange('base', e.target.value)}
|
||||||
placeholder="8px"
|
placeholder="8px"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Base unit for spacing calculations (e.g., 8px, 1rem)
|
Base unit for spacing calculations (e.g., 8px, 1rem)
|
||||||
@@ -276,12 +265,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Spacing Scale</Label>
|
<Label>Spacing Scale</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={styleSettings.spacing?.scale || ''}
|
value={styleSettings.spacing?.scale || ''}
|
||||||
onChange={(e) => handleSpacingChange('scale', e.target.value)}
|
onChange={(e) => handleSpacingChange('scale', e.target.value)}
|
||||||
placeholder="1.5"
|
placeholder="1.5"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Multiplier for spacing scale (e.g., 1.5 for 1.5x spacing)
|
Multiplier for spacing scale (e.g., 1.5 for 1.5x spacing)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
|
|||||||
import Label from '../../form/Label';
|
import Label from '../../form/Label';
|
||||||
import SelectDropdown from '../../form/SelectDropdown';
|
import SelectDropdown from '../../form/SelectDropdown';
|
||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
|
import InputField from '../../form/input/InputField';
|
||||||
|
|
||||||
export interface TemplateCustomization {
|
export interface TemplateCustomization {
|
||||||
layout: string;
|
layout: string;
|
||||||
@@ -68,54 +69,42 @@ export default function TemplateCustomizer({
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'layout' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('layout')}
|
onClick={() => setActiveTab('layout')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'layout'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<LayoutIcon className="w-4 h-4 inline mr-2" />
|
<LayoutIcon className="w-4 h-4 mr-2" />
|
||||||
Layout
|
Layout
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('colors')}
|
onClick={() => setActiveTab('colors')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'colors'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<PaletteIcon className="w-4 h-4 inline mr-2" />
|
<PaletteIcon className="w-4 h-4 mr-2" />
|
||||||
Colors
|
Colors
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('typography')}
|
onClick={() => setActiveTab('typography')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'typography'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<TypeIcon className="w-4 h-4 inline mr-2" />
|
<TypeIcon className="w-4 h-4 mr-2" />
|
||||||
Typography
|
Typography
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('spacing')}
|
onClick={() => setActiveTab('spacing')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'spacing'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-4 h-4 inline mr-2" />
|
<SettingsIcon className="w-4 h-4 mr-2" />
|
||||||
Spacing
|
Spacing
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,7 +192,7 @@ export default function TemplateCustomizer({
|
|||||||
<div>
|
<div>
|
||||||
<Label>Primary Color</Label>
|
<Label>Primary Color</Label>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
<input
|
<InputField
|
||||||
type="color"
|
type="color"
|
||||||
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -211,9 +200,9 @@ export default function TemplateCustomizer({
|
|||||||
customStyles: { ...customization.customStyles, primaryColor: e.target.value },
|
customStyles: { ...customization.customStyles, primaryColor: e.target.value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
|
className="w-16 h-10"
|
||||||
/>
|
/>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -222,7 +211,7 @@ export default function TemplateCustomizer({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="#3b82f6"
|
placeholder="#3b82f6"
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +219,7 @@ export default function TemplateCustomizer({
|
|||||||
<div>
|
<div>
|
||||||
<Label>Background Color</Label>
|
<Label>Background Color</Label>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
<input
|
<InputField
|
||||||
type="color"
|
type="color"
|
||||||
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -238,9 +227,9 @@ export default function TemplateCustomizer({
|
|||||||
customStyles: { ...customization.customStyles, backgroundColor: e.target.value },
|
customStyles: { ...customization.customStyles, backgroundColor: e.target.value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
|
className="w-16 h-10"
|
||||||
/>
|
/>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -249,7 +238,7 @@ export default function TemplateCustomizer({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="#ffffff"
|
placeholder="#ffffff"
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
|||||||
import { SearchIcon, FilterIcon, CheckIcon } from '../../icons';
|
import { SearchIcon, FilterIcon, CheckIcon } from '../../icons';
|
||||||
import { Card } from '../../ui/card';
|
import { Card } from '../../ui/card';
|
||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
|
import InputField from '../../form/input/InputField';
|
||||||
|
|
||||||
export interface TemplateOption {
|
export interface TemplateOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -106,47 +107,37 @@ export default function TemplateLibrary({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="flex-1">
|
||||||
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<InputField
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search templates..."
|
placeholder="Search templates..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
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"
|
startIcon={<SearchIcon className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={showFeaturedOnly ? 'primary' : 'outline'}
|
||||||
onClick={() => setShowFeaturedOnly(!showFeaturedOnly)}
|
onClick={() => setShowFeaturedOnly(!showFeaturedOnly)}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
startIcon={<FilterIcon className="w-4 h-4" />}
|
||||||
showFeaturedOnly
|
|
||||||
? 'bg-brand-500 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FilterIcon className="w-4 h-4 inline mr-2" />
|
|
||||||
Featured Only
|
Featured Only
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<button
|
<Button
|
||||||
key={category}
|
key={category}
|
||||||
type="button"
|
variant={selectedCategory === category ? 'primary' : 'outline'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
selectedCategory === category
|
|
||||||
? 'bg-brand-500 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
|
import IconButton from '../ui/button/IconButton';
|
||||||
import Label from '../form/Label';
|
import Label from '../form/Label';
|
||||||
import Input from '../form/input/InputField';
|
import Input from '../form/input/InputField';
|
||||||
import Checkbox from '../form/input/Checkbox';
|
import Checkbox from '../form/input/Checkbox';
|
||||||
|
import Switch from '../form/switch/Switch';
|
||||||
import { useToast } from '../ui/toast/ToastContainer';
|
import { useToast } from '../ui/toast/ToastContainer';
|
||||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
@@ -218,26 +220,11 @@ export default function WordPressIntegrationForm({
|
|||||||
|
|
||||||
{/* Toggle Switch */}
|
{/* Toggle Switch */}
|
||||||
{apiKey && (
|
{apiKey && (
|
||||||
<div className="flex items-center gap-3">
|
<Switch
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
label={integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
|
||||||
{integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
|
checked={integrationEnabled}
|
||||||
</span>
|
onChange={(checked) => handleToggleIntegration(checked)}
|
||||||
<button
|
/>
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleIntegration(!integrationEnabled)}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 ${
|
|
||||||
integrationEnabled ? 'bg-brand-600' : 'bg-gray-300 dark:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
role="switch"
|
|
||||||
aria-checked={integrationEnabled}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
||||||
integrationEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -293,30 +280,32 @@ export default function WordPressIntegrationForm({
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<Input
|
||||||
id="api-key"
|
className="w-full min-w-[360px] pr-[90px] font-mono"
|
||||||
className="dark:bg-dark-900 shadow-theme-xs focus:border-brand-300 focus:ring-brand-500/10 dark:focus:border-brand-800 h-11 w-full min-w-[360px] rounded-lg border border-gray-300 bg-transparent py-3 pr-[90px] pl-4 text-sm text-gray-800 placeholder:text-gray-400 focus:ring-3 focus:outline-hidden dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 font-mono"
|
|
||||||
readOnly
|
readOnly
|
||||||
type={apiKeyVisible ? 'text' : 'password'}
|
type={apiKeyVisible ? 'text' : 'password'}
|
||||||
value={apiKeyVisible ? apiKey : maskApiKey(apiKey)}
|
value={apiKeyVisible ? apiKey : maskApiKey(apiKey)}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleCopyApiKey}
|
onClick={handleCopyApiKey}
|
||||||
className="absolute top-1/2 right-0 inline-flex h-11 -translate-y-1/2 cursor-pointer items-center gap-1 rounded-r-lg border border-gray-300 py-3 pr-3 pl-3.5 text-sm font-medium text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
variant="outline"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
className="absolute top-1/2 right-0 -translate-y-1/2 rounded-l-none"
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4 fill-current" />
|
<CopyIcon className="w-4 h-4" />
|
||||||
<span>Copy</span>
|
Copy
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="group relative inline-block">
|
<div className="group relative inline-block">
|
||||||
<button
|
<IconButton
|
||||||
onClick={handleRegenerateApiKey}
|
onClick={handleRegenerateApiKey}
|
||||||
disabled={generatingKey}
|
disabled={generatingKey}
|
||||||
className="inline-flex h-11 w-11 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
|
variant="outline"
|
||||||
title="Regenerate"
|
title="Regenerate"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</IconButton>
|
||||||
<div className="invisible absolute bottom-full left-1/2 z-50 mb-2.5 -translate-x-1/2 opacity-0 transition-opacity duration-300 group-hover:visible group-hover:opacity-100">
|
<div className="invisible absolute bottom-full left-1/2 z-50 mb-2.5 -translate-x-1/2 opacity-0 transition-opacity duration-300 group-hover:visible group-hover:opacity-100">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium whitespace-nowrap text-gray-700 shadow-xs dark:bg-gray-800 dark:text-white">
|
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium whitespace-nowrap text-gray-700 shadow-xs dark:bg-gray-800 dark:text-white">
|
||||||
@@ -326,9 +315,9 @@ export default function WordPressIntegrationForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<IconButton
|
||||||
onClick={() => setApiKeyVisible(!apiKeyVisible)}
|
onClick={() => setApiKeyVisible(!apiKeyVisible)}
|
||||||
className="inline-flex h-11 w-11 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{apiKeyVisible ? (
|
{apiKeyVisible ? (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -340,7 +329,7 @@ export default function WordPressIntegrationForm({
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -354,22 +343,23 @@ export default function WordPressIntegrationForm({
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 whitespace-nowrap">
|
<td className="px-5 py-3 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<IconButton
|
||||||
onClick={handleRegenerateApiKey}
|
onClick={handleRegenerateApiKey}
|
||||||
disabled={generatingKey}
|
disabled={generatingKey}
|
||||||
className="text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-400 disabled:opacity-50 transition-colors"
|
variant="ghost"
|
||||||
title="Regenerate API key"
|
title="Regenerate API key"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</IconButton>
|
||||||
<button
|
<IconButton
|
||||||
onClick={handleRevokeApiKey}
|
onClick={handleRevokeApiKey}
|
||||||
disabled={generatingKey}
|
disabled={generatingKey}
|
||||||
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-400 disabled:opacity-50 transition-colors"
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
title="Revoke API key"
|
title="Revoke API key"
|
||||||
>
|
>
|
||||||
<TrashBinIcon className="w-5 h-5" />
|
<TrashBinIcon className="w-5 h-5" />
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { PlusIcon, HorizontaLDots } from "../../icons";
|
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
|
||||||
import RelationshipMap from "./RelationshipMap";
|
import RelationshipMap from "./RelationshipMap";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -103,13 +105,12 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
|
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
|
||||||
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
|
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
|
||||||
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("All")}
|
onClick={() => handleFilterChange("All")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "All"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
All Tasks
|
All Tasks
|
||||||
<span
|
<span
|
||||||
@@ -121,15 +122,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
>
|
>
|
||||||
{tasks.length}
|
{tasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("Todo")}
|
onClick={() => handleFilterChange("Todo")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "Todo"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
To do
|
To do
|
||||||
<span
|
<span
|
||||||
@@ -141,15 +141,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
>
|
>
|
||||||
{todoTasks.length}
|
{todoTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("InProgress")}
|
onClick={() => handleFilterChange("InProgress")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "InProgress"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
In Progress
|
In Progress
|
||||||
<span
|
<span
|
||||||
@@ -161,15 +160,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
>
|
>
|
||||||
{inProgressTasks.length}
|
{inProgressTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("Completed")}
|
onClick={() => handleFilterChange("Completed")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "Completed"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Completed
|
Completed
|
||||||
<span
|
<span
|
||||||
@@ -181,24 +179,28 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
|||||||
>
|
>
|
||||||
{completedTasks.length}
|
{completedTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
|
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
|
<Button
|
||||||
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
variant="outline"
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
|
tone="neutral"
|
||||||
</svg>
|
size="sm"
|
||||||
|
startIcon={<FilterIcon className="fill-current" />}
|
||||||
|
>
|
||||||
Filter & Sort
|
Filter & Sort
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={onAddTask}
|
onClick={onAddTask}
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
|
endIcon={<PlusIcon className="fill-current" />}
|
||||||
>
|
>
|
||||||
Add New Task
|
Add New Task
|
||||||
<PlusIcon className="fill-current" width={20} height={20} />
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,12 +326,14 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<HorizontaLDots className="fill-current" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="More options"
|
||||||
onClick={() => onDropdownToggle(status)}
|
onClick={() => onDropdownToggle(status)}
|
||||||
className="text-gray-700 dark:text-gray-400"
|
/>
|
||||||
>
|
|
||||||
<HorizontaLDots className="fill-current" width={24} height={24} />
|
|
||||||
</button>
|
|
||||||
{openDropdown && (
|
{openDropdown && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -337,15 +341,15 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
|||||||
onClick={() => onDropdownClose(status)}
|
onClick={() => onDropdownClose(status)}
|
||||||
/>
|
/>
|
||||||
<div className="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800">
|
<div className="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800">
|
||||||
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</Button>
|
||||||
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ArrowRightIcon } from "../../icons";
|
import { ArrowRightIcon } from "../../icons";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
|
||||||
export interface RelationshipData {
|
export interface RelationshipData {
|
||||||
taskId: number;
|
taskId: number;
|
||||||
@@ -43,13 +44,15 @@ const RelationshipMap: React.FC<RelationshipMapProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
|
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => onNavigate?.("keyword", task.keywordIds![idx])}
|
onClick={() => onNavigate?.("keyword", task.keywordIds![idx])}
|
||||||
className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{keyword}
|
{keyword}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{task.keywordIds.length > 3 && (
|
{task.keywordIds.length > 3 && (
|
||||||
<span className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
<span className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { PlusIcon, HorizontaLDots } from "../../icons";
|
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
|
||||||
import { Task } from "./KanbanBoard";
|
import { Task } from "./KanbanBoard";
|
||||||
|
import Button from "../ui/button/Button";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
import Checkbox from "../form/input/Checkbox";
|
||||||
|
|
||||||
interface TaskListProps {
|
interface TaskListProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@@ -66,13 +69,12 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
|
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
|
||||||
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
|
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
|
||||||
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("All")}
|
onClick={() => handleFilterChange("All")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "All"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
All Tasks
|
All Tasks
|
||||||
<span
|
<span
|
||||||
@@ -84,15 +86,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
>
|
>
|
||||||
{tasks.length}
|
{tasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("Todo")}
|
onClick={() => handleFilterChange("Todo")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "Todo"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
To do
|
To do
|
||||||
<span
|
<span
|
||||||
@@ -104,15 +105,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
>
|
>
|
||||||
{todoTasks.length}
|
{todoTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("InProgress")}
|
onClick={() => handleFilterChange("InProgress")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "InProgress"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
In Progress
|
In Progress
|
||||||
<span
|
<span
|
||||||
@@ -124,15 +124,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
>
|
>
|
||||||
{inProgressTasks.length}
|
{inProgressTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleFilterChange("Completed")}
|
onClick={() => handleFilterChange("Completed")}
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
|
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||||
selectedFilter === "Completed"
|
|
||||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Completed
|
Completed
|
||||||
<span
|
<span
|
||||||
@@ -144,24 +143,28 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||||||
>
|
>
|
||||||
{completedTasks.length}
|
{completedTasks.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
|
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
|
<Button
|
||||||
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
variant="outline"
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
|
tone="neutral"
|
||||||
</svg>
|
size="sm"
|
||||||
|
startIcon={<FilterIcon className="fill-current" />}
|
||||||
|
>
|
||||||
Filter & Sort
|
Filter & Sort
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={onAddTask}
|
onClick={onAddTask}
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
|
endIcon={<PlusIcon className="fill-current" />}
|
||||||
>
|
>
|
||||||
Add New Task
|
Add New Task
|
||||||
<PlusIcon className="fill-current" width={20} height={20} />
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,12 +260,14 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<HorizontaLDots className="fill-current" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
|
title="More options"
|
||||||
onClick={() => onDropdownToggle(title.toLowerCase().replace(" ", "_"))}
|
onClick={() => onDropdownToggle(title.toLowerCase().replace(" ", "_"))}
|
||||||
className="text-gray-700 dark:text-gray-400"
|
/>
|
||||||
>
|
|
||||||
<HorizontaLDots className="fill-current" width={24} height={24} />
|
|
||||||
</button>
|
|
||||||
{openDropdown && (
|
{openDropdown && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -270,15 +275,15 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
|
|||||||
onClick={() => onDropdownClose(title.toLowerCase().replace(" ", "_"))}
|
onClick={() => onDropdownClose(title.toLowerCase().replace(" ", "_"))}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-0 top-full z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 shadow-theme-md dark:border-gray-800 dark:bg-gray-dark">
|
<div className="absolute right-0 top-full z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 shadow-theme-md dark:border-gray-800 dark:bg-gray-dark">
|
||||||
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</Button>
|
||||||
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
|
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -334,27 +339,18 @@ const TaskListItem: React.FC<TaskListItemProps> = ({ task, checked, onClick, onC
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<label htmlFor={`taskCheckbox-${task.id}`} className="w-full cursor-pointer">
|
<div className="w-full flex items-start">
|
||||||
<div className="relative flex items-start">
|
<div className="mr-3">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id={`taskCheckbox-${task.id}`}
|
id={`taskCheckbox-${task.id}`}
|
||||||
className="sr-only taskCheckbox"
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => onCheckboxChange(e.target.checked)}
|
onChange={onCheckboxChange}
|
||||||
/>
|
/>
|
||||||
<div className={`flex items-center justify-center w-full h-5 mr-3 border border-gray-300 rounded-md box max-w-5 dark:border-gray-700 ${checked ? "bg-brand-500 border-brand-500" : ""}`}>
|
|
||||||
<span className={checked ? "opacity-100" : "opacity-0"}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11.6668 3.5L5.25016 9.91667L2.3335 7" stroke="white" strokeWidth="1.94437" strokeLinecap="round" strokeLinejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="-mt-0.5 text-base text-gray-800 dark:text-white/90" onClick={onClick}>
|
|
||||||
{task.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
<p className="-mt-0.5 text-base text-gray-800 dark:text-white/90 cursor-pointer" onClick={onClick}>
|
||||||
|
{task.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col-reverse items-start justify-end w-full gap-3 xl:flex-row xl:items-center xl:gap-5">
|
<div className="flex flex-col-reverse items-start justify-end w-full gap-3 xl:flex-row xl:items-center xl:gap-5">
|
||||||
|
|||||||
145
frontend/src/components/ui/button/IconButton.tsx
Normal file
145
frontend/src/components/ui/button/IconButton.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* IconButton Component
|
||||||
|
*
|
||||||
|
* 🔒 STYLE LOCKED - See DESIGN_SYSTEM.md for available variants and sizes.
|
||||||
|
* Icon-only button component for actions that only need an icon.
|
||||||
|
*/
|
||||||
|
import { ReactNode, forwardRef } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type IconButtonSize = "xs" | "sm" | "md" | "lg";
|
||||||
|
type IconButtonVariant = "solid" | "outline" | "ghost";
|
||||||
|
type IconButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
|
||||||
|
type IconButtonShape = "rounded" | "circle";
|
||||||
|
|
||||||
|
interface IconButtonProps {
|
||||||
|
icon: ReactNode;
|
||||||
|
size?: IconButtonSize;
|
||||||
|
variant?: IconButtonVariant;
|
||||||
|
tone?: IconButtonTone;
|
||||||
|
shape?: IconButtonShape;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
title?: string;
|
||||||
|
"aria-label"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toneMap: Record<
|
||||||
|
IconButtonTone,
|
||||||
|
{
|
||||||
|
solid: string;
|
||||||
|
outline: string;
|
||||||
|
ghost: string;
|
||||||
|
ring: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
brand: {
|
||||||
|
solid: "bg-brand-500 text-white hover:bg-brand-600",
|
||||||
|
outline: "text-brand-600 ring-1 ring-brand-200 hover:bg-brand-50 dark:ring-brand-500/40 dark:text-brand-300 dark:hover:bg-brand-500/10",
|
||||||
|
ghost: "text-brand-600 hover:bg-brand-50 dark:text-brand-300 dark:hover:bg-brand-500/10",
|
||||||
|
ring: "focus-visible:ring-brand-500",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
solid: "bg-success-500 text-white hover:bg-success-600",
|
||||||
|
outline: "text-success-600 ring-1 ring-success-200 hover:bg-success-50 dark:ring-success-500/40 dark:text-success-300 dark:hover:bg-success-500/10",
|
||||||
|
ghost: "text-success-600 hover:bg-success-50 dark:text-success-300 dark:hover:bg-success-500/10",
|
||||||
|
ring: "focus-visible:ring-success-500",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
solid: "bg-warning-500 text-white hover:bg-warning-600",
|
||||||
|
outline: "text-warning-600 ring-1 ring-warning-200 hover:bg-warning-50 dark:ring-warning-500/40 dark:text-warning-300 dark:hover:bg-warning-500/10",
|
||||||
|
ghost: "text-warning-600 hover:bg-warning-50 dark:text-warning-300 dark:hover:bg-warning-500/10",
|
||||||
|
ring: "focus-visible:ring-warning-500",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
solid: "bg-error-500 text-white hover:bg-error-600",
|
||||||
|
outline: "text-error-600 ring-1 ring-error-200 hover:bg-error-50 dark:ring-error-500/40 dark:text-error-300 dark:hover:bg-error-500/10",
|
||||||
|
ghost: "text-error-600 hover:bg-error-50 dark:text-error-300 dark:hover:bg-error-500/10",
|
||||||
|
ring: "focus-visible:ring-error-500",
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
solid: "bg-gray-700 text-white hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500",
|
||||||
|
outline: "text-gray-700 ring-1 ring-gray-300 hover:bg-gray-100 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-800",
|
||||||
|
ghost: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800",
|
||||||
|
ring: "focus-visible:ring-gray-400",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<IconButtonSize, string> = {
|
||||||
|
xs: "h-6 w-6 text-xs",
|
||||||
|
sm: "h-8 w-8 text-sm",
|
||||||
|
md: "h-10 w-10 text-base",
|
||||||
|
lg: "h-12 w-12 text-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconSizeClasses: Record<IconButtonSize, string> = {
|
||||||
|
xs: "[&>svg]:w-3 [&>svg]:h-3",
|
||||||
|
sm: "[&>svg]:w-4 [&>svg]:h-4",
|
||||||
|
md: "[&>svg]:w-5 [&>svg]:h-5",
|
||||||
|
lg: "[&>svg]:w-6 [&>svg]:h-6",
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
icon,
|
||||||
|
size = "md",
|
||||||
|
variant = "ghost",
|
||||||
|
tone = "neutral",
|
||||||
|
shape = "rounded",
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
disabled = false,
|
||||||
|
type = "button",
|
||||||
|
title,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const toneStyles = toneMap[tone];
|
||||||
|
|
||||||
|
const variantClasses: Record<IconButtonVariant, string> = {
|
||||||
|
solid: toneStyles.solid,
|
||||||
|
outline: clsx("bg-transparent transition-colors", toneStyles.outline),
|
||||||
|
ghost: toneStyles.ghost,
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
"inline-flex items-center justify-center transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
|
|
||||||
|
const shapeClasses = shape === "circle" ? "rounded-full" : "rounded-lg";
|
||||||
|
|
||||||
|
const computedClass = twMerge(
|
||||||
|
clsx(
|
||||||
|
baseClasses,
|
||||||
|
sizeClasses[size],
|
||||||
|
iconSizeClasses[size],
|
||||||
|
shapeClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
toneStyles.ring,
|
||||||
|
className,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={computedClass}
|
||||||
|
onClick={onClick}
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
aria-label={ariaLabel || title}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
IconButton.displayName = "IconButton";
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import Checkbox from "../../form/input/Checkbox";
|
||||||
|
|
||||||
interface ListProps {
|
interface ListProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -134,23 +135,13 @@ export const ListCheckboxItem: React.FC<ListCheckboxItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<li className={`border-b border-gray-200 px-3 py-2.5 last:border-b-0 dark:border-gray-800 ${className}`}>
|
<li className={`border-b border-gray-200 px-3 py-2.5 last:border-b-0 dark:border-gray-800 ${className}`}>
|
||||||
<div className="flex items-center gap-2">
|
<Checkbox
|
||||||
<label className="flex items-center space-x-3 group cursor-pointer">
|
id={id}
|
||||||
<div className="relative w-5 h-5">
|
label={label}
|
||||||
<input
|
checked={checked}
|
||||||
id={id}
|
disabled={disabled}
|
||||||
className="w-5 h-5 appearance-none cursor-pointer dark:border-gray-700 border border-gray-300 checked:border-transparent rounded-md checked:bg-brand-500 disabled:opacity-60"
|
onChange={(val) => onChange?.(val)}
|
||||||
type="checkbox"
|
/>
|
||||||
checked={checked}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => onChange?.(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label htmlFor={id} className="flex items-center text-sm text-gray-500 cursor-pointer select-none dark:text-gray-400">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AngleLeftIcon, AngleRightIcon } from "../../../icons";
|
import { AngleLeftIcon, AngleRightIcon } from "../../../icons";
|
||||||
|
import Select from "../../form/Select";
|
||||||
|
|
||||||
interface CompactPaginationProps {
|
interface CompactPaginationProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -61,16 +62,15 @@ export const CompactPagination: React.FC<CompactPaginationProps> = ({
|
|||||||
<label htmlFor="page-size" className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<label htmlFor="page-size" className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
Show:
|
Show:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
id="page-size"
|
options={[
|
||||||
value={pageSize}
|
{ value: '10', label: '10' },
|
||||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
{ value: '20', label: '20' },
|
||||||
className="h-8 px-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
{ value: '50', label: '50' },
|
||||||
>
|
]}
|
||||||
<option value={10}>10</option>
|
defaultValue={String(pageSize)}
|
||||||
<option value={20}>20</option>
|
onChange={(val) => onPageSizeChange(Number(val))}
|
||||||
<option value={50}>50</option>
|
/>
|
||||||
</select>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
per page
|
per page
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import Button from '../button/Button';
|
||||||
|
|
||||||
export interface PricingPlan {
|
export interface PricingPlan {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -109,8 +110,10 @@ export default function PricingTable({
|
|||||||
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
||||||
}}
|
}}
|
||||||
></span>
|
></span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
||||||
billingPeriod === 'monthly'
|
billingPeriod === 'monthly'
|
||||||
@@ -119,9 +122,11 @@ export default function PricingTable({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annually')}
|
onClick={() => setBillingPeriod('annually')}
|
||||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
||||||
billingPeriod === 'annually'
|
billingPeriod === 'annually'
|
||||||
@@ -130,7 +135,7 @@ export default function PricingTable({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Annually
|
Annually
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{billingPeriod === 'annually' && (
|
{billingPeriod === 'annually' && (
|
||||||
<span className="absolute left-[calc(100%+1rem)] whitespace-nowrap inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full text-sm">
|
<span className="absolute left-[calc(100%+1rem)] whitespace-nowrap inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full text-sm">
|
||||||
@@ -230,7 +235,10 @@ export default function PricingTable({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
onClick={() => handlePlanClick(plan)}
|
onClick={() => handlePlanClick(plan)}
|
||||||
disabled={plan.disabled}
|
disabled={plan.disabled}
|
||||||
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors mt-auto ${
|
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors mt-auto ${
|
||||||
@@ -240,7 +248,7 @@ export default function PricingTable({
|
|||||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
>
|
>
|
||||||
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -304,7 +312,10 @@ export default function PricingTable({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
onClick={() => handlePlanClick(plan)}
|
onClick={() => handlePlanClick(plan)}
|
||||||
disabled={plan.disabled}
|
disabled={plan.disabled}
|
||||||
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors ${
|
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors ${
|
||||||
@@ -314,7 +325,7 @@ export default function PricingTable({
|
|||||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
>
|
>
|
||||||
{plan.buttonText || (isHighlighted ? 'Choose This Plan' : 'Choose Starter')}
|
{plan.buttonText || (isHighlighted ? 'Choose This Plan' : 'Choose Starter')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -376,7 +387,10 @@ export default function PricingTable({
|
|||||||
>
|
>
|
||||||
{plan.period || 'For a Lifetime'}
|
{plan.period || 'For a Lifetime'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
onClick={() => handlePlanClick(plan)}
|
onClick={() => handlePlanClick(plan)}
|
||||||
disabled={plan.disabled}
|
disabled={plan.disabled}
|
||||||
className={`flex h-11 w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium shadow-theme-xs transition-colors ${
|
className={`flex h-11 w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium shadow-theme-xs transition-colors ${
|
||||||
@@ -388,7 +402,7 @@ export default function PricingTable({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{plan.buttonText || (plan.disabled ? 'Current Plan' : 'Try for Free')}
|
{plan.buttonText || (plan.disabled ? 'Current Plan' : 'Try for Free')}
|
||||||
</button>
|
</Button>
|
||||||
<ul className="mt-6 space-y-3">
|
<ul className="mt-6 space-y-3">
|
||||||
{plan.features.map((feature, idx) => {
|
{plan.features.map((feature, idx) => {
|
||||||
const isExcluded = feature.startsWith('!');
|
const isExcluded = feature.startsWith('!');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CheckIcon } from '../../../icons';
|
import { CheckIcon } from '../../../icons';
|
||||||
|
import Button from '../button/Button';
|
||||||
|
|
||||||
export interface PricingPlan {
|
export interface PricingPlan {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -66,26 +67,20 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
|||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<div className="inline-flex items-center gap-3">
|
<div className="inline-flex items-center gap-3">
|
||||||
<div className="inline-flex items-center gap-3 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
<div className="inline-flex items-center gap-3 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
<button
|
<Button
|
||||||
|
variant={billingPeriod === 'monthly' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
billingPeriod === 'monthly'
|
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
||||||
: 'text-gray-600 dark:text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={billingPeriod === 'annual' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annual')}
|
onClick={() => setBillingPeriod('annual')}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
billingPeriod === 'annual'
|
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
||||||
: 'text-gray-600 dark:text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Annually
|
Annually
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{billingPeriod === 'annual' && (
|
{billingPeriod === 'annual' && (
|
||||||
<span className="badge-success">
|
<span className="badge-success">
|
||||||
@@ -202,13 +197,14 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
className={plan.highlighted ? 'btn-primary' : 'btn-outline'}
|
variant={plan.highlighted ? 'primary' : 'outline'}
|
||||||
|
fullWidth
|
||||||
onClick={() => onPlanSelect?.(plan)}
|
onClick={() => onPlanSelect?.(plan)}
|
||||||
disabled={plan.disabled}
|
disabled={plan.disabled}
|
||||||
>
|
>
|
||||||
{plan.buttonText}
|
{plan.buttonText}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SearchModal from "../components/common/SearchModal";
|
|||||||
import SiteAndSectorSelector from "../components/common/SiteAndSectorSelector";
|
import SiteAndSectorSelector from "../components/common/SiteAndSectorSelector";
|
||||||
import SingleSiteSelector from "../components/common/SingleSiteSelector";
|
import SingleSiteSelector from "../components/common/SingleSiteSelector";
|
||||||
import SiteWithAllSitesSelector from "../components/common/SiteWithAllSitesSelector";
|
import SiteWithAllSitesSelector from "../components/common/SiteWithAllSitesSelector";
|
||||||
|
import IconButton from "../components/ui/button/IconButton";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// Route patterns for selector visibility
|
// Route patterns for selector visibility
|
||||||
@@ -104,22 +105,26 @@ const AppHeader: React.FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Mobile Menu Toggle */}
|
{/* Mobile Menu Toggle */}
|
||||||
<button
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
onClick={toggleApplicationMenu}
|
onClick={toggleApplicationMenu}
|
||||||
className="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
|
className="w-10 h-10 z-99999 lg:hidden"
|
||||||
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z" fill="currentColor" />
|
<path fillRule="evenodd" clipRule="evenodd" d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</IconButton>
|
||||||
|
|
||||||
{/* Page Title with Badge - Desktop */}
|
{/* Page Title with Badge - Desktop */}
|
||||||
{pageInfo && (
|
{pageInfo && (
|
||||||
<div className="hidden lg:flex items-center gap-3">
|
<div className="hidden lg:flex items-center gap-3">
|
||||||
{/* Sidebar Toggle Button - Always visible on desktop */}
|
{/* Sidebar Toggle Button - Always visible on desktop */}
|
||||||
<button
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="xs"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="flex items-center justify-center w-6 h-6 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-full shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
className="w-6 h-6 rounded-full"
|
||||||
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -130,7 +135,7 @@ const AppHeader: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</IconButton>
|
||||||
|
|
||||||
{pageInfo.badge && (
|
{pageInfo.badge && (
|
||||||
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${badgeColors[pageInfo.badge.color]?.bg || 'bg-gray-600'} flex-shrink-0`}>
|
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${badgeColors[pageInfo.badge.color]?.bg || 'bg-gray-600'} flex-shrink-0`}>
|
||||||
@@ -181,15 +186,17 @@ const AppHeader: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Search Icon */}
|
{/* Search Icon */}
|
||||||
<button
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setIsSearchOpen(true)}
|
onClick={() => setIsSearchOpen(true)}
|
||||||
className="flex items-center justify-center w-10 h-10 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors"
|
className="w-10 h-10"
|
||||||
title="Search (⌘K)"
|
title="Search (⌘K)"
|
||||||
|
aria-label="Search"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</IconButton>
|
||||||
|
|
||||||
{/* Dark Mode Toggler */}
|
{/* Dark Mode Toggler */}
|
||||||
<ThemeToggleButton />
|
<ThemeToggleButton />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import SEO from "../components/SEO";
|
import SEO from "../components/SEO";
|
||||||
import { getMetaTags } from "../config/metaTags";
|
import { getMetaTags } from "../config/metaTags";
|
||||||
|
import Button from "../../components/ui/button/Button";
|
||||||
|
|
||||||
const CaseStudies: React.FC = () => {
|
const CaseStudies: React.FC = () => {
|
||||||
const renderCta = (cta: { label: string; href: string }, className: string) => {
|
const renderCta = (cta: { label: string; href: string }, className: string) => {
|
||||||
@@ -190,10 +191,12 @@ const CaseStudies: React.FC = () => {
|
|||||||
<p className="text-base text-gray-700 leading-relaxed">
|
<p className="text-base text-gray-700 leading-relaxed">
|
||||||
Igny8's roadmap is shaped by an active community of customer strategists, agency partners, and product marketers. Join and get early access to features, template libraries, and industry benchmarks.
|
Igny8's roadmap is shaped by an active community of customer strategists, agency partners, and product marketers. Join and get early access to features, template libraries, and industry benchmarks.
|
||||||
</p>
|
</p>
|
||||||
<button className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white px-6 py-3 text-sm font-semibold hover:shadow-lg transition">
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
endIcon={<ArrowRightIcon className="h-4 w-4" />}
|
||||||
|
>
|
||||||
Join the CAB waitlist
|
Join the CAB waitlist
|
||||||
<ArrowRightIcon className="h-4 w-4" />
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import SectionHeading from "../components/SectionHeading";
|
|||||||
import CTASection from "../components/CTASection";
|
import CTASection from "../components/CTASection";
|
||||||
import SEO from "../components/SEO";
|
import SEO from "../components/SEO";
|
||||||
import { getMetaTags } from "../config/metaTags";
|
import { getMetaTags } from "../config/metaTags";
|
||||||
|
import InputField from "../../components/form/input/InputField";
|
||||||
|
import TextArea from "../../components/form/input/TextArea";
|
||||||
|
import Button from "../../components/ui/button/Button";
|
||||||
|
|
||||||
const Contact: React.FC = () => {
|
const Contact: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +25,7 @@ const Contact: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
||||||
First name
|
First name
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Alex"
|
placeholder="Alex"
|
||||||
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||||
@@ -30,7 +33,7 @@ const Contact: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
||||||
Last name
|
Last name
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rivera"
|
placeholder="Rivera"
|
||||||
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||||
@@ -40,7 +43,7 @@ const Contact: React.FC = () => {
|
|||||||
|
|
||||||
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
||||||
Work email
|
Work email
|
||||||
<input
|
<InputField
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="you@company.com"
|
placeholder="you@company.com"
|
||||||
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||||
@@ -49,7 +52,7 @@ const Contact: React.FC = () => {
|
|||||||
|
|
||||||
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
||||||
Company
|
Company
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Company name"
|
placeholder="Company name"
|
||||||
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||||
@@ -58,19 +61,22 @@ const Contact: React.FC = () => {
|
|||||||
|
|
||||||
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
<label className="flex flex-col gap-2 text-sm text-gray-600">
|
||||||
How can we help?
|
How can we help?
|
||||||
<textarea
|
<TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Tell us about your current workflow, challenges, and goals."
|
placeholder="Tell us about your current workflow, challenges, and goals."
|
||||||
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 resize-none"
|
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 resize-none"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
className="inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-brand-700)] hover:from-[var(--color-brand-700)] hover:to-[var(--color-primary)] text-white px-6 py-3 text-sm font-semibold shadow-lg shadow-[var(--color-primary)]/30 transition-all w-full"
|
className="inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-brand-700)] hover:from-[var(--color-brand-700)] hover:to-[var(--color-primary)] text-white px-6 py-3 text-sm font-semibold shadow-lg shadow-[var(--color-primary)]/30 transition-all w-full"
|
||||||
>
|
>
|
||||||
Book strategy call
|
Book strategy call
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { PricingTable, PricingPlan } from "../../components/ui/pricing-table";
|
|||||||
import PricingTable1 from "../../components/ui/pricing-table/pricing-table-1";
|
import PricingTable1 from "../../components/ui/pricing-table/pricing-table-1";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Plan, convertToPricingPlan } from "../../utils/pricingHelpers";
|
import { Plan, convertToPricingPlan } from "../../utils/pricingHelpers";
|
||||||
|
import Button from "../../components/ui/button/Button";
|
||||||
|
|
||||||
const Pricing: React.FC = () => {
|
const Pricing: React.FC = () => {
|
||||||
const [plans, setPlans] = useState<Plan[]>([]);
|
const [plans, setPlans] = useState<Plan[]>([]);
|
||||||
@@ -326,12 +327,15 @@ const Pricing: React.FC = () => {
|
|||||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-error-600">{error}</p>
|
<p className="text-error-600">{error}</p>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
size="md"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="mt-4 px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)]"
|
className="mt-4 px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)]"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import SectionHeading from "../components/SectionHeading";
|
|||||||
import CTASection from "../components/CTASection";
|
import CTASection from "../components/CTASection";
|
||||||
import SEO from "../components/SEO";
|
import SEO from "../components/SEO";
|
||||||
import { getMetaTags } from "../config/metaTags";
|
import { getMetaTags } from "../config/metaTags";
|
||||||
|
import InputField from "../../components/form/input/InputField";
|
||||||
|
import TextArea from "../../components/form/input/TextArea";
|
||||||
|
import Button from "../../components/ui/button/Button";
|
||||||
|
|
||||||
const roadmapItems = [
|
const roadmapItems = [
|
||||||
{
|
{
|
||||||
@@ -42,27 +45,25 @@ const Waitlist: React.FC = () => {
|
|||||||
Share your details and we'll invite you to beta cohorts with onboarding resources and direct feedback loops to our product team.
|
Share your details and we'll invite you to beta cohorts with onboarding resources and direct feedback loops to our product team.
|
||||||
</p>
|
</p>
|
||||||
<form className="space-y-4">
|
<form className="space-y-4">
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<InputField
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Work email"
|
placeholder="Work email"
|
||||||
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
|
||||||
/>
|
/>
|
||||||
<textarea
|
<TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Tell us about your current workflow and why you're excited."
|
placeholder="Tell us about your current workflow and why you're excited."
|
||||||
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 resize-none"
|
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-brand-700)] hover:from-[var(--color-brand-700)] hover:to-[var(--color-primary)] text-white px-6 py-3 text-sm font-semibold shadow-lg shadow-[var(--color-primary)]/30 transition-all w-full"
|
variant="primary"
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
Join waitlist
|
Join waitlist
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { EventInput, DateSelectArg, EventClickArg } from "@fullcalendar/core";
|
|||||||
import { Modal } from "../components/ui/modal";
|
import { Modal } from "../components/ui/modal";
|
||||||
import { useModal } from "../hooks/useModal";
|
import { useModal } from "../hooks/useModal";
|
||||||
import PageMeta from "../components/common/PageMeta";
|
import PageMeta from "../components/common/PageMeta";
|
||||||
|
import Button from "../components/ui/button/Button";
|
||||||
|
import InputField from "../components/form/input/InputField";
|
||||||
|
|
||||||
interface CalendarEvent extends EventInput {
|
interface CalendarEvent extends EventInput {
|
||||||
extendedProps: {
|
extendedProps: {
|
||||||
@@ -166,12 +168,11 @@ const Calendar: React.FC = () => {
|
|||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||||
Event Title
|
Event Title
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
id="event-title"
|
id="event-title"
|
||||||
type="text"
|
type="text"
|
||||||
value={eventTitle}
|
value={eventTitle}
|
||||||
onChange={(e) => setEventTitle(e.target.value)}
|
onChange={(e) => setEventTitle(e.target.value)}
|
||||||
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,12 +221,11 @@ const Calendar: React.FC = () => {
|
|||||||
Enter Start Date
|
Enter Start Date
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<InputField
|
||||||
id="event-start-date"
|
id="event-start-date"
|
||||||
type="date"
|
type="date"
|
||||||
value={eventStartDate}
|
value={eventStartDate}
|
||||||
onChange={(e) => setEventStartDate(e.target.value)}
|
onChange={(e) => setEventStartDate(e.target.value)}
|
||||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,31 +235,32 @@ const Calendar: React.FC = () => {
|
|||||||
Enter End Date
|
Enter End Date
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<InputField
|
||||||
id="event-end-date"
|
id="event-end-date"
|
||||||
type="date"
|
type="date"
|
||||||
value={eventEndDate}
|
value={eventEndDate}
|
||||||
onChange={(e) => setEventEndDate(e.target.value)}
|
onChange={(e) => setEventEndDate(e.target.value)}
|
||||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
|
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
|
||||||
<button
|
<Button
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
type="button"
|
variant="outline"
|
||||||
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleAddOrUpdateEvent}
|
onClick={handleAddOrUpdateEvent}
|
||||||
type="button"
|
variant="primary"
|
||||||
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
tone="brand"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
{selectedEvent ? "Update Changes" : "Add Event"}
|
{selectedEvent ? "Update Changes" : "Add Event"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Pagination } from '../components/ui/pagination/Pagination';
|
|||||||
import { Card, CardImage, CardTitle, CardDescription, CardAction, CardIcon } from '../components/ui/card/Card';
|
import { Card, CardImage, CardTitle, CardDescription, CardAction, CardIcon } from '../components/ui/card/Card';
|
||||||
import ChartTab from '../components/common/ChartTab';
|
import ChartTab from '../components/common/ChartTab';
|
||||||
import PageMeta from '../components/common/PageMeta';
|
import PageMeta from '../components/common/PageMeta';
|
||||||
|
import InputField from '../components/form/input/InputField';
|
||||||
|
import TextArea from '../components/form/input/TextArea';
|
||||||
|
|
||||||
export default function Components() {
|
export default function Components() {
|
||||||
// Alert modals state
|
// Alert modals state
|
||||||
@@ -257,9 +259,8 @@ export default function Components() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
First Name
|
First Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
defaultValue="Emirhan"
|
defaultValue="Emirhan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,9 +268,8 @@ export default function Components() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Last Name
|
Last Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
defaultValue="Boruch"
|
defaultValue="Boruch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,9 +279,8 @@ export default function Components() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="email"
|
type="email"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
defaultValue="emirhanboruch55@gmail.com"
|
defaultValue="emirhanboruch55@gmail.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,9 +288,8 @@ export default function Components() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Phone
|
Phone
|
||||||
</label>
|
</label>
|
||||||
<input
|
<InputField
|
||||||
type="tel"
|
type="tel"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
defaultValue="+09 363 398 46"
|
defaultValue="+09 363 398 46"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,9 +298,8 @@ export default function Components() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Bio
|
Bio
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
defaultValue="Team Manager"
|
defaultValue="Team Manager"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,34 +446,34 @@ function ButtonGroupsShowcase() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Default Button Group */}
|
{/* Default Button Group */}
|
||||||
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
|
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
|
||||||
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="rounded-r-none border-r-0">
|
||||||
Left
|
Left
|
||||||
</button>
|
</Button>
|
||||||
<button className="px-4 py-2 text-sm font-medium text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="rounded-none border-x border-gray-300 dark:border-gray-700">
|
||||||
Center
|
Center
|
||||||
</button>
|
</Button>
|
||||||
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="rounded-l-none border-l-0">
|
||||||
Right
|
Right
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Icon Button Group */}
|
{/* Icon Button Group */}
|
||||||
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
|
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
|
||||||
<button className="p-2 text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="p-2 rounded-r-none border-r-0">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
<button className="p-2 text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="p-2 rounded-none border-x border-gray-300 dark:border-gray-700">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
<button className="p-2 text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
|
<Button variant="ghost" className="p-2 rounded-l-none border-l-0">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 10a1 1 0 011 1h12a1 1 0 110-2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M3 10a1 1 0 011 1h12a1 1 0 110-2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
import { linkerApi } from '../../api/linker.api';
|
import { linkerApi } from '../../api/linker.api';
|
||||||
import { fetchContent, Content as ContentType } from '../../services/api';
|
import { fetchContent, Content as ContentType } from '../../services/api';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
@@ -170,10 +171,11 @@ export default function LinkerContentList() {
|
|||||||
{item.linker_version || 0}
|
{item.linker_version || 0}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<button
|
<Button
|
||||||
onClick={() => handleLink(item.id)}
|
onClick={() => handleLink(item.id)}
|
||||||
disabled={isProcessing || processing === -1}
|
disabled={isProcessing || processing === -1}
|
||||||
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
@@ -186,7 +188,7 @@ export default function LinkerContentList() {
|
|||||||
Add Links
|
Add Links
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -202,20 +204,22 @@ export default function LinkerContentList() {
|
|||||||
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
|
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setCurrentPage(prev => prev + 1)}
|
onClick={() => setCurrentPage(prev => prev + 1)}
|
||||||
disabled={currentPage * pageSize >= totalCount}
|
disabled={currentPage * pageSize >= totalCount}
|
||||||
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { OptimizationScores } from '../../components/optimizer/OptimizationScore
|
|||||||
import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons';
|
import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons';
|
||||||
import { useSectorStore } from '../../store/sectorStore';
|
import { useSectorStore } from '../../store/sectorStore';
|
||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
|
||||||
export default function OptimizerContentSelector() {
|
export default function OptimizerContentSelector() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -153,21 +156,21 @@ export default function OptimizerContentSelector() {
|
|||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<select
|
<Select
|
||||||
value={entryPoint}
|
options={[
|
||||||
onChange={(e) => setEntryPoint(e.target.value as EntryPoint)}
|
{ value: 'auto', label: 'Auto-detect' },
|
||||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
{ value: 'writer', label: 'From Writer' },
|
||||||
>
|
{ value: 'wordpress', label: 'From WordPress' },
|
||||||
<option value="auto">Auto-detect</option>
|
{ value: 'external', label: 'From External' },
|
||||||
<option value="writer">From Writer</option>
|
{ value: 'manual', label: 'Manual' },
|
||||||
<option value="wordpress">From WordPress</option>
|
]}
|
||||||
<option value="external">From External</option>
|
defaultValue={entryPoint}
|
||||||
<option value="manual">Manual</option>
|
onChange={(val) => setEntryPoint(val as EntryPoint)}
|
||||||
</select>
|
/>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={handleBatchOptimize}
|
onClick={handleBatchOptimize}
|
||||||
disabled={selectedIds.length === 0 || processing.length > 0}
|
disabled={selectedIds.length === 0 || processing.length > 0}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
>
|
||||||
{processing.length > 0 ? (
|
{processing.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -180,7 +183,7 @@ export default function OptimizerContentSelector() {
|
|||||||
Optimize Selected ({selectedIds.length})
|
Optimize Selected ({selectedIds.length})
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
@@ -202,11 +205,9 @@ export default function OptimizerContentSelector() {
|
|||||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left">
|
<th className="px-6 py-3 text-left">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={selectedIds.length === filteredContent.length && filteredContent.length > 0}
|
checked={selectedIds.length === filteredContent.length && filteredContent.length > 0}
|
||||||
onChange={toggleSelectAll}
|
onChange={toggleSelectAll}
|
||||||
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
@@ -238,11 +239,9 @@ export default function OptimizerContentSelector() {
|
|||||||
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-brand-50 dark:bg-brand-900/20' : ''}`}
|
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-brand-50 dark:bg-brand-900/20' : ''}`}
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => toggleSelection(item.id)}
|
onChange={() => toggleSelection(item.id)}
|
||||||
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
@@ -266,10 +265,11 @@ export default function OptimizerContentSelector() {
|
|||||||
{item.optimizer_version || 0}
|
{item.optimizer_version || 0}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleOptimize(item.id)}
|
onClick={() => handleOptimize(item.id)}
|
||||||
disabled={isProcessing || processing.length > 0}
|
disabled={isProcessing || processing.length > 0}
|
||||||
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
@@ -282,7 +282,7 @@ export default function OptimizerContentSelector() {
|
|||||||
Optimize
|
Optimize
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -298,20 +298,22 @@ export default function OptimizerContentSelector() {
|
|||||||
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
|
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setCurrentPage(prev => prev + 1)}
|
onClick={() => setCurrentPage(prev => prev + 1)}
|
||||||
disabled={currentPage * pageSize >= totalCount}
|
disabled={currentPage * pageSize >= totalCount}
|
||||||
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation, useNavigate, Link } from "react-router-dom";
|
import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
|
import InputField from "../components/form/input/InputField";
|
||||||
|
import TextArea from "../components/form/input/TextArea";
|
||||||
|
import Label from "../components/form/Label";
|
||||||
|
import Button from "../components/ui/button/Button";
|
||||||
|
|
||||||
const PLAN_COPY: Record<string, { name: string; price: string; content: string }> = {
|
const PLAN_COPY: Record<string, { name: string; price: string; content: string }> = {
|
||||||
starter: { name: "Starter", price: "$49/mo", content: "50 content pieces/month" },
|
starter: { name: "Starter", price: "$49/mo", content: "50 content pieces/month" },
|
||||||
@@ -76,42 +80,39 @@ export default function Payment() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="block text-sm font-medium text-gray-800">
|
<InputField
|
||||||
Contact email
|
label="Contact email"
|
||||||
<input
|
type="email"
|
||||||
type="email"
|
value={contactEmail}
|
||||||
value={contactEmail}
|
onChange={(e) => setContactEmail(e.target.value)}
|
||||||
onChange={(e) => setContactEmail(e.target.value)}
|
placeholder="you@example.com"
|
||||||
placeholder="you@example.com"
|
/>
|
||||||
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
|
<div>
|
||||||
/>
|
<Label className="mb-2">Notes (optional)</Label>
|
||||||
</label>
|
<TextArea
|
||||||
<label className="block text-sm font-medium text-gray-800">
|
|
||||||
Notes (optional)
|
|
||||||
<textarea
|
|
||||||
value={note}
|
value={note}
|
||||||
onChange={(e) => setNote(e.target.value)}
|
onChange={(value) => setNote(value)}
|
||||||
placeholder="Company name, billing contact, or questions"
|
placeholder="Company name, billing contact, or questions"
|
||||||
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
|
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link to="/signup" className="text-sm text-gray-600 hover:text-gray-800">
|
<Link to="/signup" className="text-sm text-gray-600 hover:text-gray-800">
|
||||||
Prefer the free plan? Start your trial
|
Prefer the free plan? Start your trial
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<Button
|
||||||
href={mailtoHref || "#"}
|
onClick={() => {
|
||||||
onClick={handleRequest}
|
if (mailtoHref && contactEmail.trim()) {
|
||||||
className={`inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold text-white ${
|
window.location.href = mailtoHref;
|
||||||
contactEmail.trim() ? "bg-brand-600 hover:bg-brand-700" : "bg-brand-400 cursor-not-allowed"
|
}
|
||||||
}`}
|
}}
|
||||||
aria-disabled={!contactEmail.trim()}
|
disabled={!contactEmail.trim()}
|
||||||
|
variant="primary"
|
||||||
>
|
>
|
||||||
Request payment instructions
|
Request payment instructions
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-sm text-error-600">{error}</p>}
|
{error && <p className="text-sm text-error-600">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -233,10 +233,10 @@ export default function ClusterDetail() {
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => handleTabChange('articles')}
|
onClick={() => handleTabChange('articles')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'articles'
|
activeTab === 'articles'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -244,11 +244,11 @@ export default function ClusterDetail() {
|
|||||||
>
|
>
|
||||||
<FileIcon className="w-4 h-4 inline mr-2" />
|
<FileIcon className="w-4 h-4 inline mr-2" />
|
||||||
Articles
|
Articles
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => handleTabChange('pages')}
|
onClick={() => handleTabChange('pages')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'pages'
|
activeTab === 'pages'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -256,11 +256,11 @@ export default function ClusterDetail() {
|
|||||||
>
|
>
|
||||||
<PageIcon className="w-4 h-4 inline mr-2" />
|
<PageIcon className="w-4 h-4 inline mr-2" />
|
||||||
Pages
|
Pages
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => handleTabChange('products')}
|
onClick={() => handleTabChange('products')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'products'
|
activeTab === 'products'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -268,11 +268,11 @@ export default function ClusterDetail() {
|
|||||||
>
|
>
|
||||||
<GridIcon className="w-4 h-4 inline mr-2" />
|
<GridIcon className="w-4 h-4 inline mr-2" />
|
||||||
Products
|
Products
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => handleTabChange('taxonomy')}
|
onClick={() => handleTabChange('taxonomy')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'taxonomy'
|
activeTab === 'taxonomy'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -280,7 +280,7 @@ export default function ClusterDetail() {
|
|||||||
>
|
>
|
||||||
<TagIcon className="w-4 h-4 inline mr-2" />
|
<TagIcon className="w-4 h-4 inline mr-2" />
|
||||||
Taxonomy
|
Taxonomy
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -127,26 +127,28 @@ const CreditsAndBilling: React.FC = () => {
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setActiveTab('overview')}
|
onClick={() => setActiveTab('overview')}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
|
||||||
activeTab === 'overview'
|
activeTab === 'overview'
|
||||||
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Overview
|
Overview
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setActiveTab('transactions')}
|
onClick={() => setActiveTab('transactions')}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
|
||||||
activeTab === 'transactions'
|
activeTab === 'transactions'
|
||||||
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Transactions ({transactions.length})
|
Transactions ({transactions.length})
|
||||||
</button>
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useSettingsStore } from '../../store/settingsStore';
|
|||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import Label from '../../components/form/Label';
|
import Label from '../../components/form/Label';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
|
||||||
export default function GeneralSettings() {
|
export default function GeneralSettings() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -49,13 +51,10 @@ export default function GeneralSettings() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="records_per_page">Records Per Page</Label>
|
<Label htmlFor="records_per_page">Records Per Page</Label>
|
||||||
<input
|
<InputField
|
||||||
id="records_per_page"
|
id="records_per_page"
|
||||||
type="number"
|
type="number"
|
||||||
min="5"
|
value={tableSettings.records_per_page.toString()}
|
||||||
max="100"
|
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={tableSettings.records_per_page}
|
|
||||||
onChange={(e) => setTableSettings({
|
onChange={(e) => setTableSettings({
|
||||||
...tableSettings,
|
...tableSettings,
|
||||||
records_per_page: parseInt(e.target.value) || 20
|
records_per_page: parseInt(e.target.value) || 20
|
||||||
@@ -65,10 +64,9 @@ export default function GeneralSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="default_sort">Default Sort Field</Label>
|
<Label htmlFor="default_sort">Default Sort Field</Label>
|
||||||
<input
|
<InputField
|
||||||
id="default_sort"
|
id="default_sort"
|
||||||
type="text"
|
type="text"
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
value={tableSettings.default_sort}
|
value={tableSettings.default_sort}
|
||||||
onChange={(e) => setTableSettings({
|
onChange={(e) => setTableSettings({
|
||||||
...tableSettings,
|
...tableSettings,
|
||||||
@@ -79,18 +77,17 @@ export default function GeneralSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="default_sort_direction">Default Sort Direction</Label>
|
<Label htmlFor="default_sort_direction">Default Sort Direction</Label>
|
||||||
<select
|
<Select
|
||||||
id="default_sort_direction"
|
options={[
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
|
{ value: 'asc', label: 'Ascending' },
|
||||||
|
{ value: 'desc', label: 'Descending' },
|
||||||
|
]}
|
||||||
value={tableSettings.default_sort_direction}
|
value={tableSettings.default_sort_direction}
|
||||||
onChange={(e) => setTableSettings({
|
onChange={(value) => setTableSettings({
|
||||||
...tableSettings,
|
...tableSettings,
|
||||||
default_sort_direction: e.target.value as 'asc' | 'desc'
|
default_sort_direction: value as 'asc' | 'desc'
|
||||||
})}
|
})}
|
||||||
>
|
/>
|
||||||
<option value="asc">Ascending</option>
|
|
||||||
<option value="desc">Descending</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import FormModal, { FormField } from '../../components/common/FormModal';
|
|||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import Alert from '../../components/ui/alert/Alert';
|
import Alert from '../../components/ui/alert/Alert';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
import {
|
import {
|
||||||
fetchSites,
|
fetchSites,
|
||||||
createSite,
|
createSite,
|
||||||
@@ -475,21 +477,20 @@ export default function Sites() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Select Industry
|
Select Industry
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select an industry...' },
|
||||||
|
...industries.map((industry) => ({
|
||||||
|
value: industry.slug,
|
||||||
|
label: industry.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
value={selectedIndustry}
|
value={selectedIndustry}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
setSelectedIndustry(e.target.value);
|
setSelectedIndustry(value);
|
||||||
setSelectedSectors([]); // Reset sectors when industry changes
|
setSelectedSectors([]); // Reset sectors when industry changes
|
||||||
}}
|
}}
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
/>
|
||||||
>
|
|
||||||
<option value="">Select an industry...</option>
|
|
||||||
{industries.map((industry) => (
|
|
||||||
<option key={industry.slug} value={industry.slug}>
|
|
||||||
{industry.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{selectedIndustry && (
|
{selectedIndustry && (
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||||
@@ -504,15 +505,14 @@ export default function Sites() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||||
{getIndustrySectors().map((sector) => (
|
{getIndustrySectors().map((sector) => (
|
||||||
<label
|
<div
|
||||||
key={sector.slug}
|
key={sector.slug}
|
||||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||||
>
|
>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={selectedSectors.includes(sector.slug)}
|
checked={selectedSectors.includes(sector.slug)}
|
||||||
onChange={(e) => {
|
onChange={(checked) => {
|
||||||
if (e.target.checked) {
|
if (checked) {
|
||||||
if (selectedSectors.length >= 5) {
|
if (selectedSectors.length >= 5) {
|
||||||
toast.error('Maximum 5 sectors allowed per site');
|
toast.error('Maximum 5 sectors allowed per site');
|
||||||
return;
|
return;
|
||||||
@@ -522,17 +522,18 @@ export default function Sites() {
|
|||||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
label={
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||||
|
{sector.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{sector.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
</div>
|
||||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
|
||||||
{sector.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
{sector.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import PageMeta from '../../components/common/PageMeta';
|
|||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
import { SearchIcon } from '../../icons';
|
import { SearchIcon } from '../../icons';
|
||||||
@@ -140,63 +142,50 @@ export default function SiteContentManager() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
|
||||||
placeholder="Search content..."
|
placeholder="Search content..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchTerm(e.target.value);
|
setSearchTerm(e.target.value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select
|
<Select
|
||||||
value={statusFilter}
|
options={STATUS_OPTIONS}
|
||||||
onChange={(e) => {
|
defaultValue={statusFilter}
|
||||||
setStatusFilter(e.target.value);
|
onChange={(value) => {
|
||||||
|
setStatusFilter(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
{STATUS_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
<Select
|
||||||
value={sourceFilter}
|
options={SOURCE_OPTIONS}
|
||||||
onChange={(e) => {
|
defaultValue={sourceFilter}
|
||||||
setSourceFilter(e.target.value);
|
onChange={(value) => {
|
||||||
|
setSourceFilter(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
{SOURCE_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
<Select
|
||||||
value={`${sortBy}-${sortDirection}`}
|
options={[
|
||||||
onChange={(e) => {
|
{ value: 'created_at-desc', label: 'Newest First' },
|
||||||
const [field, direction] = e.target.value.split('-');
|
{ value: 'created_at-asc', label: 'Oldest First' },
|
||||||
|
{ value: 'updated_at-desc', label: 'Recently Updated' },
|
||||||
|
{ value: 'title-asc', label: 'Title A-Z' },
|
||||||
|
{ value: 'title-desc', label: 'Title Z-A' },
|
||||||
|
]}
|
||||||
|
defaultValue={`${sortBy}-${sortDirection}`}
|
||||||
|
onChange={(value) => {
|
||||||
|
const [field, direction] = value.split('-');
|
||||||
setSortBy(field as typeof sortBy);
|
setSortBy(field as typeof sortBy);
|
||||||
setSortDirection(direction as 'asc' | 'desc');
|
setSortDirection(direction as 'asc' | 'desc');
|
||||||
}}
|
}}
|
||||||
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
<option value="created_at-desc">Newest First</option>
|
|
||||||
<option value="created_at-asc">Oldest First</option>
|
|
||||||
<option value="updated_at-desc">Recently Updated</option>
|
|
||||||
<option value="title-asc">Title A-Z</option>
|
|
||||||
<option value="title-desc">Title Z-A</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -263,9 +263,11 @@ export default function SiteDashboard() {
|
|||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<PageIcon className="h-6 w-6" />
|
<PageIcon className="h-6 w-6" />
|
||||||
@@ -275,11 +277,13 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">View and edit pages</p>
|
<p className="text-sm text-gray-600">View and edit pages</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/content`)}
|
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<FileIcon className="h-6 w-6" />
|
<FileIcon className="h-6 w-6" />
|
||||||
@@ -289,11 +293,13 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">View and edit content</p>
|
<p className="text-sm text-gray-600">View and edit content</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<PlugInIcon className="h-6 w-6" />
|
<PlugInIcon className="h-6 w-6" />
|
||||||
@@ -303,11 +309,13 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">Manage connections</p>
|
<p className="text-sm text-gray-600">Manage connections</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<BoltIcon className="h-6 w-6" />
|
<BoltIcon className="h-6 w-6" />
|
||||||
@@ -317,11 +325,13 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">View sync status</p>
|
<p className="text-sm text-gray-600">View sync status</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<ArrowUpIcon className="h-6 w-6" />
|
<ArrowUpIcon className="h-6 w-6" />
|
||||||
@@ -331,11 +341,13 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">Deploy to production</p>
|
<p className="text-sm text-gray-600">Deploy to production</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(`/sites/${siteId}/publishing-queue`)}
|
onClick={() => navigate(`/sites/${siteId}/publishing-queue`)}
|
||||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group"
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group h-auto justify-start"
|
||||||
>
|
>
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
|
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
|
||||||
<ClockIcon className="h-6 w-6" />
|
<ClockIcon className="h-6 w-6" />
|
||||||
@@ -345,7 +357,7 @@ export default function SiteDashboard() {
|
|||||||
<p className="text-sm text-gray-600">View scheduled content</p>
|
<p className="text-sm text-gray-600">View scheduled content</p>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
|
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ComponentCard>
|
</ComponentCard>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import Button from '../../components/ui/button/Button';
|
|||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import Alert from '../../components/ui/alert/Alert';
|
import Alert from '../../components/ui/alert/Alert';
|
||||||
import Switch from '../../components/form/switch/Switch';
|
import Switch from '../../components/form/switch/Switch';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
import ViewToggle from '../../components/common/ViewToggle';
|
import ViewToggle from '../../components/common/ViewToggle';
|
||||||
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
|
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
|
||||||
import {
|
import {
|
||||||
@@ -605,40 +607,38 @@ export default function SiteList() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-nowrap gap-3 items-center justify-between w-full">
|
<div className="flex flex-nowrap gap-3 items-center justify-between w-full">
|
||||||
<div className="flex flex-nowrap gap-3 items-center flex-1 min-w-0 w-full">
|
<div className="flex flex-nowrap gap-3 items-center flex-1 min-w-0 w-full">
|
||||||
<input
|
<div className="flex-1 min-w-[200px]">
|
||||||
type="text"
|
<InputField
|
||||||
placeholder="Search sites..."
|
type="text"
|
||||||
value={searchTerm}
|
placeholder="Search sites..."
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
value={searchTerm}
|
||||||
className="flex-1 min-w-[200px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<select
|
</div>
|
||||||
value={siteTypeFilter}
|
<div className="flex-1 min-w-[140px]">
|
||||||
onChange={(e) => setSiteTypeFilter(e.target.value)}
|
<Select
|
||||||
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
options={SITE_TYPES}
|
||||||
>
|
placeholder="Show All Types"
|
||||||
{SITE_TYPES.map(opt => (
|
defaultValue={siteTypeFilter}
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
onChange={(val) => setSiteTypeFilter(val)}
|
||||||
))}
|
/>
|
||||||
</select>
|
</div>
|
||||||
<select
|
<div className="flex-1 min-w-[140px]">
|
||||||
value={hostingTypeFilter}
|
<Select
|
||||||
onChange={(e) => setHostingTypeFilter(e.target.value)}
|
options={HOSTING_TYPES}
|
||||||
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
placeholder="Show All Hosting"
|
||||||
>
|
defaultValue={hostingTypeFilter}
|
||||||
{HOSTING_TYPES.map(opt => (
|
onChange={(val) => setHostingTypeFilter(val)}
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
/>
|
||||||
))}
|
</div>
|
||||||
</select>
|
<div className="flex-1 min-w-[140px]">
|
||||||
<select
|
<Select
|
||||||
value={statusFilter}
|
options={STATUS_OPTIONS}
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
placeholder="Show All Status"
|
||||||
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
defaultValue={statusFilter}
|
||||||
>
|
onChange={(val) => setStatusFilter(val)}
|
||||||
{STATUS_OPTIONS.map(opt => (
|
/>
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
</div>
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ const DraggablePageItem: React.FC<{
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect(page.id);
|
onSelect(page.id);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
aria-label={isSelected ? 'Deselect page' : 'Select page'}
|
||||||
>
|
>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
<CheckLineIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
<CheckLineIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||||
@@ -365,8 +366,9 @@ export default function PageManager() {
|
|||||||
|
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={handleSelectAll}
|
onClick={handleSelectAll}
|
||||||
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
@@ -376,7 +378,7 @@ export default function PageManager() {
|
|||||||
<div className="w-5 h-5 border-2 border-gray-400 rounded" />
|
<div className="w-5 h-5 border-2 border-gray-400 rounded" />
|
||||||
)}
|
)}
|
||||||
<span>Select All</span>
|
<span>Select All</span>
|
||||||
</button>
|
</Button>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Drag and drop to reorder pages
|
Drag and drop to reorder pages
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Card } from '../../components/ui/card';
|
|||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import Label from '../../components/form/Label';
|
import Label from '../../components/form/Label';
|
||||||
import TextArea from '../../components/form/input/TextArea';
|
import TextArea from '../../components/form/input/TextArea';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
import SelectDropdown from '../../components/form/SelectDropdown';
|
import SelectDropdown from '../../components/form/SelectDropdown';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
|
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
|
||||||
@@ -266,51 +267,39 @@ export default function PostEditor() {
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'content' ? 'primary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('content')}
|
onClick={() => setActiveTab('content')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'content'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FileTextIcon className="w-4 h-4 inline mr-2" />
|
<FileTextIcon className="w-4 h-4" />
|
||||||
Content
|
Content
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'taxonomy' ? 'primary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('taxonomy')}
|
onClick={() => setActiveTab('taxonomy')}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'taxonomy'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<TagIcon className="w-4 h-4 inline mr-2" />
|
<TagIcon className="w-4 h-4" />
|
||||||
Taxonomy & Cluster
|
Taxonomy & Cluster
|
||||||
</button>
|
</Button>
|
||||||
{content.id && (
|
{content.id && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === 'validation' ? 'primary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('validation');
|
setActiveTab('validation');
|
||||||
loadValidation();
|
loadValidation();
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === 'validation'
|
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<CheckCircleIcon className="w-4 h-4 inline mr-2" />
|
<CheckCircleIcon className="w-4 h-4" />
|
||||||
Validation
|
Validation
|
||||||
{validationResult && !validationResult.is_valid && (
|
{validationResult && !validationResult.is_valid && (
|
||||||
<span className="ml-2 px-2 py-0.5 text-xs bg-error-100 dark:bg-error-900 text-error-600 dark:text-error-400 rounded-full">
|
<span className="ml-2 px-2 py-0.5 text-xs bg-error-100 dark:bg-error-900 text-error-600 dark:text-error-400 rounded-full">
|
||||||
{validationResult.validation_errors.length}
|
{validationResult.validation_errors.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,12 +311,12 @@ export default function PostEditor() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Title *</Label>
|
<Label>Title *</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={content.title}
|
value={content.title}
|
||||||
onChange={(e) => setContent({ ...content, title: e.target.value })}
|
onChange={(e) => setContent({ ...content, title: e.target.value })}
|
||||||
placeholder="Enter post title"
|
placeholder="Enter post title"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import PageHeader from '../../components/common/PageHeader';
|
|||||||
import ComponentCard from '../../components/common/ComponentCard';
|
import ComponentCard from '../../components/common/ComponentCard';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import IconButton from '../../components/ui/button/IconButton';
|
||||||
|
import { ButtonGroup, ButtonGroupItem } from '../../components/ui/button-group/ButtonGroup';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { fetchContent, Content } from '../../services/api';
|
import { fetchContent, Content } from '../../services/api';
|
||||||
import {
|
import {
|
||||||
@@ -277,30 +279,22 @@ export default function PublishingQueue() {
|
|||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{queueItems.length} items in queue
|
{queueItems.length} items in queue
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2 bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
<ButtonGroup>
|
||||||
<button
|
<ButtonGroupItem
|
||||||
|
isActive={viewMode === 'list'}
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => setViewMode('list')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
viewMode === 'list'
|
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<ListIcon className="w-4 h-4" />
|
<ListIcon className="w-4 h-4 mr-1.5" />
|
||||||
List
|
List
|
||||||
</button>
|
</ButtonGroupItem>
|
||||||
<button
|
<ButtonGroupItem
|
||||||
|
isActive={viewMode === 'calendar'}
|
||||||
onClick={() => setViewMode('calendar')}
|
onClick={() => setViewMode('calendar')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
viewMode === 'calendar'
|
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<CalendarIcon className="w-4 h-4" />
|
<CalendarIcon className="w-4 h-4 mr-1.5" />
|
||||||
Calendar
|
Calendar
|
||||||
</button>
|
</ButtonGroupItem>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Queue Content */}
|
{/* Queue Content */}
|
||||||
@@ -361,27 +355,30 @@ export default function PublishingQueue() {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<IconButton
|
||||||
|
icon={<EyeIcon className="w-4 h-4" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleViewContent(item)}
|
onClick={() => handleViewContent(item)}
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
title="View content"
|
title="View content"
|
||||||
>
|
/>
|
||||||
<EyeIcon className="w-4 h-4" />
|
<IconButton
|
||||||
</button>
|
icon={item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
|
||||||
<button
|
variant="ghost"
|
||||||
|
tone="neutral"
|
||||||
|
size="sm"
|
||||||
onClick={() => handlePauseItem(item)}
|
onClick={() => handlePauseItem(item)}
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
title={item.isPaused ? 'Resume' : 'Pause'}
|
title={item.isPaused ? 'Resume' : 'Pause'}
|
||||||
>
|
/>
|
||||||
{item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
|
<IconButton
|
||||||
</button>
|
icon={<TrashBinIcon className="w-4 h-4" />}
|
||||||
<button
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
onClick={() => handleRemoveFromQueue(item)}
|
onClick={() => handleRemoveFromQueue(item)}
|
||||||
className="p-2 text-error-500 hover:text-error-700 dark:text-error-400 dark:hover:text-error-300 rounded-lg hover:bg-error-50 dark:hover:bg-error-900/20"
|
|
||||||
title="Remove from queue"
|
title="Remove from queue"
|
||||||
>
|
/>
|
||||||
<TrashBinIcon className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import PageMeta from '../../components/common/PageMeta';
|
|||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import IconButton from '../../components/ui/button/IconButton';
|
||||||
import Label from '../../components/form/Label';
|
import Label from '../../components/form/Label';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
import SelectDropdown from '../../components/form/SelectDropdown';
|
import SelectDropdown from '../../components/form/SelectDropdown';
|
||||||
import Checkbox from '../../components/form/input/Checkbox';
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
import TextArea from '../../components/form/input/TextArea';
|
import TextArea from '../../components/form/input/TextArea';
|
||||||
|
import Switch from '../../components/form/switch/Switch';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import {
|
import {
|
||||||
fetchAPI,
|
fetchAPI,
|
||||||
@@ -23,7 +27,7 @@ import {
|
|||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
|
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '../../icons';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
|
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
|
||||||
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
|
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
|
||||||
@@ -554,10 +558,11 @@ export default function SiteSettings() {
|
|||||||
{/* Site Selector - Only show if more than 1 site */}
|
{/* Site Selector - Only show if more than 1 site */}
|
||||||
{!sitesLoading && sites.length > 1 && (
|
{!sitesLoading && sites.length > 1 && (
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
<button
|
<Button
|
||||||
ref={siteSelectorRef}
|
ref={siteSelectorRef}
|
||||||
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
|
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-brand-200 rounded-lg hover:bg-brand-50 hover:border-brand-300 dark:bg-gray-800 dark:text-gray-300 dark:border-brand-700/50 dark:hover:bg-brand-500/10 dark:hover:border-brand-600/50 transition-colors"
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
aria-label="Switch site"
|
aria-label="Switch site"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
@@ -565,7 +570,7 @@ export default function SiteSettings() {
|
|||||||
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
|
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
|
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
|
||||||
</button>
|
</Button>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isSiteSelectorOpen}
|
isOpen={isSiteSelectorOpen}
|
||||||
onClose={() => setIsSiteSelectorOpen(false)}
|
onClose={() => setIsSiteSelectorOpen(false)}
|
||||||
@@ -605,13 +610,13 @@ export default function SiteSettings() {
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('general');
|
setActiveTab('general');
|
||||||
navigate(`/sites/${siteId}/settings`, { replace: true });
|
navigate(`/sites/${siteId}/settings`, { replace: true });
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'general'
|
activeTab === 'general'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -619,14 +624,14 @@ export default function SiteSettings() {
|
|||||||
>
|
>
|
||||||
<GridIcon className="w-4 h-4 inline mr-2" />
|
<GridIcon className="w-4 h-4 inline mr-2" />
|
||||||
General
|
General
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('integrations');
|
setActiveTab('integrations');
|
||||||
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
|
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'integrations'
|
activeTab === 'integrations'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -634,14 +639,14 @@ export default function SiteSettings() {
|
|||||||
>
|
>
|
||||||
<PlugInIcon className="w-4 h-4 inline mr-2" />
|
<PlugInIcon className="w-4 h-4 inline mr-2" />
|
||||||
Integrations
|
Integrations
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('publishing');
|
setActiveTab('publishing');
|
||||||
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
|
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'publishing'
|
activeTab === 'publishing'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -649,15 +654,15 @@ export default function SiteSettings() {
|
|||||||
>
|
>
|
||||||
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
|
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
|
||||||
Publishing
|
Publishing
|
||||||
</button>
|
</Button>
|
||||||
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
|
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('content-types');
|
setActiveTab('content-types');
|
||||||
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
|
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||||
activeTab === 'content-types'
|
activeTab === 'content-types'
|
||||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
@@ -665,7 +670,7 @@ export default function SiteSettings() {
|
|||||||
>
|
>
|
||||||
<FileIcon className="w-4 h-4 inline mr-2" />
|
<FileIcon className="w-4 h-4 inline mr-2" />
|
||||||
Content Types
|
Content Types
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -696,17 +701,15 @@ export default function SiteSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<Switch
|
||||||
type="checkbox"
|
label=""
|
||||||
checked={publishingSettings.auto_approval_enabled}
|
checked={publishingSettings.auto_approval_enabled}
|
||||||
onChange={(e) => {
|
onChange={(checked) => {
|
||||||
const newSettings = { ...publishingSettings, auto_approval_enabled: e.target.checked };
|
const newSettings = { ...publishingSettings, auto_approval_enabled: checked };
|
||||||
setPublishingSettings(newSettings);
|
setPublishingSettings(newSettings);
|
||||||
savePublishingSettings({ auto_approval_enabled: e.target.checked });
|
savePublishingSettings({ auto_approval_enabled: checked });
|
||||||
}}
|
}}
|
||||||
className="sr-only peer"
|
|
||||||
/>
|
/>
|
||||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -721,17 +724,15 @@ export default function SiteSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<Switch
|
||||||
type="checkbox"
|
label=""
|
||||||
checked={publishingSettings.auto_publish_enabled}
|
checked={publishingSettings.auto_publish_enabled}
|
||||||
onChange={(e) => {
|
onChange={(checked) => {
|
||||||
const newSettings = { ...publishingSettings, auto_publish_enabled: e.target.checked };
|
const newSettings = { ...publishingSettings, auto_publish_enabled: checked };
|
||||||
setPublishingSettings(newSettings);
|
setPublishingSettings(newSettings);
|
||||||
savePublishingSettings({ auto_publish_enabled: e.target.checked });
|
savePublishingSettings({ auto_publish_enabled: checked });
|
||||||
}}
|
}}
|
||||||
className="sr-only peer"
|
|
||||||
/>
|
/>
|
||||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -745,7 +746,7 @@ export default function SiteSettings() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Daily Limit</Label>
|
<Label>Daily Limit</Label>
|
||||||
<input
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="50"
|
max="50"
|
||||||
@@ -754,14 +755,12 @@ export default function SiteSettings() {
|
|||||||
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
|
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
|
||||||
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
|
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
|
||||||
}}
|
}}
|
||||||
onBlur={() => savePublishingSettings({ daily_publish_limit: publishingSettings.daily_publish_limit })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
|
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Weekly Limit</Label>
|
<Label>Weekly Limit</Label>
|
||||||
<input
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="200"
|
max="200"
|
||||||
@@ -770,14 +769,12 @@ export default function SiteSettings() {
|
|||||||
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
|
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
|
||||||
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
|
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
|
||||||
}}
|
}}
|
||||||
onBlur={() => savePublishingSettings({ weekly_publish_limit: publishingSettings.weekly_publish_limit })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
|
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Monthly Limit</Label>
|
<Label>Monthly Limit</Label>
|
||||||
<input
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="500"
|
max="500"
|
||||||
@@ -786,8 +783,6 @@ export default function SiteSettings() {
|
|||||||
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
|
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
|
||||||
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
|
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
|
||||||
}}
|
}}
|
||||||
onBlur={() => savePublishingSettings({ monthly_publish_limit: publishingSettings.monthly_publish_limit })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
|
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -810,9 +805,11 @@ export default function SiteSettings() {
|
|||||||
{ value: 'sat', label: 'Sat' },
|
{ value: 'sat', label: 'Sat' },
|
||||||
{ value: 'sun', label: 'Sun' },
|
{ value: 'sun', label: 'Sun' },
|
||||||
].map((day) => (
|
].map((day) => (
|
||||||
<button
|
<Button
|
||||||
key={day.value}
|
key={day.value}
|
||||||
type="button"
|
variant={(publishingSettings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentDays = publishingSettings.publish_days || [];
|
const currentDays = publishingSettings.publish_days || [];
|
||||||
const newDays = currentDays.includes(day.value)
|
const newDays = currentDays.includes(day.value)
|
||||||
@@ -821,14 +818,9 @@ export default function SiteSettings() {
|
|||||||
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
|
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
|
||||||
savePublishingSettings({ publish_days: newDays });
|
savePublishingSettings({ publish_days: newDays });
|
||||||
}}
|
}}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
||||||
(publishingSettings.publish_days || []).includes(day.value)
|
|
||||||
? 'bg-brand-600 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{day.label}
|
{day.label}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -842,7 +834,7 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
|
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<input
|
<InputField
|
||||||
type="time"
|
type="time"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -850,40 +842,36 @@ export default function SiteSettings() {
|
|||||||
newSlots[index] = e.target.value;
|
newSlots[index] = e.target.value;
|
||||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||||
}}
|
}}
|
||||||
onBlur={() => savePublishingSettings({ publish_time_slots: publishingSettings.publish_time_slots })}
|
|
||||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
|
|
||||||
/>
|
/>
|
||||||
{(publishingSettings.publish_time_slots || []).length > 1 && (
|
{(publishingSettings.publish_time_slots || []).length > 1 && (
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
icon={<CloseIcon className="w-5 h-5" />}
|
||||||
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
size="sm"
|
||||||
|
title="Remove time slot"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
|
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
|
||||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||||
savePublishingSettings({ publish_time_slots: newSlots });
|
savePublishingSettings({ publish_time_slots: newSlots });
|
||||||
}}
|
}}
|
||||||
className="p-2 text-gray-400 hover:text-error-500 transition-colors"
|
/>
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
|
tone="brand"
|
||||||
|
size="sm"
|
||||||
|
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
|
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
|
||||||
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
||||||
savePublishingSettings({ publish_time_slots: newSlots });
|
savePublishingSettings({ publish_time_slots: newSlots });
|
||||||
}}
|
}}
|
||||||
className="text-sm text-brand-600 hover:text-brand-700 font-medium flex items-center gap-1"
|
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
Add Time Slot
|
Add Time Slot
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1084,32 +1072,29 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Site Name</Label>
|
<Label>Site Name</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Slug</Label>
|
<Label>Slug</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.slug}
|
value={formData.slug}
|
||||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Site URL</Label>
|
<Label>Site URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.site_url}
|
value={formData.site_url}
|
||||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1134,7 +1119,7 @@ export default function SiteSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={formData.is_active}
|
checked={formData.is_active}
|
||||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
onChange={(checked) => setFormData({ ...formData, is_active: checked })}
|
||||||
label="Active"
|
label="Active"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1150,13 +1135,12 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Meta Title</Label>
|
<Label>Meta Title</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.meta_title}
|
value={formData.meta_title}
|
||||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||||
placeholder="SEO title (recommended: 50-60 characters)"
|
placeholder="SEO title (recommended: 50-60 characters)"
|
||||||
maxLength={60}
|
max="60"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{formData.meta_title.length}/60 characters
|
{formData.meta_title.length}/60 characters
|
||||||
@@ -1180,12 +1164,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Meta Keywords (comma-separated)</Label>
|
<Label>Meta Keywords (comma-separated)</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.meta_keywords}
|
value={formData.meta_keywords}
|
||||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||||
placeholder="keyword1, keyword2, keyword3"
|
placeholder="keyword1, keyword2, keyword3"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Separate keywords with commas
|
Separate keywords with commas
|
||||||
@@ -1203,12 +1186,11 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Title</Label>
|
<Label>OG Title</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.og_title}
|
value={formData.og_title}
|
||||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||||
placeholder="Open Graph title"
|
placeholder="Open Graph title"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1225,12 +1207,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Image URL</Label>
|
<Label>OG Image URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.og_image}
|
value={formData.og_image}
|
||||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||||
placeholder="https://example.com/image.jpg"
|
placeholder="https://example.com/image.jpg"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Recommended: 1200x630px image
|
Recommended: 1200x630px image
|
||||||
@@ -1253,12 +1234,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Site Name</Label>
|
<Label>OG Site Name</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.og_site_name}
|
value={formData.og_site_name}
|
||||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||||
placeholder="Site name for social sharing"
|
placeholder="Site name for social sharing"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1288,12 +1268,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema Name</Label>
|
<Label>Schema Name</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.schema_name}
|
value={formData.schema_name}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||||
placeholder="Organization name"
|
placeholder="Organization name"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1310,34 +1289,31 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema URL</Label>
|
<Label>Schema URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.schema_url}
|
value={formData.schema_url}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema Logo URL</Label>
|
<Label>Schema Logo URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.schema_logo}
|
value={formData.schema_logo}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||||
placeholder="https://example.com/logo.png"
|
placeholder="https://example.com/logo.png"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Same As URLs (comma-separated)</Label>
|
<Label>Same As URLs (comma-separated)</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.schema_same_as}
|
value={formData.schema_same_as}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
||||||
placeholder="https://facebook.com/page, https://twitter.com/page"
|
placeholder="https://facebook.com/page, https://twitter.com/page"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Social media profiles and other related URLs
|
Social media profiles and other related URLs
|
||||||
@@ -1359,21 +1335,18 @@ export default function SiteSettings() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Select Industry
|
Select Industry
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={selectedIndustry}
|
options={industries.map((industry) => ({
|
||||||
onChange={(e) => {
|
value: industry.slug,
|
||||||
setSelectedIndustry(e.target.value);
|
label: industry.name,
|
||||||
|
}))}
|
||||||
|
placeholder="Select an industry..."
|
||||||
|
defaultValue={selectedIndustry}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedIndustry(value);
|
||||||
setSelectedSectors([]);
|
setSelectedSectors([]);
|
||||||
}}
|
}}
|
||||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
/>
|
||||||
>
|
|
||||||
<option value="">Select an industry...</option>
|
|
||||||
{industries.map((industry) => (
|
|
||||||
<option key={industry.slug} value={industry.slug}>
|
|
||||||
{industry.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{selectedIndustry && (
|
{selectedIndustry && (
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||||
@@ -1388,15 +1361,14 @@ export default function SiteSettings() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||||
{getIndustrySectors().map((sector) => (
|
{getIndustrySectors().map((sector) => (
|
||||||
<label
|
<div
|
||||||
key={sector.slug}
|
key={sector.slug}
|
||||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
>
|
>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={selectedSectors.includes(sector.slug)}
|
checked={selectedSectors.includes(sector.slug)}
|
||||||
onChange={(e) => {
|
onChange={(checked) => {
|
||||||
if (e.target.checked) {
|
if (checked) {
|
||||||
if (selectedSectors.length >= 5) {
|
if (selectedSectors.length >= 5) {
|
||||||
toast.error('Maximum 5 sectors allowed per site');
|
toast.error('Maximum 5 sectors allowed per site');
|
||||||
return;
|
return;
|
||||||
@@ -1406,7 +1378,6 @@ export default function SiteSettings() {
|
|||||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||||
@@ -1416,7 +1387,7 @@ export default function SiteSettings() {
|
|||||||
{sector.description}
|
{sector.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
@@ -1447,13 +1418,12 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Meta Title</Label>
|
<Label>Meta Title</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.meta_title}
|
value={formData.meta_title}
|
||||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||||
placeholder="SEO title (recommended: 50-60 characters)"
|
placeholder="SEO title (recommended: 50-60 characters)"
|
||||||
maxLength={60}
|
max="60"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{formData.meta_title.length}/60 characters
|
{formData.meta_title.length}/60 characters
|
||||||
@@ -1477,12 +1447,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Meta Keywords (comma-separated)</Label>
|
<Label>Meta Keywords (comma-separated)</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.meta_keywords}
|
value={formData.meta_keywords}
|
||||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||||
placeholder="keyword1, keyword2, keyword3"
|
placeholder="keyword1, keyword2, keyword3"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Separate keywords with commas
|
Separate keywords with commas
|
||||||
@@ -1498,12 +1467,11 @@ export default function SiteSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Title</Label>
|
<Label>OG Title</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.og_title}
|
value={formData.og_title}
|
||||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||||
placeholder="Open Graph title"
|
placeholder="Open Graph title"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1520,12 +1488,11 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Image URL</Label>
|
<Label>OG Image URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.og_image}
|
value={formData.og_image}
|
||||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||||
placeholder="https://example.com/image.jpg"
|
placeholder="https://example.com/image.jpg"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Recommended: 1200x630px image
|
Recommended: 1200x630px image
|
||||||
@@ -1542,18 +1509,17 @@ export default function SiteSettings() {
|
|||||||
{ value: 'product', label: 'Product' },
|
{ value: 'product', label: 'Product' },
|
||||||
]}
|
]}
|
||||||
value={formData.og_type}
|
value={formData.og_type}
|
||||||
onChange={(e) => setFormData({ ...formData, og_type: e.target.value })}
|
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>OG Site Name</Label>
|
<Label>OG Site Name</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.og_site_name}
|
value={formData.og_site_name}
|
||||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||||
placeholder="Site name for social sharing"
|
placeholder="Site name for social sharing"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1575,18 +1541,17 @@ export default function SiteSettings() {
|
|||||||
{ value: 'NGO', label: 'NGO' },
|
{ value: 'NGO', label: 'NGO' },
|
||||||
]}
|
]}
|
||||||
value={formData.schema_type}
|
value={formData.schema_type}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_type: e.target.value })}
|
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema Name</Label>
|
<Label>Schema Name</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.schema_name}
|
value={formData.schema_name}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||||
placeholder="Organization name"
|
placeholder="Organization name"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1603,34 +1568,31 @@ export default function SiteSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema URL</Label>
|
<Label>Schema URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.schema_url}
|
value={formData.schema_url}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Schema Logo URL</Label>
|
<Label>Schema Logo URL</Label>
|
||||||
<input
|
<InputField
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.schema_logo}
|
value={formData.schema_logo}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||||
placeholder="https://example.com/logo.png"
|
placeholder="https://example.com/logo.png"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Same As URLs (comma-separated)</Label>
|
<Label>Same As URLs (comma-separated)</Label>
|
||||||
<input
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.schema_same_as}
|
value={formData.schema_same_as}
|
||||||
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
||||||
placeholder="https://facebook.com/page, https://twitter.com/page"
|
placeholder="https://facebook.com/page, https://twitter.com/page"
|
||||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Social media profiles and other related URLs
|
Social media profiles and other related URLs
|
||||||
|
|||||||
@@ -287,9 +287,10 @@ export default function SyncDashboard() {
|
|||||||
{/* Mismatches Section */}
|
{/* Mismatches Section */}
|
||||||
{totalMismatches > 0 && (
|
{totalMismatches > 0 && (
|
||||||
<Card className="p-6 mb-6">
|
<Card className="p-6 mb-6">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setShowMismatches(!showMismatches)}
|
onClick={() => setShowMismatches(!showMismatches)}
|
||||||
className="w-full flex items-center justify-between mb-4"
|
variant="ghost"
|
||||||
|
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
Mismatches ({totalMismatches})
|
Mismatches ({totalMismatches})
|
||||||
@@ -299,7 +300,7 @@ export default function SyncDashboard() {
|
|||||||
) : (
|
) : (
|
||||||
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
|
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{showMismatches && mismatches && (
|
{showMismatches && mismatches && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -412,9 +413,10 @@ export default function SyncDashboard() {
|
|||||||
|
|
||||||
{/* Sync Logs */}
|
{/* Sync Logs */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setShowLogs(!showLogs)}
|
onClick={() => setShowLogs(!showLogs)}
|
||||||
className="w-full flex items-center justify-between mb-4"
|
variant="ghost"
|
||||||
|
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
Sync History ({logs.length})
|
Sync History ({logs.length})
|
||||||
@@ -424,7 +426,7 @@ export default function SyncDashboard() {
|
|||||||
) : (
|
) : (
|
||||||
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
|
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{showLogs && (
|
{showLogs && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
737
frontend/src/pages/UIElements.tsx
Normal file
737
frontend/src/pages/UIElements.tsx
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
/**
|
||||||
|
* UI Elements Showcase Page
|
||||||
|
*
|
||||||
|
* Single source of truth for all UI components from components/ui/
|
||||||
|
* This page is non-indexable and serves as documentation/reference
|
||||||
|
*
|
||||||
|
* Route: /ui-elements
|
||||||
|
*/
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PageMeta from '../components/common/PageMeta';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UI COMPONENTS - All imports from components/ui/ (SINGLE SOURCE OF TRUTH)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Accordion
|
||||||
|
import { Accordion, AccordionItem } from '../components/ui/accordion/Accordion';
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
import Alert from '../components/ui/alert/Alert';
|
||||||
|
import AlertModal from '../components/ui/alert/AlertModal';
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
import Avatar from '../components/ui/avatar/Avatar';
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
import Badge from '../components/ui/badge/Badge';
|
||||||
|
|
||||||
|
// Breadcrumb
|
||||||
|
import { Breadcrumb } from '../components/ui/breadcrumb/Breadcrumb';
|
||||||
|
|
||||||
|
// Button
|
||||||
|
import Button from '../components/ui/button/Button';
|
||||||
|
import ButtonWithTooltip from '../components/ui/button/ButtonWithTooltip';
|
||||||
|
import IconButton from '../components/ui/button/IconButton';
|
||||||
|
|
||||||
|
// Button Group
|
||||||
|
import { ButtonGroup, ButtonGroupItem } from '../components/ui/button-group/ButtonGroup';
|
||||||
|
|
||||||
|
// Card
|
||||||
|
import { Card, CardImage, CardTitle, CardContent, CardDescription, CardAction, CardIcon, HorizontalCard } from '../components/ui/card/Card';
|
||||||
|
|
||||||
|
// DataView
|
||||||
|
import { DataView, DataViewHeader, DataViewToolbar, DataViewEmptyState } from '../components/ui/dataview/DataView';
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
||||||
|
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
||||||
|
|
||||||
|
// List
|
||||||
|
import { List, ListItem, ListDot, ListIcon, ListCheckboxItem, ListRadioItem } from '../components/ui/list/List';
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
import { Modal } from '../components/ui/modal';
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
import { Pagination } from '../components/ui/pagination/Pagination';
|
||||||
|
import { CompactPagination } from '../components/ui/pagination/CompactPagination';
|
||||||
|
|
||||||
|
// Progress
|
||||||
|
import { ProgressBar } from '../components/ui/progress/ProgressBar';
|
||||||
|
|
||||||
|
// Ribbon
|
||||||
|
import { Ribbon } from '../components/ui/ribbon/Ribbon';
|
||||||
|
|
||||||
|
// Spinner
|
||||||
|
import { Spinner } from '../components/ui/spinner/Spinner';
|
||||||
|
|
||||||
|
// Table
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableCell } from '../components/ui/table';
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
import { Tabs, TabList, Tab, TabPanel } from '../components/ui/tabs/Tabs';
|
||||||
|
|
||||||
|
// Toast
|
||||||
|
import { useToast } from '../components/ui/toast/ToastContainer';
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
import { Tooltip } from '../components/ui/tooltip/Tooltip';
|
||||||
|
import { EnhancedTooltip } from '../components/ui/tooltip/EnhancedTooltip';
|
||||||
|
|
||||||
|
// Icons for demos
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
CloseIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
GridIcon,
|
||||||
|
FileIcon,
|
||||||
|
BoltIcon,
|
||||||
|
} from '../icons';
|
||||||
|
|
||||||
|
// Component categories for navigation
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: 'buttons', label: 'Buttons' },
|
||||||
|
{ id: 'badges', label: 'Badges' },
|
||||||
|
{ id: 'cards', label: 'Cards' },
|
||||||
|
{ id: 'alerts', label: 'Alerts' },
|
||||||
|
{ id: 'modals', label: 'Modals' },
|
||||||
|
{ id: 'tables', label: 'Tables' },
|
||||||
|
{ id: 'tabs', label: 'Tabs' },
|
||||||
|
{ id: 'accordion', label: 'Accordion' },
|
||||||
|
{ id: 'dropdown', label: 'Dropdown' },
|
||||||
|
{ id: 'pagination', label: 'Pagination' },
|
||||||
|
{ id: 'progress', label: 'Progress' },
|
||||||
|
{ id: 'spinner', label: 'Spinner' },
|
||||||
|
{ id: 'avatar', label: 'Avatar' },
|
||||||
|
{ id: 'breadcrumb', label: 'Breadcrumb' },
|
||||||
|
{ id: 'list', label: 'List' },
|
||||||
|
{ id: 'tooltip', label: 'Tooltip' },
|
||||||
|
{ id: 'ribbon', label: 'Ribbon' },
|
||||||
|
{ id: 'toast', label: 'Toast' },
|
||||||
|
{ id: 'dataview', label: 'DataView' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Section wrapper component
|
||||||
|
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section id={id} className="mb-12 scroll-mt-24">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo box component
|
||||||
|
function DemoBox({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||||
|
<p className="text-xs font-mono text-gray-500 dark:text-gray-400 mb-3">{label}</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UIElements() {
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Modal states
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showAlertModal, setShowAlertModal] = useState(false);
|
||||||
|
|
||||||
|
// Dropdown state
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
// Tabs state
|
||||||
|
const [activeTab, setActiveTab] = useState('tab1');
|
||||||
|
|
||||||
|
// List state
|
||||||
|
const [checkboxChecked, setCheckboxChecked] = useState(false);
|
||||||
|
const [radioValue, setRadioValue] = useState('option1');
|
||||||
|
|
||||||
|
// Button group state
|
||||||
|
const [activeButton, setActiveButton] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageMeta
|
||||||
|
title="UI Elements | IGNY8 Design System"
|
||||||
|
description="Component library reference - Single source of truth for all UI components"
|
||||||
|
noIndex={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-8">
|
||||||
|
{/* Sticky Navigation */}
|
||||||
|
<nav className="hidden lg:block w-48 shrink-0">
|
||||||
|
<div className="sticky top-24 space-y-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">Components</h3>
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<a
|
||||||
|
key={cat.id}
|
||||||
|
href={`#${cat.id}`}
|
||||||
|
className="block py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
UI Elements Library
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Single source of truth for all UI components. All components are imported from <code className="text-sm bg-gray-100 dark:bg-gray-800 px-1 rounded">components/ui/</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* BUTTONS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="buttons" title="Button">
|
||||||
|
<DemoBox label="variants: primary | secondary | outline | ghost | gradient">
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="outline">Outline</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
<Button variant="gradient">Gradient</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="tones: brand | success | warning | danger | neutral">
|
||||||
|
<Button tone="brand">Brand</Button>
|
||||||
|
<Button tone="success">Success</Button>
|
||||||
|
<Button tone="warning">Warning</Button>
|
||||||
|
<Button tone="danger">Danger</Button>
|
||||||
|
<Button tone="neutral">Neutral</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="sizes: xs | sm | md | lg | xl | 2xl">
|
||||||
|
<Button size="xs">XS</Button>
|
||||||
|
<Button size="sm">SM</Button>
|
||||||
|
<Button size="md">MD</Button>
|
||||||
|
<Button size="lg">LG</Button>
|
||||||
|
<Button size="xl">XL</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="shapes: rounded | pill">
|
||||||
|
<Button shape="rounded">Rounded</Button>
|
||||||
|
<Button shape="pill">Pill</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="with icons: startIcon | endIcon">
|
||||||
|
<Button startIcon={<PlusIcon className="w-4 h-4" />}>Add Item</Button>
|
||||||
|
<Button endIcon={<ArrowRightIcon className="w-4 h-4" />}>Continue</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="states: disabled | fullWidth">
|
||||||
|
<Button disabled>Disabled</Button>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="ButtonWithTooltip (shows tooltip when disabled)">
|
||||||
|
<ButtonWithTooltip disabled tooltip="This action is not available">
|
||||||
|
Disabled with Tooltip
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="ButtonGroup">
|
||||||
|
<ButtonGroup>
|
||||||
|
<ButtonGroupItem isActive={activeButton === 0} onClick={() => setActiveButton(0)}>Day</ButtonGroupItem>
|
||||||
|
<ButtonGroupItem isActive={activeButton === 1} onClick={() => setActiveButton(1)}>Week</ButtonGroupItem>
|
||||||
|
<ButtonGroupItem isActive={activeButton === 2} onClick={() => setActiveButton(2)}>Month</ButtonGroupItem>
|
||||||
|
</ButtonGroup>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="IconButton - variants: solid | outline | ghost">
|
||||||
|
<IconButton icon={<PlusIcon />} variant="solid" tone="brand" title="Add" />
|
||||||
|
<IconButton icon={<PlusIcon />} variant="outline" tone="brand" title="Add" />
|
||||||
|
<IconButton icon={<PlusIcon />} variant="ghost" tone="brand" title="Add" />
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="IconButton - sizes: xs | sm | md | lg">
|
||||||
|
<IconButton icon={<CloseIcon />} size="xs" title="Close" />
|
||||||
|
<IconButton icon={<CloseIcon />} size="sm" title="Close" />
|
||||||
|
<IconButton icon={<CloseIcon />} size="md" title="Close" />
|
||||||
|
<IconButton icon={<CloseIcon />} size="lg" title="Close" />
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="IconButton - shapes: rounded | circle">
|
||||||
|
<IconButton icon={<CheckCircleIcon />} shape="rounded" variant="solid" tone="success" title="Approve" />
|
||||||
|
<IconButton icon={<CheckCircleIcon />} shape="circle" variant="solid" tone="success" title="Approve" />
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="IconButton - tones">
|
||||||
|
<IconButton icon={<BoltIcon />} variant="ghost" tone="brand" title="Brand" />
|
||||||
|
<IconButton icon={<BoltIcon />} variant="ghost" tone="success" title="Success" />
|
||||||
|
<IconButton icon={<BoltIcon />} variant="ghost" tone="warning" title="Warning" />
|
||||||
|
<IconButton icon={<BoltIcon />} variant="ghost" tone="danger" title="Danger" />
|
||||||
|
<IconButton icon={<BoltIcon />} variant="ghost" tone="neutral" title="Neutral" />
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* BADGES */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="badges" title="Badge">
|
||||||
|
<DemoBox label="variants: solid | soft | outline | light">
|
||||||
|
<Badge variant="solid" tone="brand">Solid</Badge>
|
||||||
|
<Badge variant="soft" tone="brand">Soft</Badge>
|
||||||
|
<Badge variant="outline" tone="brand">Outline</Badge>
|
||||||
|
<Badge variant="light" tone="brand">Light</Badge>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="tones: brand | success | warning | danger | info | neutral">
|
||||||
|
<Badge tone="brand">Brand</Badge>
|
||||||
|
<Badge tone="success">Success</Badge>
|
||||||
|
<Badge tone="warning">Warning</Badge>
|
||||||
|
<Badge tone="danger">Danger</Badge>
|
||||||
|
<Badge tone="info">Info</Badge>
|
||||||
|
<Badge tone="neutral">Neutral</Badge>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="sizes: xs | sm | md">
|
||||||
|
<Badge size="xs">XS</Badge>
|
||||||
|
<Badge size="sm">SM</Badge>
|
||||||
|
<Badge size="md">MD</Badge>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="with icons: startIcon | endIcon">
|
||||||
|
<Badge startIcon={<CheckCircleIcon className="w-3 h-3" />} tone="success">Completed</Badge>
|
||||||
|
<Badge endIcon={<CloseIcon className="w-3 h-3" />} tone="danger">Remove</Badge>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* CARDS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="cards" title="Card">
|
||||||
|
<DemoBox label="variants: surface | panel | frosted | borderless | gradient">
|
||||||
|
<Card variant="surface" className="w-48">
|
||||||
|
<CardContent>Surface Card</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card variant="panel" className="w-48">
|
||||||
|
<CardContent>Panel Card</CardContent>
|
||||||
|
</Card>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="padding: none | sm | md | lg">
|
||||||
|
<Card padding="sm" className="w-48">
|
||||||
|
<CardContent>Small Padding</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card padding="lg" className="w-48">
|
||||||
|
<CardContent>Large Padding</CardContent>
|
||||||
|
</Card>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="Card with components">
|
||||||
|
<Card className="w-64">
|
||||||
|
<CardIcon><FileIcon className="w-8 h-8 text-brand-500" /></CardIcon>
|
||||||
|
<CardTitle>Card Title</CardTitle>
|
||||||
|
<CardDescription>Card description text goes here</CardDescription>
|
||||||
|
<CardAction>View More</CardAction>
|
||||||
|
</Card>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* ALERTS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="alerts" title="Alert">
|
||||||
|
<DemoBox label="variants: success | error | warning | info">
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<Alert variant="success" title="Success" message="Operation completed successfully!" />
|
||||||
|
<Alert variant="error" title="Error" message="Something went wrong. Please try again." />
|
||||||
|
<Alert variant="warning" title="Warning" message="Please review before continuing." />
|
||||||
|
<Alert variant="info" title="Info" message="Here's some useful information." />
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="AlertModal">
|
||||||
|
<Button onClick={() => setShowAlertModal(true)}>Open Alert Modal</Button>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={showAlertModal}
|
||||||
|
onClose={() => setShowAlertModal(false)}
|
||||||
|
variant="warning"
|
||||||
|
title="Confirm Action"
|
||||||
|
message="Are you sure you want to proceed?"
|
||||||
|
isConfirmation={true}
|
||||||
|
onConfirm={() => setShowAlertModal(false)}
|
||||||
|
/>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* MODALS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="modals" title="Modal">
|
||||||
|
<DemoBox label="Basic Modal">
|
||||||
|
<Button onClick={() => setShowModal(true)}>Open Modal</Button>
|
||||||
|
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Modal Title</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
This is modal content. You can put any React components here.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button variant="outline" onClick={() => setShowModal(false)}>Cancel</Button>
|
||||||
|
<Button onClick={() => setShowModal(false)}>Confirm</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* TABLES */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="tables" title="Table">
|
||||||
|
<DemoBox label="Table components">
|
||||||
|
<Table className="w-full">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell isHeader>Name</TableCell>
|
||||||
|
<TableCell isHeader>Status</TableCell>
|
||||||
|
<TableCell isHeader>Date</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Item One</TableCell>
|
||||||
|
<TableCell><Badge tone="success">Active</Badge></TableCell>
|
||||||
|
<TableCell>Jan 1, 2024</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Item Two</TableCell>
|
||||||
|
<TableCell><Badge tone="warning">Pending</Badge></TableCell>
|
||||||
|
<TableCell>Jan 2, 2024</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* TABS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="tabs" title="Tabs">
|
||||||
|
<DemoBox label="Tabs component">
|
||||||
|
<div className="w-full">
|
||||||
|
<Tabs defaultTab="tab1" onChange={setActiveTab}>
|
||||||
|
<TabList>
|
||||||
|
<Tab tabId="tab1">Tab One</Tab>
|
||||||
|
<Tab tabId="tab2">Tab Two</Tab>
|
||||||
|
<Tab tabId="tab3">Tab Three</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel tabId="tab1">
|
||||||
|
<div className="py-4">Content for Tab One</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel tabId="tab2">
|
||||||
|
<div className="py-4">Content for Tab Two</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel tabId="tab3">
|
||||||
|
<div className="py-4">Content for Tab Three</div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* ACCORDION */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="accordion" title="Accordion">
|
||||||
|
<DemoBox label="Accordion component">
|
||||||
|
<div className="w-full">
|
||||||
|
<Accordion>
|
||||||
|
<AccordionItem title="Section One" defaultOpen>
|
||||||
|
Content for section one goes here.
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem title="Section Two">
|
||||||
|
Content for section two goes here.
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem title="Section Three">
|
||||||
|
Content for section three goes here.
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* DROPDOWN */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="dropdown" title="Dropdown">
|
||||||
|
<DemoBox label="Dropdown component">
|
||||||
|
<div className="relative">
|
||||||
|
<Button ref={dropdownRef} onClick={() => setDropdownOpen(!dropdownOpen)}>
|
||||||
|
Open Dropdown
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
isOpen={dropdownOpen}
|
||||||
|
onClose={() => setDropdownOpen(false)}
|
||||||
|
anchorRef={dropdownRef}
|
||||||
|
>
|
||||||
|
<DropdownItem onClick={() => setDropdownOpen(false)}>Option One</DropdownItem>
|
||||||
|
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Two</DropdownItem>
|
||||||
|
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Three</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* PAGINATION */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="pagination" title="Pagination">
|
||||||
|
<DemoBox label="Pagination variants">
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={10}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
variant="text"
|
||||||
|
/>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="CompactPagination (with page size)">
|
||||||
|
<CompactPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={10}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* PROGRESS */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="progress" title="Progress">
|
||||||
|
<DemoBox label="ProgressBar colors">
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<ProgressBar value={75} color="primary" showLabel />
|
||||||
|
<ProgressBar value={60} color="success" showLabel />
|
||||||
|
<ProgressBar value={45} color="warning" showLabel />
|
||||||
|
<ProgressBar value={30} color="error" showLabel />
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="ProgressBar sizes: sm | md | lg">
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<ProgressBar value={50} size="sm" />
|
||||||
|
<ProgressBar value={50} size="md" />
|
||||||
|
<ProgressBar value={50} size="lg" />
|
||||||
|
</div>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* SPINNER */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="spinner" title="Spinner">
|
||||||
|
<DemoBox label="sizes: sm | md | lg">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
<Spinner size="md" />
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="colors: primary | success | error | warning | info">
|
||||||
|
<Spinner color="primary" />
|
||||||
|
<Spinner color="success" />
|
||||||
|
<Spinner color="error" />
|
||||||
|
<Spinner color="warning" />
|
||||||
|
<Spinner color="info" />
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* AVATAR */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="avatar" title="Avatar">
|
||||||
|
<DemoBox label="sizes: xsmall | small | medium | large | xlarge | xxlarge">
|
||||||
|
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" size="small" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" size="medium" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" size="large" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="status: online | offline | busy | none">
|
||||||
|
<Avatar src="/images/user/user-01.jpg" status="online" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" status="offline" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" status="busy" />
|
||||||
|
<Avatar src="/images/user/user-01.jpg" status="none" />
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* BREADCRUMB */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="breadcrumb" title="Breadcrumb">
|
||||||
|
<DemoBox label="Breadcrumb component">
|
||||||
|
<Breadcrumb items={[
|
||||||
|
{ label: 'Home', path: '/', icon: <GridIcon className="w-4 h-4" /> },
|
||||||
|
{ label: 'Category', path: '/category' },
|
||||||
|
{ label: 'Current Page' },
|
||||||
|
]} />
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* LIST */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="list" title="List">
|
||||||
|
<DemoBox label="List variants">
|
||||||
|
<List variant="unordered">
|
||||||
|
<ListItem><ListDot />Item One</ListItem>
|
||||||
|
<ListItem><ListDot />Item Two</ListItem>
|
||||||
|
<ListItem><ListDot />Item Three</ListItem>
|
||||||
|
</List>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="ListCheckboxItem">
|
||||||
|
<List variant="checkbox">
|
||||||
|
<ListCheckboxItem
|
||||||
|
id="check1"
|
||||||
|
label="Checkbox Option"
|
||||||
|
checked={checkboxChecked}
|
||||||
|
onChange={setCheckboxChecked}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="ListRadioItem">
|
||||||
|
<List variant="radio">
|
||||||
|
<ListRadioItem
|
||||||
|
id="radio1"
|
||||||
|
name="radioGroup"
|
||||||
|
value="option1"
|
||||||
|
label="Option One"
|
||||||
|
checked={radioValue === 'option1'}
|
||||||
|
onChange={setRadioValue}
|
||||||
|
/>
|
||||||
|
<ListRadioItem
|
||||||
|
id="radio2"
|
||||||
|
name="radioGroup"
|
||||||
|
value="option2"
|
||||||
|
label="Option Two"
|
||||||
|
checked={radioValue === 'option2'}
|
||||||
|
onChange={setRadioValue}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* TOOLTIP */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="tooltip" title="Tooltip">
|
||||||
|
<DemoBox label="placements: top | bottom | left | right">
|
||||||
|
<Tooltip text="Top tooltip" placement="top">
|
||||||
|
<Button variant="outline" size="sm">Top</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Bottom tooltip" placement="bottom">
|
||||||
|
<Button variant="outline" size="sm">Bottom</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Left tooltip" placement="left">
|
||||||
|
<Button variant="outline" size="sm">Left</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Right tooltip" placement="right">
|
||||||
|
<Button variant="outline" size="sm">Right</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="EnhancedTooltip (supports ReactNode content)">
|
||||||
|
<EnhancedTooltip
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<strong>Rich Content</strong>
|
||||||
|
<p className="text-xs">With multiple lines</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="sm">Hover Me</Button>
|
||||||
|
</EnhancedTooltip>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* RIBBON */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="ribbon" title="Ribbon">
|
||||||
|
<DemoBox label="Ribbon variants">
|
||||||
|
<Ribbon text="New" variant="rounded" color="primary">
|
||||||
|
<Card className="w-48 h-24">
|
||||||
|
<CardContent>Card with ribbon</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Ribbon>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* TOAST */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="toast" title="Toast">
|
||||||
|
<DemoBox label="Toast notifications (via useToast hook)">
|
||||||
|
<Button onClick={() => toast.success('Success', 'Operation completed!')}>
|
||||||
|
Show Success
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => toast.error('Error', 'Something went wrong!')} tone="danger">
|
||||||
|
Show Error
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => toast.warning('Warning', 'Please review!')} tone="warning">
|
||||||
|
Show Warning
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => toast.info('Info', 'Here is some information')} variant="outline">
|
||||||
|
Show Info
|
||||||
|
</Button>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* ================================================================ */}
|
||||||
|
{/* DATAVIEW */}
|
||||||
|
{/* ================================================================ */}
|
||||||
|
<Section id="dataview" title="DataView">
|
||||||
|
<DemoBox label="DataView container">
|
||||||
|
<DataView className="w-full">
|
||||||
|
<DataViewHeader
|
||||||
|
title="Data View Title"
|
||||||
|
description="Description of the data view"
|
||||||
|
actions={<Button size="sm">Action</Button>}
|
||||||
|
/>
|
||||||
|
<DataViewToolbar>
|
||||||
|
<Button size="sm" variant="outline">Filter</Button>
|
||||||
|
<Button size="sm" variant="outline">Sort</Button>
|
||||||
|
</DataViewToolbar>
|
||||||
|
<div className="p-4 text-gray-500">Data content goes here...</div>
|
||||||
|
</DataView>
|
||||||
|
</DemoBox>
|
||||||
|
|
||||||
|
<DemoBox label="DataViewEmptyState">
|
||||||
|
<DataViewEmptyState
|
||||||
|
title="No Data Found"
|
||||||
|
description="Try adjusting your filters or add new items"
|
||||||
|
action={<Button size="sm">Add Item</Button>}
|
||||||
|
/>
|
||||||
|
</DemoBox>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
@@ -323,40 +326,30 @@ export default function AccountSettingsPage() {
|
|||||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
|
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Account Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
|
label="Account Name"
|
||||||
value={accountForm.name}
|
value={accountForm.name}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Account Slug
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Account Slug"
|
||||||
value={settings?.slug || ''}
|
value={settings?.slug || ''}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
|
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Billing Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
name="billing_email"
|
name="billing_email"
|
||||||
|
label="Billing Email"
|
||||||
value={accountForm.billing_email}
|
value={accountForm.billing_email}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -366,77 +359,59 @@ export default function AccountSettingsPage() {
|
|||||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
|
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Address Line 1
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_address_line1"
|
name="billing_address_line1"
|
||||||
|
label="Address Line 1"
|
||||||
value={accountForm.billing_address_line1}
|
value={accountForm.billing_address_line1}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Address Line 2
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_address_line2"
|
name="billing_address_line2"
|
||||||
|
label="Address Line 2"
|
||||||
value={accountForm.billing_address_line2}
|
value={accountForm.billing_address_line2}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
City
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_city"
|
name="billing_city"
|
||||||
|
label="City"
|
||||||
value={accountForm.billing_city}
|
value={accountForm.billing_city}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
State/Province
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_state"
|
name="billing_state"
|
||||||
|
label="State/Province"
|
||||||
value={accountForm.billing_state}
|
value={accountForm.billing_state}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Postal Code
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_postal_code"
|
name="billing_postal_code"
|
||||||
|
label="Postal Code"
|
||||||
value={accountForm.billing_postal_code}
|
value={accountForm.billing_postal_code}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Country
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="billing_country"
|
name="billing_country"
|
||||||
|
label="Country"
|
||||||
value={accountForm.billing_country}
|
value={accountForm.billing_country}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
placeholder="US, GB, IN, etc."
|
placeholder="US, GB, IN, etc."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,15 +422,12 @@ export default function AccountSettingsPage() {
|
|||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
|
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Tax ID / VAT Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
name="tax_id"
|
name="tax_id"
|
||||||
|
label="Tax ID / VAT Number"
|
||||||
value={accountForm.tax_id}
|
value={accountForm.tax_id}
|
||||||
onChange={handleAccountChange}
|
onChange={handleAccountChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,47 +457,35 @@ export default function AccountSettingsPage() {
|
|||||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
|
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
First Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="First Name"
|
||||||
value={profileForm.firstName}
|
value={profileForm.firstName}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
|
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Last Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Last Name"
|
||||||
value={profileForm.lastName}
|
value={profileForm.lastName}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
|
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
value={profileForm.email}
|
value={profileForm.email}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
|
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Phone Number (optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
type="tel"
|
||||||
|
label="Phone Number (optional)"
|
||||||
value={profileForm.phone}
|
value={profileForm.phone}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
|
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -538,33 +498,33 @@ export default function AccountSettingsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Your Timezone
|
Your Timezone
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={profileForm.timezone}
|
options={[
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, timezone: e.target.value })}
|
{ value: 'America/New_York', label: 'Eastern Time' },
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
{ value: 'America/Chicago', label: 'Central Time' },
|
||||||
>
|
{ value: 'America/Denver', label: 'Mountain Time' },
|
||||||
<option value="America/New_York">Eastern Time</option>
|
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
|
||||||
<option value="America/Chicago">Central Time</option>
|
{ value: 'UTC', label: 'UTC' },
|
||||||
<option value="America/Denver">Mountain Time</option>
|
{ value: 'Europe/London', label: 'London' },
|
||||||
<option value="America/Los_Angeles">Pacific Time</option>
|
{ value: 'Asia/Kolkata', label: 'India' },
|
||||||
<option value="UTC">UTC</option>
|
]}
|
||||||
<option value="Europe/London">London</option>
|
defaultValue={profileForm.timezone}
|
||||||
<option value="Asia/Kolkata">India</option>
|
onChange={(value) => setProfileForm({ ...profileForm, timezone: value })}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Language
|
Language
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={profileForm.language}
|
options={[
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, language: e.target.value })}
|
{ value: 'en', label: 'English' },
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
|
{ value: 'es', label: 'Spanish' },
|
||||||
>
|
{ value: 'fr', label: 'French' },
|
||||||
<option value="en">English</option>
|
]}
|
||||||
<option value="es">Spanish</option>
|
defaultValue={profileForm.language}
|
||||||
<option value="fr">French</option>
|
onChange={(value) => setProfileForm({ ...profileForm, language: value })}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -582,11 +542,9 @@ export default function AccountSettingsPage() {
|
|||||||
Get notified about important changes to your account
|
Get notified about important changes to your account
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={profileForm.emailNotifications}
|
checked={profileForm.emailNotifications}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, emailNotifications: e.target.checked })}
|
onChange={(checked) => setProfileForm({ ...profileForm, emailNotifications: checked })}
|
||||||
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -596,11 +554,9 @@ export default function AccountSettingsPage() {
|
|||||||
Hear about new features and content tips
|
Hear about new features and content tips
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={profileForm.marketingEmails}
|
checked={profileForm.marketingEmails}
|
||||||
onChange={(e) => setProfileForm({ ...profileForm, marketingEmails: e.target.checked })}
|
onChange={(checked) => setProfileForm({ ...profileForm, marketingEmails: checked })}
|
||||||
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -769,39 +725,30 @@ export default function AccountSettingsPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email *"
|
||||||
value={inviteForm.email}
|
value={inviteForm.email}
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
First Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="First Name"
|
||||||
value={inviteForm.first_name}
|
value={inviteForm.first_name}
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
|
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Last Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Last Name"
|
||||||
value={inviteForm.last_name}
|
value={inviteForm.last_name}
|
||||||
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
|
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -854,38 +801,29 @@ export default function AccountSettingsPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Current Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Current Password"
|
||||||
value={passwordForm.currentPassword}
|
value={passwordForm.currentPassword}
|
||||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
|
onChange={(e) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="New Password"
|
||||||
value={passwordForm.newPassword}
|
value={passwordForm.newPassword}
|
||||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
|
onChange={(e) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<InputField
|
||||||
Confirm New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Confirm New Password"
|
||||||
value={passwordForm.confirmPassword}
|
value={passwordForm.confirmPassword}
|
||||||
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
|
|||||||
import SelectDropdown from '../../components/form/SelectDropdown';
|
import SelectDropdown from '../../components/form/SelectDropdown';
|
||||||
import Label from '../../components/form/Label';
|
import Label from '../../components/form/Label';
|
||||||
import Checkbox from '../../components/form/input/Checkbox';
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
|
import TextArea from '../../components/form/input/TextArea';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { BoxCubeIcon } from '../../icons';
|
import { BoxCubeIcon } from '../../icons';
|
||||||
@@ -357,11 +358,11 @@ export default function ContentSettingsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2">Append to Every Prompt</Label>
|
<Label className="mb-2">Append to Every Prompt</Label>
|
||||||
<textarea
|
<TextArea
|
||||||
value={contentSettings.appendToPrompt}
|
value={contentSettings.appendToPrompt}
|
||||||
onChange={(e) => setContentSettings({ ...contentSettings, appendToPrompt: e.target.value })}
|
onChange={(value) => setContentSettings({ ...contentSettings, appendToPrompt: value })}
|
||||||
placeholder="Add custom instructions that will be included with every content generation request..."
|
placeholder="Add custom instructions that will be included with every content generation request..."
|
||||||
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800 min-h-[120px] resize-y"
|
rows={5}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.
|
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { useNotificationStore } from '../../store/notificationStore';
|
import { useNotificationStore } from '../../store/notificationStore';
|
||||||
@@ -237,19 +238,19 @@ export default function NotificationsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Severity
|
Severity
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={filters.severity}
|
options={[
|
||||||
onChange={(e) =>
|
{ value: '', label: 'All' },
|
||||||
setFilters({ ...filters, severity: e.target.value })
|
{ value: 'info', label: 'Info' },
|
||||||
|
{ value: 'success', label: 'Success' },
|
||||||
|
{ value: 'warning', label: 'Warning' },
|
||||||
|
{ value: 'error', label: 'Error' },
|
||||||
|
]}
|
||||||
|
defaultValue={filters.severity}
|
||||||
|
onChange={(value) =>
|
||||||
|
setFilters({ ...filters, severity: value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
<option value="info">Info</option>
|
|
||||||
<option value="success">Success</option>
|
|
||||||
<option value="warning">Warning</option>
|
|
||||||
<option value="error">Error</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Type Filter */}
|
{/* Type Filter */}
|
||||||
@@ -257,20 +258,19 @@ export default function NotificationsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Type
|
Type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={filters.notification_type}
|
options={[
|
||||||
onChange={(e) =>
|
{ value: '', label: 'All' },
|
||||||
setFilters({ ...filters, notification_type: e.target.value })
|
...notificationTypes.map((type) => ({
|
||||||
|
value: type,
|
||||||
|
label: getTypeLabel(type),
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
defaultValue={filters.notification_type}
|
||||||
|
onChange={(value) =>
|
||||||
|
setFilters({ ...filters, notification_type: value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
{notificationTypes.map((type) => (
|
|
||||||
<option key={type} value={type}>
|
|
||||||
{getTypeLabel(type)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Read Status Filter */}
|
{/* Read Status Filter */}
|
||||||
@@ -278,17 +278,17 @@ export default function NotificationsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Status
|
Status
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
value={filters.is_read}
|
options={[
|
||||||
onChange={(e) =>
|
{ value: '', label: 'All' },
|
||||||
setFilters({ ...filters, is_read: e.target.value })
|
{ value: 'false', label: 'Unread' },
|
||||||
|
{ value: 'true', label: 'Read' },
|
||||||
|
]}
|
||||||
|
defaultValue={filters.is_read}
|
||||||
|
onChange={(value) =>
|
||||||
|
setFilters({ ...filters, is_read: value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
/>
|
||||||
>
|
|
||||||
<option value="">All</option>
|
|
||||||
<option value="false">Unread</option>
|
|
||||||
<option value="true">Read</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { AlertCircleIcon, CheckIcon, CreditCardIcon, Building2Icon, WalletIcon, Loader2Icon, ZapIcon } from '../../icons';
|
import { AlertCircleIcon, CheckIcon, CreditCardIcon, Building2Icon, WalletIcon, Loader2Icon, ZapIcon } from '../../icons';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import TextArea from '../../components/form/input/TextArea';
|
||||||
|
import Label from '../../components/form/Label';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import {
|
import {
|
||||||
@@ -245,33 +248,26 @@ export default function PurchaseCreditsPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<InputField
|
||||||
Transaction Reference / ID *
|
label="Transaction Reference / ID *"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
value={manualPaymentData.transaction_reference}
|
value={manualPaymentData.transaction_reference}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setManualPaymentData({ ...manualPaymentData, transaction_reference: e.target.value })
|
setManualPaymentData({ ...manualPaymentData, transaction_reference: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Enter transaction ID or reference number"
|
placeholder="Enter transaction ID or reference number"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<Label className="mb-2">Additional Notes (Optional)</Label>
|
||||||
Additional Notes (Optional)
|
<TextArea
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={manualPaymentData.notes}
|
value={manualPaymentData.notes}
|
||||||
onChange={(e) =>
|
onChange={(value) =>
|
||||||
setManualPaymentData({ ...manualPaymentData, notes: e.target.value })
|
setManualPaymentData({ ...manualPaymentData, notes: value })
|
||||||
}
|
}
|
||||||
placeholder="Any additional information..."
|
placeholder="Any additional information..."
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { SaveIcon, UserIcon, MailIcon, LockIcon, Loader2Icon } from '../../icons';
|
import { SaveIcon, UserIcon, MailIcon, LockIcon, Loader2Icon } from '../../icons';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import InputField from '../../components/form/input/InputField';
|
||||||
|
import Select from '../../components/form/Select';
|
||||||
|
import Checkbox from '../../components/form/input/Checkbox';
|
||||||
|
|
||||||
export default function ProfileSettingsPage() {
|
export default function ProfileSettingsPage() {
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -38,14 +42,15 @@ export default function ProfileSettingsPage() {
|
|||||||
Update your personal settings - Your name, preferences, and notification choices
|
Update your personal settings - Your name, preferences, and notification choices
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50"
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
{saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
||||||
{saving ? 'Saving...' : '✓ Save My Settings'}
|
{saving ? 'Saving...' : '✓ Save My Settings'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -53,47 +58,35 @@ export default function ProfileSettingsPage() {
|
|||||||
<h2 className="text-lg font-semibold mb-4">About You</h2>
|
<h2 className="text-lg font-semibold mb-4">About You</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
First Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="First Name"
|
||||||
value={profile.firstName}
|
value={profile.firstName}
|
||||||
onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
|
onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Last Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Last Name"
|
||||||
value={profile.lastName}
|
value={profile.lastName}
|
||||||
onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
|
onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
value={profile.email}
|
value={profile.email}
|
||||||
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<InputField
|
||||||
Phone Number (optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
type="tel"
|
||||||
|
label="Phone Number (optional)"
|
||||||
value={profile.phone}
|
value={profile.phone}
|
||||||
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,34 +96,30 @@ export default function ProfileSettingsPage() {
|
|||||||
<h2 className="text-lg font-semibold mb-4">How You Like It</h2>
|
<h2 className="text-lg font-semibold mb-4">How You Like It</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Select
|
||||||
Your Timezone
|
label="Your Timezone"
|
||||||
</label>
|
options={[
|
||||||
<select
|
{ value: 'America/New_York', label: 'Eastern Time' },
|
||||||
value={profile.timezone}
|
{ value: 'America/Chicago', label: 'Central Time' },
|
||||||
onChange={(e) => setProfile({ ...profile, timezone: e.target.value })}
|
{ value: 'America/Denver', label: 'Mountain Time' },
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
|
||||||
>
|
{ value: 'UTC', label: 'UTC' },
|
||||||
<option value="America/New_York">Eastern Time</option>
|
]}
|
||||||
<option value="America/Chicago">Central Time</option>
|
defaultValue={profile.timezone}
|
||||||
<option value="America/Denver">Mountain Time</option>
|
onChange={(val) => setProfile({ ...profile, timezone: val })}
|
||||||
<option value="America/Los_Angeles">Pacific Time</option>
|
/>
|
||||||
<option value="UTC">UTC</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<Select
|
||||||
Language
|
label="Language"
|
||||||
</label>
|
options={[
|
||||||
<select
|
{ value: 'en', label: 'English' },
|
||||||
value={profile.language}
|
{ value: 'es', label: 'Spanish' },
|
||||||
onChange={(e) => setProfile({ ...profile, language: e.target.value })}
|
{ value: 'fr', label: 'French' },
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
|
]}
|
||||||
>
|
defaultValue={profile.language}
|
||||||
<option value="en">English</option>
|
onChange={(val) => setProfile({ ...profile, language: val })}
|
||||||
<option value="es">Spanish</option>
|
/>
|
||||||
<option value="fr">French</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -148,11 +137,9 @@ export default function ProfileSettingsPage() {
|
|||||||
Get notified about important changes to your account
|
Get notified about important changes to your account
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={profile.emailNotifications}
|
checked={profile.emailNotifications}
|
||||||
onChange={(e) => setProfile({ ...profile, emailNotifications: e.target.checked })}
|
onChange={(checked) => setProfile({ ...profile, emailNotifications: checked })}
|
||||||
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -162,11 +149,9 @@ export default function ProfileSettingsPage() {
|
|||||||
Hear about new features and content tips
|
Hear about new features and content tips
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={profile.marketingEmails}
|
checked={profile.marketingEmails}
|
||||||
onChange={(e) => setProfile({ ...profile, marketingEmails: e.target.checked })}
|
onChange={(checked) => setProfile({ ...profile, marketingEmails: checked })}
|
||||||
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,9 +162,9 @@ export default function ProfileSettingsPage() {
|
|||||||
<LockIcon className="w-5 h-5" />
|
<LockIcon className="w-5 h-5" />
|
||||||
Security
|
Security
|
||||||
</h2>
|
</h2>
|
||||||
<button className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800">
|
<Button variant="outline" tone="neutral">
|
||||||
Change Password
|
Change Password
|
||||||
</button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
import { Content, fetchImages, ImageRecord } from '../services/api';
|
import { Content, fetchImages, ImageRecord } from '../services/api';
|
||||||
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon, PencilIcon, ImageIcon, BoltIcon } from '../icons';
|
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon, PencilIcon, ImageIcon, BoltIcon } from '../icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import Button from '../components/ui/button/Button';
|
||||||
|
|
||||||
interface ContentViewTemplateProps {
|
interface ContentViewTemplateProps {
|
||||||
content: Content | null;
|
content: Content | null;
|
||||||
@@ -759,13 +760,13 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">The content you're looking for doesn't exist or has been deleted.</p>
|
<p className="text-gray-600 dark:text-gray-400 mb-6">The content you're looking for doesn't exist or has been deleted.</p>
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
<ArrowLeftIcon className="w-4 h-4" />
|
||||||
Back to Content List
|
Back to Content List
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -824,13 +825,14 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="mb-6 inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
|
className="mb-6"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-5 h-5" />
|
<ArrowLeftIcon className="w-4 h-4" />
|
||||||
<span className="font-medium">Back to Content</span>
|
Back to Content List
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content Card */}
|
{/* Main Content Card */}
|
||||||
@@ -1025,40 +1027,42 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
{/* Draft status: Show Edit Content + Generate Images */}
|
{/* Draft status: Show Edit Content + Generate Images */}
|
||||||
{content.status.toLowerCase() === 'draft' && (
|
{content.status.toLowerCase() === 'draft' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
|
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 hover:bg-brand-600 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
<PencilIcon className="w-4 h-4" />
|
<PencilIcon className="w-4 h-4" />
|
||||||
Edit Content
|
Edit Content
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
onClick={() => navigate(`/writer/images?contentId=${content.id}`)}
|
onClick={() => navigate(`/writer/images?contentId=${content.id}`)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
<ImageIcon className="w-4 h-4" />
|
<ImageIcon className="w-4 h-4" />
|
||||||
Generate Images
|
Generate Images
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Review status: Show Edit Content + Publish */}
|
{/* Review status: Show Edit Content + Publish */}
|
||||||
{content.status.toLowerCase() === 'review' && (
|
{content.status.toLowerCase() === 'review' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
|
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 hover:bg-brand-600 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
<PencilIcon className="w-4 h-4" />
|
<PencilIcon className="w-4 h-4" />
|
||||||
Edit Content
|
Edit Content
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
onClick={() => navigate(`/writer/published?contentId=${content.id}&action=publish`)}
|
onClick={() => navigate(`/writer/published?contentId=${content.id}&action=publish`)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-success-500 hover:bg-success-600 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
<BoltIcon className="w-4 h-4" />
|
<BoltIcon className="w-4 h-4" />
|
||||||
Publish
|
Publish
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode, useState } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
import Button from '../components/ui/button/Button';
|
||||||
|
|
||||||
interface FormSection {
|
interface FormSection {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -89,21 +90,21 @@ export default function FormPageTemplate({
|
|||||||
{(onSave || onCancel) && (
|
{(onSave || onCancel) && (
|
||||||
<div className="mt-6 flex items-center justify-end gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-white/[0.03]">
|
<div className="mt-6 flex items-center justify-end gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<button
|
<Button
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-white/[0.03]"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onSave && (
|
{onSave && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={loading || !isDirty}
|
disabled={loading || !isDirty}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
variant="primary"
|
||||||
>
|
>
|
||||||
{loading ? 'Saving...' : saveLabel}
|
{loading ? 'Saving...' : saveLabel}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user