componenets standardization 1

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 21:42:04 +00:00
parent c880e24fc0
commit a4691ad2da
95 changed files with 3597 additions and 1745 deletions

183
DESIGN-GUIDE.md Normal file
View 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

View 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.

View File

@@ -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',
}, },
}, },
) )

View 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',
});
}
},
};
},
},
},
};

View File

@@ -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 */}

View File

@@ -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">

View File

@@ -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: KeywordsClusters"
</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: ClustersIdeas"
</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: IdeasTasks"
</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: TasksContent"
</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: ContentImage 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 PromptsImages"
</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>

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 ${

View File

@@ -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>

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
) : ( ) : (

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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>
); );

View File

@@ -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">

View File

@@ -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>
); );

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>
))} ))}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
</> </>
)} )}

View File

@@ -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">

View File

@@ -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">

View 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;

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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('!');

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>
)} )}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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>
)} )}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
))} ))}

View File

@@ -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

View File

@@ -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">

View 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>
</>
);
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)} )}