componenets standardization 1
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { automationService } from '../../services/automationService';
|
||||
import ComponentCard from '../common/ComponentCard';
|
||||
import Select from '../form/Select';
|
||||
|
||||
interface ActivityLogProps {
|
||||
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 gap-2">
|
||||
<label className="text-sm text-gray-600 dark:text-gray-400">Lines:</label>
|
||||
<select
|
||||
value={lines}
|
||||
onChange={(e) => setLines(parseInt(e.target.value))}
|
||||
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"
|
||||
>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
<option value={500}>500</option>
|
||||
</select>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '50', label: '50' },
|
||||
{ value: '100', label: '100' },
|
||||
{ value: '200', label: '200' },
|
||||
{ value: '500', label: '500' },
|
||||
]}
|
||||
defaultValue={String(lines)}
|
||||
onChange={(val) => setLines(parseInt(val))}
|
||||
/>
|
||||
</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">
|
||||
|
||||
@@ -6,6 +6,9 @@ import React, { useState } from 'react';
|
||||
import { AutomationConfig } from '../../services/automationService';
|
||||
import { Modal } from '../ui/modal';
|
||||
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 {
|
||||
config: AutomationConfig;
|
||||
@@ -51,17 +54,13 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Enable/Disable */}
|
||||
<div className="mb-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_enabled || false}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, is_enabled: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="font-semibold">Enable Automation</span>
|
||||
</label>
|
||||
<Checkbox
|
||||
label="Enable Automation"
|
||||
checked={formData.is_enabled || false}
|
||||
onChange={(checked) =>
|
||||
setFormData({ ...formData, is_enabled: checked })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-gray-600 ml-6">
|
||||
When enabled, automation will run on the configured schedule
|
||||
</p>
|
||||
@@ -70,36 +69,33 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
{/* Frequency */}
|
||||
<div className="mb-4">
|
||||
<label className="block font-semibold mb-1">Frequency</label>
|
||||
<select
|
||||
value={formData.frequency || 'daily'}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'weekly', label: 'Weekly (Mondays)' },
|
||||
{ value: 'monthly', label: 'Monthly (1st of month)' },
|
||||
]}
|
||||
defaultValue={formData.frequency || 'daily'}
|
||||
onChange={(val) =>
|
||||
setFormData({
|
||||
...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>
|
||||
|
||||
{/* Scheduled Time */}
|
||||
<div className="mb-4">
|
||||
<label className="block font-semibold mb-1">Scheduled Time</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Scheduled Time"
|
||||
type="time"
|
||||
value={formData.scheduled_time || '02:00'}
|
||||
onChange={(e) =>
|
||||
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>
|
||||
|
||||
{/* Batch Sizes */}
|
||||
@@ -111,10 +107,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 1: Keywords → Clusters
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 1: Keywords → Clusters"
|
||||
type="number"
|
||||
value={formData.stage_1_batch_size || 20}
|
||||
onChange={(e) =>
|
||||
@@ -123,17 +117,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_1_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={100}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 2: Clusters → Ideas
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 2: Clusters → Ideas"
|
||||
type="number"
|
||||
value={formData.stage_2_batch_size || 1}
|
||||
onChange={(e) =>
|
||||
@@ -142,17 +133,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_2_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 3: Ideas → Tasks
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 3: Ideas → Tasks"
|
||||
type="number"
|
||||
value={formData.stage_3_batch_size || 20}
|
||||
onChange={(e) =>
|
||||
@@ -161,17 +149,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_3_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={100}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 4: Tasks → Content
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 4: Tasks → Content"
|
||||
type="number"
|
||||
value={formData.stage_4_batch_size || 1}
|
||||
onChange={(e) =>
|
||||
@@ -180,17 +165,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_4_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 5: Content → Image Prompts
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 5: Content → Image Prompts"
|
||||
type="number"
|
||||
value={formData.stage_5_batch_size || 1}
|
||||
onChange={(e) =>
|
||||
@@ -199,17 +181,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_5_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Stage 6: Image Prompts → Images
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Stage 6: Image Prompts → Images"
|
||||
type="number"
|
||||
value={formData.stage_6_batch_size || 1}
|
||||
onChange={(e) =>
|
||||
@@ -218,9 +197,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_6_batch_size: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,10 +213,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Within-Stage Delay (seconds)
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Within-Stage Delay (seconds)"
|
||||
type="number"
|
||||
value={formData.within_stage_delay || 3}
|
||||
onChange={(e) =>
|
||||
@@ -247,20 +223,15 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
within_stage_delay: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
max={30}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="0"
|
||||
max="30"
|
||||
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>
|
||||
<label className="block text-sm mb-1">
|
||||
Between-Stage Delay (seconds)
|
||||
</label>
|
||||
<input
|
||||
<InputField
|
||||
label="Between-Stage Delay (seconds)"
|
||||
type="number"
|
||||
value={formData.between_stage_delay || 5}
|
||||
onChange={(e) =>
|
||||
@@ -269,13 +240,10 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
between_stage_delay: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
max={60}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="0"
|
||||
max="60"
|
||||
hint="Delay between stage transitions"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Delay between stage transitions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import {
|
||||
PlayIcon,
|
||||
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="flex items-center justify-between">
|
||||
<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}
|
||||
className="text-error-500 hover:text-error-700 dark:hover:text-error-300"
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -564,13 +567,15 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
</div>
|
||||
{/* Debug table toggle + table for stage data */}
|
||||
<div className="mt-4">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="xs"
|
||||
onClick={() => setShowDebugTable(!showDebugTable)}
|
||||
className="text-xs text-gray-600 hover:underline"
|
||||
>
|
||||
{showDebugTable ? 'Hide' : 'Show'} debug table
|
||||
</button>
|
||||
</Button>
|
||||
{showDebugTable && (
|
||||
<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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import {
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
@@ -228,13 +229,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Close Button - Top Right */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1"
|
||||
<IconButton
|
||||
icon={<XMarkIcon className="w-5 h-5" />}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
title="Close"
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress Section */}
|
||||
|
||||
@@ -59,27 +59,31 @@ export default function UserAddressCard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
|
||||
@@ -67,27 +67,31 @@ export default function UserInfoCard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
|
||||
@@ -119,27 +119,31 @@ export default function UserMetaCard() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Button } from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import { Dropdown } from '../ui/dropdown/Dropdown';
|
||||
import {
|
||||
MoreDotIcon,
|
||||
@@ -66,14 +67,15 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
<IconButton
|
||||
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)}
|
||||
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
|
||||
isOpen={isOpen}
|
||||
@@ -86,61 +88,76 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
{/* WordPress Publishing - Only show if images are ready */}
|
||||
{canPublishToWordPress && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>Publish to Site</span>
|
||||
</button>
|
||||
Publish to Site
|
||||
</Button>
|
||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit Action */}
|
||||
{onEdit && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Generate Image Action */}
|
||||
{onGenerateImage && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>Generate Image Prompts</span>
|
||||
</button>
|
||||
Generate Image Prompts
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Export Action */}
|
||||
{onExport && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>Export</span>
|
||||
</button>
|
||||
Export
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Delete Action */}
|
||||
{onDelete && (
|
||||
<>
|
||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="danger"
|
||||
size="sm"
|
||||
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" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function SignInForm() {
|
||||
</div>
|
||||
<div>
|
||||
<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
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -140,8 +140,8 @@ export default function SignInForm() {
|
||||
/>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</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>
|
||||
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||
<svg
|
||||
width="21"
|
||||
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" />
|
||||
</svg>
|
||||
Sign in with X
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative py-3 sm:py-5">
|
||||
<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>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
tone="warning"
|
||||
size="sm"
|
||||
onClick={handleForceLogout}
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
tone="warning"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
||||
import Label from "../form/Label";
|
||||
import Input from "../form/input/InputField";
|
||||
import Checkbox from "../form/input/Checkbox";
|
||||
import Button from "../ui/button/Button";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
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 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
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -160,8 +161,8 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
||||
/>
|
||||
</svg>
|
||||
Sign up with Google
|
||||
</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>
|
||||
<Button variant="outline" tone="neutral" size="md" className="gap-3">
|
||||
<svg
|
||||
width="21"
|
||||
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" />
|
||||
</svg>
|
||||
Sign up with X
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative py-3 sm:py-5">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
@@ -294,13 +295,16 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
|
||||
</div>
|
||||
{/* Submit Button */}
|
||||
<div>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="md"
|
||||
fullWidth
|
||||
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"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -11,6 +11,7 @@ import Label from '../form/Label';
|
||||
import Input from '../form/input/InputField';
|
||||
import Checkbox from '../form/input/Checkbox';
|
||||
import Button from '../ui/button/Button';
|
||||
import SelectDropdown from '../form/SelectDropdown';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface PaymentMethodConfig {
|
||||
@@ -321,21 +322,21 @@ export default function SignUpFormSimplified({ planDetails: planDetailsProp, pla
|
||||
<Label>
|
||||
Country<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<select
|
||||
name="billingCountry"
|
||||
<SelectDropdown
|
||||
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}
|
||||
onChange={handleChange}
|
||||
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>
|
||||
onChange={(val) => setFormData({ ...formData, billingCountry: val })}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Payment methods will be filtered by your country
|
||||
</p>
|
||||
|
||||
@@ -284,24 +284,28 @@ export default function SignUpFormUnified({
|
||||
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-28'
|
||||
}`}
|
||||
></span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('annually')}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<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 ${
|
||||
@@ -345,8 +349,11 @@ export default function SignUpFormUnified({
|
||||
const isFree = parseFloat(String(plan.price || 0)) === 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
key={plan.id}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="md"
|
||||
onClick={() => onPlanSelect(plan)}
|
||||
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
||||
isSelected
|
||||
@@ -366,7 +373,7 @@ export default function SignUpFormUnified({
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{billingPeriod === 'annually' && !isFree ? '/year' : '/month'}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -550,24 +557,28 @@ export default function SignUpFormUnified({
|
||||
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-32'
|
||||
}`}
|
||||
></span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('annually')}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<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 ${
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
import Label from '../form/Label';
|
||||
import Input from '../form/input/InputField';
|
||||
import TextArea from '../form/input/TextArea';
|
||||
import { Loader2Icon, UploadIcon, XIcon, CheckCircleIcon } from '../../icons';
|
||||
import { API_BASE_URL } from '../../services/api';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
@@ -245,15 +246,12 @@ export default function PaymentConfirmationModal({
|
||||
{/* Additional Notes */}
|
||||
<div>
|
||||
<Label>Additional Notes (Optional)</Label>
|
||||
<textarea
|
||||
id="manual_notes"
|
||||
name="manual_notes"
|
||||
value={formData.manual_notes}
|
||||
onChange={(e) => setFormData({ ...formData, manual_notes: e.target.value })}
|
||||
<TextArea
|
||||
placeholder="Any additional information about the payment..."
|
||||
rows={3}
|
||||
value={formData.manual_notes}
|
||||
onChange={(val) => setFormData({ ...formData, manual_notes: val })}
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import Button from "../ui/button/Button";
|
||||
|
||||
const ChartTab: React.FC = () => {
|
||||
const [selected, setSelected] = useState<
|
||||
@@ -12,32 +13,35 @@ const ChartTab: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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")}
|
||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
||||
"optionOne"
|
||||
)}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
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(
|
||||
"optionTwo"
|
||||
)}`}
|
||||
>
|
||||
Quarterly
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
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(
|
||||
"optionThree"
|
||||
)}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
export interface ContentImageData {
|
||||
id?: number;
|
||||
@@ -73,12 +74,15 @@ export default function ContentImageCell({ image, maxPromptLength = 100, showPro
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{displayPrompt}
|
||||
{shouldTruncate && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="brand"
|
||||
size="xs"
|
||||
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'}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import { Modal } from '../ui/modal';
|
||||
import { CloseIcon } from '../../icons';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
|
||||
interface ContentViewerModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -30,12 +32,14 @@ export default function ContentViewerModal({
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
<IconButton
|
||||
icon={<CloseIcon className="w-6 h-6" />}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
title="Close"
|
||||
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>
|
||||
|
||||
{/* Content */}
|
||||
@@ -48,12 +52,14 @@ export default function ContentViewerModal({
|
||||
|
||||
{/* Footer */}
|
||||
<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}
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
import SelectDropdown from '../form/SelectDropdown';
|
||||
import Label from '../form/Label';
|
||||
import InputField from '../form/input/InputField';
|
||||
import TextArea from '../form/input/TextArea';
|
||||
|
||||
export interface FormField {
|
||||
key: string;
|
||||
@@ -69,17 +71,15 @@ export default function FormModal({
|
||||
<>
|
||||
{fields.find(f => f.key === 'keyword') && (
|
||||
<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')!.required && <span className="text-error-500 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<InputField
|
||||
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 || ''}
|
||||
onChange={(e) => fields.find(f => f.key === 'keyword')!.onChange(e.target.value)}
|
||||
placeholder={fields.find(f => f.key === 'keyword')!.placeholder}
|
||||
required={fields.find(f => f.key === 'keyword')!.required}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -87,20 +87,18 @@ export default function FormModal({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{fields.find(f => f.key === 'volume') && (
|
||||
<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')!.required && <span className="text-error-500 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<InputField
|
||||
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 || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
||||
fields.find(f => f.key === 'volume')!.onChange(value);
|
||||
}}
|
||||
placeholder={fields.find(f => f.key === 'volume')!.placeholder}
|
||||
required={fields.find(f => f.key === 'volume')!.required}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -121,18 +119,14 @@ export default function FormModal({
|
||||
className="w-full"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
<InputField
|
||||
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 || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
|
||||
difficultyField.onChange(value);
|
||||
}}
|
||||
placeholder={difficultyField.placeholder}
|
||||
required={difficultyField.required}
|
||||
min={difficultyField.min}
|
||||
max={difficultyField.max}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -161,36 +155,30 @@ export default function FormModal({
|
||||
if (field.type === 'textarea') {
|
||||
return (
|
||||
<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.required && <span className="text-error-500 ml-1">*</span>}
|
||||
</label>
|
||||
<textarea
|
||||
</Label>
|
||||
<TextArea
|
||||
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 || ''}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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.required && <span className="text-error-500 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<InputField
|
||||
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 || ''}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useErrorHandler } from '../../hooks/useErrorHandler';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
export default function GlobalErrorDisplay() {
|
||||
const { errors, clearError, clearAllErrors } = useErrorHandler('GlobalErrorDisplay');
|
||||
@@ -42,23 +44,27 @@ export default function GlobalErrorDisplay() {
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<IconButton
|
||||
icon={<span className="text-xl leading-none">×</span>}
|
||||
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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{errors.length > 1 && (
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ReactNode, useState, useEffect } from 'react';
|
||||
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 { fetchAPI } from '../../services/api';
|
||||
|
||||
@@ -292,14 +295,13 @@ export default function ImageGenerationCard({
|
||||
|
||||
{/* Prompt Description - Full Width */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Label className="mb-2">
|
||||
Prompt Description *
|
||||
</label>
|
||||
<textarea
|
||||
</Label>
|
||||
<TextArea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onChange={(val) => setPrompt(val)}
|
||||
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..."
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -309,14 +311,13 @@ export default function ImageGenerationCard({
|
||||
|
||||
{/* Negative Prompt - Small */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Label className="mb-2">
|
||||
Negative Prompt
|
||||
</label>
|
||||
<textarea
|
||||
</Label>
|
||||
<TextArea
|
||||
value={negativePrompt}
|
||||
onChange={(e) => setNegativePrompt(e.target.value)}
|
||||
onChange={(val) => setNegativePrompt(val)}
|
||||
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..."
|
||||
/>
|
||||
<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">
|
||||
{/* Image Type */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Label className="mb-2">
|
||||
Image Type
|
||||
</label>
|
||||
<select
|
||||
value={imageType}
|
||||
onChange={(e) => setImageType(e.target.value)}
|
||||
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"
|
||||
>
|
||||
{typeOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Label>
|
||||
<Select
|
||||
options={typeOptions}
|
||||
defaultValue={imageType}
|
||||
onChange={(val) => setImageType(val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Image Size */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Label className="mb-2">
|
||||
Image Size
|
||||
</label>
|
||||
<select
|
||||
value={imageSize}
|
||||
onChange={(e) => setImageSize(e.target.value)}
|
||||
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"
|
||||
>
|
||||
{sizeOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Label>
|
||||
<Select
|
||||
options={sizeOptions}
|
||||
defaultValue={imageSize}
|
||||
onChange={(val) => setImageSize(val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Image Format */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Label className="mb-2">
|
||||
Image Format
|
||||
</label>
|
||||
<select
|
||||
value={imageFormat}
|
||||
onChange={(e) => setImageFormat(e.target.value)}
|
||||
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"
|
||||
>
|
||||
{formatOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Label>
|
||||
<Select
|
||||
options={formatOptions}
|
||||
defaultValue={imageFormat}
|
||||
onChange={(val) => setImageFormat(val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Modal } from '../ui/modal';
|
||||
import { FileIcon, TimeIcon, CheckCircleIcon, ErrorIcon } from '../../icons';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
export interface ImageQueueItem {
|
||||
imageId: number | null;
|
||||
@@ -593,12 +594,14 @@ export default function ImageQueueModal({
|
||||
{completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''} of {totalImages} total
|
||||
</div>
|
||||
{allDone && (
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
interface ImageResultCardProps {
|
||||
title: string;
|
||||
@@ -195,11 +196,13 @@ export default function ImageResultCard({
|
||||
</svg>
|
||||
View Original
|
||||
</a>
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -211,12 +214,13 @@ export default function ImageResultCard({
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-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" />
|
||||
</svg>
|
||||
Copy URL
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
interface SearchModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -87,11 +88,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
<Modal isOpen={isOpen} onClose={onClose} className="sm:max-w-lg">
|
||||
<div className="p-0">
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
{/* Using native input for ref and onKeyDown support - styled to match design system */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
@@ -99,9 +101,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
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
|
||||
</span>
|
||||
</div>
|
||||
@@ -113,10 +115,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
</div>
|
||||
) : (
|
||||
filteredResults.map((result, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={result.path}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
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
|
||||
? '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'
|
||||
@@ -126,7 +130,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span className="font-medium">{result.title}</span>
|
||||
</button>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { GridIcon, ListIcon, TableIcon } from "../../icons";
|
||||
import Button from "../ui/button/Button";
|
||||
|
||||
export type ViewType = "table" | "kanban" | "list";
|
||||
|
||||
@@ -23,19 +24,18 @@ const ViewToggle: React.FC<ViewToggleProps> = ({
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-1 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900 ${className}`}>
|
||||
{views.map((view) => (
|
||||
<button
|
||||
<Button
|
||||
key={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 ${
|
||||
currentView === view.type
|
||||
? "bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm"
|
||||
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
}`}
|
||||
variant={currentView === view.type ? "secondary" : "ghost"}
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
className={currentView === view.type ? "shadow-sm" : ""}
|
||||
title={view.label}
|
||||
startIcon={view.icon}
|
||||
>
|
||||
{view.icon}
|
||||
<span className="hidden sm:inline">{view.label}</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SourceBadge, ContentSource } from './SourceBadge';
|
||||
import InputField from '../form/input/InputField';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
interface ContentFilterProps {
|
||||
onFilterChange: (filters: FilterState) => void;
|
||||
@@ -34,12 +36,11 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* Search */}
|
||||
<div>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="Search content..."
|
||||
value={filters.search}
|
||||
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>
|
||||
|
||||
@@ -47,28 +48,24 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
|
||||
<div>
|
||||
<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">
|
||||
<button
|
||||
<Button
|
||||
variant={filters.source === 'all' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
{(['igny8', 'wordpress', 'shopify', 'custom'] as ContentSource[]).map((source) => (
|
||||
<button
|
||||
<Button
|
||||
key={source}
|
||||
variant={filters.source === source ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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} />
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FileIcon,
|
||||
ChevronDownIcon,
|
||||
} from '../../icons';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
export interface AIOperation {
|
||||
type: 'clustering' | 'ideas' | 'content' | 'images';
|
||||
@@ -73,31 +74,33 @@ export default function AIOperationsWidget({ data, onPeriodChange, loading }: AI
|
||||
|
||||
{/* Period Dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
<Button
|
||||
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}
|
||||
<ChevronDownIcon className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{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">
|
||||
{periods.map((period) => (
|
||||
<button
|
||||
<Button
|
||||
key={period.value}
|
||||
onClick={() => {
|
||||
onPeriodChange?.(period.value);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
data.period === period.value
|
||||
? 'text-brand-600 dark:text-brand-400 font-medium'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
variant="ghost"
|
||||
tone={data.period === period.value ? 'brand' : 'neutral'}
|
||||
size="xs"
|
||||
fullWidth
|
||||
className="justify-start"
|
||||
>
|
||||
{period.label}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
CheckCircleIcon,
|
||||
CloseIcon,
|
||||
} from '../../icons';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
|
||||
export interface AttentionItem {
|
||||
id: string;
|
||||
@@ -80,9 +82,17 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{/* Header */}
|
||||
<button
|
||||
<Button
|
||||
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">
|
||||
<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})
|
||||
</span>
|
||||
</div>
|
||||
{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" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Content */}
|
||||
{!isCollapsed && (
|
||||
@@ -127,12 +132,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
||||
{item.actionLabel} →
|
||||
</Link>
|
||||
) : item.onAction ? (
|
||||
<button
|
||||
<Button
|
||||
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}
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
{item.secondaryActionHref && (
|
||||
<Link
|
||||
@@ -145,12 +152,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
|
||||
</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
<IconButton
|
||||
icon={<CloseIcon className="w-4 h-4" />}
|
||||
onClick={() => onDismiss(item.id)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<CloseIcon className="w-4 h-4" />
|
||||
</button>
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
aria-label="Dismiss"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBillingStore } from '../../store/billingStore';
|
||||
import ComponentCard from '../common/ComponentCard';
|
||||
import Select from '../form/Select';
|
||||
|
||||
export default function UsageChartWidget() {
|
||||
const { usageSummary, loading, loadUsageSummary } = useBillingStore();
|
||||
@@ -40,15 +41,15 @@ export default function UsageChartWidget() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end items-center mb-4">
|
||||
<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"
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange(e.target.value as 'week' | 'month' | 'year')}
|
||||
>
|
||||
<option value="week">Last 7 Days</option>
|
||||
<option value="month">This Month</option>
|
||||
<option value="year">This Year</option>
|
||||
</select>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'week', label: 'Last 7 Days' },
|
||||
{ value: 'month', label: 'This Month' },
|
||||
{ value: 'year', label: 'This Year' },
|
||||
]}
|
||||
defaultValue={dateRange}
|
||||
onChange={(val) => setDateRange(val as 'week' | 'month' | 'year')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card } from '../ui/card/Card';
|
||||
import Button from '../ui/button/Button';
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/solid';
|
||||
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
|
||||
import { WORKFLOW_COLORS } from '../../config/colors.config';
|
||||
@@ -55,17 +56,15 @@ function TimeFilterButtons({ value, onChange }: TimeFilterProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
<Button
|
||||
key={option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={`px-2 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
value === option.value
|
||||
? 'bg-brand-500 text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
variant={value === option.value ? 'primary' : 'ghost'}
|
||||
tone={value === option.value ? 'brand' : 'neutral'}
|
||||
size="xs"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { MoreDotIcon } from "../../icons";
|
||||
import CountryMap from "./CountryMap";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
|
||||
export default function DemographicCard() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -26,9 +27,9 @@ export default function DemographicCard() {
|
||||
</p>
|
||||
</div>
|
||||
<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" />
|
||||
</button>
|
||||
</IconButton>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { MoreDotIcon } from "../../icons";
|
||||
import { useState } from "react";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
|
||||
export default function MonthlySalesChart() {
|
||||
const options: ApexOptions = {
|
||||
@@ -107,9 +108,9 @@ export default function MonthlySalesChart() {
|
||||
Monthly Sales
|
||||
</h3>
|
||||
<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" />
|
||||
</button>
|
||||
</IconButton>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { MoreDotIcon } from "../../icons";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
|
||||
export default function MonthlyTarget() {
|
||||
const series = [75.55];
|
||||
@@ -76,9 +77,9 @@ export default function MonthlyTarget() {
|
||||
</p>
|
||||
</div>
|
||||
<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" />
|
||||
</button>
|
||||
</IconButton>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={closeDropdown}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import Badge from "../ui/badge/Badge";
|
||||
import Button from "../ui/button/Button";
|
||||
|
||||
// Define the TypeScript interface for the table rows
|
||||
interface Product {
|
||||
@@ -79,7 +80,7 @@ export default function RecentOrders() {
|
||||
</div>
|
||||
|
||||
<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
|
||||
className="stroke-current fill-white dark:fill-gray-800"
|
||||
width="20"
|
||||
@@ -116,10 +117,10 @@ export default function RecentOrders() {
|
||||
/>
|
||||
</svg>
|
||||
Filter
|
||||
</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>
|
||||
<Button variant="outline" size="sm">
|
||||
See all
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import Input from './input/InputField';
|
||||
import TextArea from './input/TextArea';
|
||||
import SelectDropdown from './SelectDropdown';
|
||||
import Label from './Label';
|
||||
|
||||
@@ -97,19 +98,13 @@ export default function FormFieldRenderer({
|
||||
className="w-full"
|
||||
/>
|
||||
) : field.type === '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)}
|
||||
<TextArea
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={field.rows || 4}
|
||||
value={value}
|
||||
onChange={(val) => onChange(field.key, val)}
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ThemeToggleButton } from "../common/ThemeToggleButton";
|
||||
import NotificationDropdown from "./NotificationDropdown";
|
||||
import UserDropdown from "./UserDropdown";
|
||||
import { Link } from "react-router-dom";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
import InputField from "../form/input/InputField";
|
||||
|
||||
// Define the interface for the props
|
||||
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">
|
||||
<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">
|
||||
<button
|
||||
className="block w-10 h-10 text-gray-500 lg:hidden dark:text-gray-400"
|
||||
<IconButton
|
||||
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}
|
||||
>
|
||||
{/* Hamburger Icon */}
|
||||
<svg
|
||||
className={`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="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="hidden"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
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
|
||||
className="lg:hidden"
|
||||
/>
|
||||
<IconButton
|
||||
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="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
className="hidden lg:flex"
|
||||
/>
|
||||
|
||||
<Link to="/" className="lg:hidden">
|
||||
<img
|
||||
@@ -91,30 +84,35 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
|
||||
/>
|
||||
</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}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
className="lg:hidden"
|
||||
/>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<form action="https://formbold.com/s/unique_form_id" method="POST">
|
||||
<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
|
||||
className="fill-gray-500 dark:fill-gray-400"
|
||||
width="20"
|
||||
@@ -130,17 +128,17 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
</span>
|
||||
<InputField
|
||||
type="text"
|
||||
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> K </span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useState, useRef, useEffect } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
import Button from "../ui/button/Button";
|
||||
import {
|
||||
useNotificationStore,
|
||||
formatNotificationTime,
|
||||
@@ -126,9 +128,10 @@ export default function NotificationDropdown() {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
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"
|
||||
<IconButton
|
||||
ref={buttonRef as React.RefObject<HTMLButtonElement>}
|
||||
variant="outline"
|
||||
className="relative dropdown-toggle h-11 w-11"
|
||||
onClick={handleClick}
|
||||
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
||||
>
|
||||
@@ -153,7 +156,7 @@ export default function NotificationDropdown() {
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</IconButton>
|
||||
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
@@ -174,16 +177,21 @@ export default function NotificationDropdown() {
|
||||
</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleDropdown}
|
||||
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||
aria-label="Close notifications"
|
||||
>
|
||||
<svg
|
||||
@@ -200,7 +208,7 @@ export default function NotificationDropdown() {
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useState, useRef } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
import Button from "../ui/button/Button";
|
||||
import {
|
||||
useNotificationStore,
|
||||
formatNotificationTime,
|
||||
@@ -97,34 +99,38 @@ export default function NotificationDropdown() {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
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}
|
||||
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
||||
>
|
||||
{/* Notification badge */}
|
||||
{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">
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></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>
|
||||
/>
|
||||
{/* Notification badge */}
|
||||
{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">
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
@@ -145,18 +151,23 @@ export default function NotificationDropdown() {
|
||||
</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<IconButton
|
||||
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"
|
||||
>
|
||||
icon={
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
@@ -171,7 +182,8 @@ export default function NotificationDropdown() {
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
import Button from "../ui/button/Button";
|
||||
|
||||
export default function UserDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -26,9 +27,11 @@ export default function UserDropdown() {
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={toggleDropdown}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
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">
|
||||
@@ -62,7 +65,7 @@ export default function UserDropdown() {
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
@@ -162,8 +165,10 @@ export default function UserDropdown() {
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
@@ -182,7 +187,7 @@ export default function UserDropdown() {
|
||||
/>
|
||||
</svg>
|
||||
Sign out
|
||||
</button>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Card } from '../ui/card';
|
||||
import Button from '../ui/button/Button';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import InputField from '../form/input/InputField';
|
||||
import {
|
||||
CloseIcon,
|
||||
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">
|
||||
Site Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<InputField
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -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">
|
||||
Website Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<InputField
|
||||
value={websiteAddress}
|
||||
onChange={(e) => setWebsiteAddress(e.target.value)}
|
||||
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>
|
||||
|
||||
|
||||
@@ -93,18 +93,17 @@ export default function Step1Welcome({ onNext, onSkip }: Step1WelcomeProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onSkip}
|
||||
className="text-gray-500"
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onNext}
|
||||
className="gap-2"
|
||||
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||
>
|
||||
Let's Get Started
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -278,20 +278,19 @@ export default function Step2AddSite({
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onBack}
|
||||
className="gap-2"
|
||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreateSite}
|
||||
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'}
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -245,24 +245,13 @@ export default function Step3ConnectIntegration({
|
||||
size="sm"
|
||||
onClick={handleTestConnection}
|
||||
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 ? (
|
||||
<>
|
||||
<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
|
||||
</>
|
||||
)}
|
||||
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -278,15 +267,16 @@ export default function Step3ConnectIntegration({
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onBack}
|
||||
className="gap-2"
|
||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onSkip}
|
||||
>
|
||||
Skip for now
|
||||
@@ -294,10 +284,9 @@ export default function Step3ConnectIntegration({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onNext}
|
||||
className="gap-2"
|
||||
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import Button from '../../ui/button/Button';
|
||||
import IconButton from '../../ui/button/IconButton';
|
||||
import InputField from '../../form/input/InputField';
|
||||
import { Card } from '../../ui/card';
|
||||
import Badge from '../../ui/badge/Badge';
|
||||
import Alert from '../../ui/alert/Alert';
|
||||
@@ -158,14 +160,11 @@ export default function Step4AddKeywords({
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
<InputField
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
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>
|
||||
<Button
|
||||
@@ -199,12 +198,14 @@ export default function Step4AddKeywords({
|
||||
className="gap-1 pr-1"
|
||||
>
|
||||
{keyword}
|
||||
<button
|
||||
<IconButton
|
||||
icon={<CloseIcon className="w-3 h-3" />}
|
||||
onClick={() => handleRemoveKeyword(keyword)}
|
||||
className="ml-1 p-0.5 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
|
||||
>
|
||||
<CloseIcon className="w-3 h-3" />
|
||||
</button>
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="ml-1"
|
||||
aria-label="Remove keyword"
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -214,12 +215,14 @@ export default function Step4AddKeywords({
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
|
||||
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
|
||||
{keywords.length > 0 && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setKeywords([])}
|
||||
className="text-error-500 hover:text-error-600"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -251,15 +254,16 @@ export default function Step4AddKeywords({
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onBack}
|
||||
className="gap-2"
|
||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onSkip}
|
||||
>
|
||||
Skip for now
|
||||
@@ -268,10 +272,9 @@ export default function Step4AddKeywords({
|
||||
variant="primary"
|
||||
onClick={handleSubmitKeywords}
|
||||
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' : ''}`}
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,10 +157,10 @@ export default function Step5Complete({
|
||||
size="lg"
|
||||
onClick={onComplete}
|
||||
disabled={isLoading}
|
||||
className="gap-2 w-full"
|
||||
fullWidth
|
||||
endIcon={!isLoading ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Go to Dashboard'}
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import './blocks.css';
|
||||
import Button from '../../ui/button/Button';
|
||||
|
||||
export interface CTABlockProps {
|
||||
title: string;
|
||||
@@ -44,13 +45,12 @@ export function CTABlock({
|
||||
{primaryCtaLabel}
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="shared-button shared-button--primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onPrimaryCtaClick}
|
||||
>
|
||||
{primaryCtaLabel}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{secondaryCtaLabel && (
|
||||
@@ -59,13 +59,12 @@ export function CTABlock({
|
||||
{secondaryCtaLabel}
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="shared-button shared-button--secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onSecondaryCtaClick}
|
||||
>
|
||||
{secondaryCtaLabel}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import './blocks.css';
|
||||
import InputField from '../../form/input/InputField';
|
||||
import TextArea from '../../form/input/TextArea';
|
||||
import Button from '../../ui/button/Button';
|
||||
|
||||
export interface ContactFormBlockProps {
|
||||
title?: string;
|
||||
@@ -49,33 +52,25 @@ export function ContactFormBlock({
|
||||
{field.required && <span className="shared-contact-form__required">*</span>}
|
||||
</label>
|
||||
{field.type === 'textarea' ? (
|
||||
<textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
<TextArea
|
||||
placeholder={field.placeholder}
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
className="shared-contact-form__input"
|
||||
onChange={(val) => handleChange(field.name, val)}
|
||||
rows={4}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
<InputField
|
||||
type={field.type}
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
className="shared-contact-form__input"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button type="submit" className="shared-button shared-button--primary">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import './blocks.css';
|
||||
import Button from '../../ui/button/Button';
|
||||
|
||||
export interface HeroBlockProps {
|
||||
eyebrow?: string;
|
||||
@@ -18,9 +19,9 @@ export function HeroBlock({ eyebrow, title, subtitle, ctaLabel, onCtaClick, supp
|
||||
{subtitle && <p className="shared-hero__subtitle">{subtitle}</p>}
|
||||
{supportingContent && <div className="shared-hero__support">{supportingContent}</div>}
|
||||
{ctaLabel && (
|
||||
<button type="button" className="shared-button" onClick={onCtaClick}>
|
||||
<Button variant="primary" onClick={onCtaClick}>
|
||||
{ctaLabel}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import './blocks.css';
|
||||
import Button from '../../ui/button/Button';
|
||||
|
||||
export interface ProductItem {
|
||||
name: string;
|
||||
@@ -41,13 +42,12 @@ export function ProductsBlock({
|
||||
)}
|
||||
{product.price && <p className="shared-products__price">{product.price}</p>}
|
||||
{product.ctaLabel && (
|
||||
<button
|
||||
type="button"
|
||||
className="shared-button"
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={product.onCtaClick}
|
||||
>
|
||||
{product.ctaLabel}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card } from '../ui/card';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import Button from '../ui/button/Button';
|
||||
// import { fetchSiteProgress, SiteProgress } from '../../services/api';
|
||||
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">
|
||||
<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">
|
||||
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>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
|
||||
import Button from '../../ui/button/Button';
|
||||
import Label from '../../form/Label';
|
||||
import TextArea from '../../form/input/TextArea';
|
||||
import InputField from '../../form/input/InputField';
|
||||
import { useToast } from '../../ui/toast/ToastContainer';
|
||||
|
||||
export interface StyleSettings {
|
||||
@@ -106,53 +107,41 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant={activeTab === 'css' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,18 +174,18 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
||||
<div key={colorKey}>
|
||||
<Label>{colorKey.charAt(0).toUpperCase() + colorKey.slice(1)} Color</Label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<input
|
||||
<InputField
|
||||
type="color"
|
||||
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || '#000000'}
|
||||
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"
|
||||
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || ''}
|
||||
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
||||
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>
|
||||
@@ -211,45 +200,45 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Font Family</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.typography?.fontFamily || ''}
|
||||
onChange={(e) => handleTypographyChange('fontFamily', e.target.value)}
|
||||
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>
|
||||
<Label>Heading Font</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.typography?.headingFont || ''}
|
||||
onChange={(e) => handleTypographyChange('headingFont', e.target.value)}
|
||||
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>
|
||||
<Label>Base Font Size</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.typography?.fontSize || ''}
|
||||
onChange={(e) => handleTypographyChange('fontSize', e.target.value)}
|
||||
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>
|
||||
<Label>Line Height</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.typography?.lineHeight || ''}
|
||||
onChange={(e) => handleTypographyChange('lineHeight', e.target.value)}
|
||||
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>
|
||||
@@ -262,12 +251,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Base Spacing Unit</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.spacing?.base || ''}
|
||||
onChange={(e) => handleSpacingChange('base', e.target.value)}
|
||||
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">
|
||||
Base unit for spacing calculations (e.g., 8px, 1rem)
|
||||
@@ -276,12 +265,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
|
||||
|
||||
<div>
|
||||
<Label>Spacing Scale</Label>
|
||||
<input
|
||||
<InputField
|
||||
type="text"
|
||||
value={styleSettings.spacing?.scale || ''}
|
||||
onChange={(e) => handleSpacingChange('scale', e.target.value)}
|
||||
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">
|
||||
Multiplier for spacing scale (e.g., 1.5 for 1.5x spacing)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
|
||||
import Label from '../../form/Label';
|
||||
import SelectDropdown from '../../form/SelectDropdown';
|
||||
import Button from '../../ui/button/Button';
|
||||
import InputField from '../../form/input/InputField';
|
||||
|
||||
export interface TemplateCustomization {
|
||||
layout: string;
|
||||
@@ -68,54 +69,42 @@ export default function TemplateCustomizer({
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant={activeTab === 'layout' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +192,7 @@ export default function TemplateCustomizer({
|
||||
<div>
|
||||
<Label>Primary Color</Label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<input
|
||||
<InputField
|
||||
type="color"
|
||||
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
||||
onChange={(e) =>
|
||||
@@ -211,9 +200,9 @@ export default function TemplateCustomizer({
|
||||
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"
|
||||
value={customization.customStyles?.primaryColor || '#3b82f6'}
|
||||
onChange={(e) =>
|
||||
@@ -222,7 +211,7 @@ export default function TemplateCustomizer({
|
||||
})
|
||||
}
|
||||
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>
|
||||
@@ -230,7 +219,7 @@ export default function TemplateCustomizer({
|
||||
<div>
|
||||
<Label>Background Color</Label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<input
|
||||
<InputField
|
||||
type="color"
|
||||
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
||||
onChange={(e) =>
|
||||
@@ -238,9 +227,9 @@ export default function TemplateCustomizer({
|
||||
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"
|
||||
value={customization.customStyles?.backgroundColor || '#ffffff'}
|
||||
onChange={(e) =>
|
||||
@@ -249,7 +238,7 @@ export default function TemplateCustomizer({
|
||||
})
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
||||
import { SearchIcon, FilterIcon, CheckIcon } from '../../icons';
|
||||
import { Card } from '../../ui/card';
|
||||
import Button from '../../ui/button/Button';
|
||||
import InputField from '../../form/input/InputField';
|
||||
|
||||
export interface TemplateOption {
|
||||
id: string;
|
||||
@@ -106,47 +107,37 @@ export default function TemplateLibrary({
|
||||
<div className="space-y-4">
|
||||
{/* Search and Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
<div className="flex-1">
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="Search templates..."
|
||||
value={searchTerm}
|
||||
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 className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant={showFeaturedOnly ? 'primary' : 'outline'}
|
||||
onClick={() => setShowFeaturedOnly(!showFeaturedOnly)}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
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'
|
||||
}`}
|
||||
startIcon={<FilterIcon className="w-4 h-4" />}
|
||||
>
|
||||
<FilterIcon className="w-4 h-4 inline mr-2" />
|
||||
Featured Only
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
<Button
|
||||
key={category}
|
||||
type="button"
|
||||
variant={selectedCategory === category ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
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)}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import Label from '../form/Label';
|
||||
import Input from '../form/input/InputField';
|
||||
import Checkbox from '../form/input/Checkbox';
|
||||
import Switch from '../form/switch/Switch';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
@@ -218,26 +220,11 @@ export default function WordPressIntegrationForm({
|
||||
|
||||
{/* Toggle Switch */}
|
||||
{apiKey && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
|
||||
</span>
|
||||
<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>
|
||||
<Switch
|
||||
label={integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
|
||||
checked={integrationEnabled}
|
||||
onChange={(checked) => handleToggleIntegration(checked)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -293,30 +280,32 @@ export default function WordPressIntegrationForm({
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
id="api-key"
|
||||
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"
|
||||
<Input
|
||||
className="w-full min-w-[360px] pr-[90px] font-mono"
|
||||
readOnly
|
||||
type={apiKeyVisible ? 'text' : 'password'}
|
||||
value={apiKeyVisible ? apiKey : maskApiKey(apiKey)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
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" />
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<div className="group relative inline-block">
|
||||
<button
|
||||
<IconButton
|
||||
onClick={handleRegenerateApiKey}
|
||||
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"
|
||||
>
|
||||
<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="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">
|
||||
@@ -326,9 +315,9 @@ export default function WordPressIntegrationForm({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<IconButton
|
||||
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 ? (
|
||||
<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" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -354,22 +343,23 @@ export default function WordPressIntegrationForm({
|
||||
</td>
|
||||
<td className="px-5 py-3 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<IconButton
|
||||
onClick={handleRegenerateApiKey}
|
||||
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"
|
||||
>
|
||||
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleRevokeApiKey}
|
||||
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"
|
||||
>
|
||||
<TrashBinIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { PlusIcon, HorizontaLDots } from "../../icons";
|
||||
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
|
||||
import RelationshipMap from "./RelationshipMap";
|
||||
import Button from "../ui/button/Button";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
|
||||
export interface Task {
|
||||
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 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">
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "All"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
All Tasks
|
||||
<span
|
||||
@@ -121,15 +122,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
||||
>
|
||||
{tasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "Todo"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
To do
|
||||
<span
|
||||
@@ -141,15 +141,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
||||
>
|
||||
{todoTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "InProgress"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
In Progress
|
||||
<span
|
||||
@@ -161,15 +160,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
||||
>
|
||||
{inProgressTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "Completed"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
Completed
|
||||
<span
|
||||
@@ -181,24 +179,28 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
|
||||
>
|
||||
{completedTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
</svg>
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
startIcon={<FilterIcon className="fill-current" />}
|
||||
>
|
||||
Filter & Sort
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
<PlusIcon className="fill-current" width={20} height={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,12 +326,14 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
||||
</h3>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
icon={<HorizontaLDots className="fill-current" />}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
title="More options"
|
||||
onClick={() => onDropdownToggle(status)}
|
||||
className="text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<HorizontaLDots className="fill-current" width={24} height={24} />
|
||||
</button>
|
||||
/>
|
||||
{openDropdown && (
|
||||
<>
|
||||
<div
|
||||
@@ -337,15 +341,15 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
|
||||
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">
|
||||
<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
|
||||
</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>
|
||||
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||
Delete
|
||||
</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>
|
||||
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||
Clear All
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ArrowRightIcon } from "../../icons";
|
||||
import Button from "../ui/button/Button";
|
||||
|
||||
export interface RelationshipData {
|
||||
taskId: number;
|
||||
@@ -43,13 +44,15 @@ const RelationshipMap: React.FC<RelationshipMapProps> = ({
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={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}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
{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">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { PlusIcon, HorizontaLDots } from "../../icons";
|
||||
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
|
||||
import { Task } from "./KanbanBoard";
|
||||
import Button from "../ui/button/Button";
|
||||
import IconButton from "../ui/button/IconButton";
|
||||
import Checkbox from "../form/input/Checkbox";
|
||||
|
||||
interface TaskListProps {
|
||||
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 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">
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "All"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
All Tasks
|
||||
<span
|
||||
@@ -84,15 +86,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||
>
|
||||
{tasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "Todo"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
To do
|
||||
<span
|
||||
@@ -104,15 +105,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||
>
|
||||
{todoTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "InProgress"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
In Progress
|
||||
<span
|
||||
@@ -124,15 +124,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||
>
|
||||
{inProgressTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
selectedFilter === "Completed"
|
||||
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
}`}
|
||||
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
|
||||
>
|
||||
Completed
|
||||
<span
|
||||
@@ -144,24 +143,28 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||
>
|
||||
{completedTasks.length}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
</svg>
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
startIcon={<FilterIcon className="fill-current" />}
|
||||
>
|
||||
Filter & Sort
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="sm"
|
||||
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
|
||||
<PlusIcon className="fill-current" width={20} height={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,12 +260,14 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
|
||||
</h3>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
icon={<HorizontaLDots className="fill-current" />}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
title="More options"
|
||||
onClick={() => onDropdownToggle(title.toLowerCase().replace(" ", "_"))}
|
||||
className="text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<HorizontaLDots className="fill-current" width={24} height={24} />
|
||||
</button>
|
||||
/>
|
||||
{openDropdown && (
|
||||
<>
|
||||
<div
|
||||
@@ -270,15 +275,15 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
|
||||
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">
|
||||
<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
|
||||
</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>
|
||||
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||
Delete
|
||||
</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>
|
||||
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
|
||||
Clear All
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -334,27 +339,18 @@ const TaskListItem: React.FC<TaskListItemProps> = ({ task, checked, onClick, onC
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<label htmlFor={`taskCheckbox-${task.id}`} className="w-full cursor-pointer">
|
||||
<div className="relative flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
<div className="w-full flex items-start">
|
||||
<div className="mr-3">
|
||||
<Checkbox
|
||||
id={`taskCheckbox-${task.id}`}
|
||||
className="sr-only taskCheckbox"
|
||||
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>
|
||||
</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 className="flex flex-col-reverse items-start justify-end w-full gap-3 xl:flex-row xl:items-center xl:gap-5">
|
||||
|
||||
145
frontend/src/components/ui/button/IconButton.tsx
Normal file
145
frontend/src/components/ui/button/IconButton.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* IconButton Component
|
||||
*
|
||||
* 🔒 STYLE LOCKED - See DESIGN_SYSTEM.md for available variants and sizes.
|
||||
* Icon-only button component for actions that only need an icon.
|
||||
*/
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type IconButtonSize = "xs" | "sm" | "md" | "lg";
|
||||
type IconButtonVariant = "solid" | "outline" | "ghost";
|
||||
type IconButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
|
||||
type IconButtonShape = "rounded" | "circle";
|
||||
|
||||
interface IconButtonProps {
|
||||
icon: ReactNode;
|
||||
size?: IconButtonSize;
|
||||
variant?: IconButtonVariant;
|
||||
tone?: IconButtonTone;
|
||||
shape?: IconButtonShape;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
title?: string;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
const toneMap: Record<
|
||||
IconButtonTone,
|
||||
{
|
||||
solid: string;
|
||||
outline: string;
|
||||
ghost: string;
|
||||
ring: string;
|
||||
}
|
||||
> = {
|
||||
brand: {
|
||||
solid: "bg-brand-500 text-white hover:bg-brand-600",
|
||||
outline: "text-brand-600 ring-1 ring-brand-200 hover:bg-brand-50 dark:ring-brand-500/40 dark:text-brand-300 dark:hover:bg-brand-500/10",
|
||||
ghost: "text-brand-600 hover:bg-brand-50 dark:text-brand-300 dark:hover:bg-brand-500/10",
|
||||
ring: "focus-visible:ring-brand-500",
|
||||
},
|
||||
success: {
|
||||
solid: "bg-success-500 text-white hover:bg-success-600",
|
||||
outline: "text-success-600 ring-1 ring-success-200 hover:bg-success-50 dark:ring-success-500/40 dark:text-success-300 dark:hover:bg-success-500/10",
|
||||
ghost: "text-success-600 hover:bg-success-50 dark:text-success-300 dark:hover:bg-success-500/10",
|
||||
ring: "focus-visible:ring-success-500",
|
||||
},
|
||||
warning: {
|
||||
solid: "bg-warning-500 text-white hover:bg-warning-600",
|
||||
outline: "text-warning-600 ring-1 ring-warning-200 hover:bg-warning-50 dark:ring-warning-500/40 dark:text-warning-300 dark:hover:bg-warning-500/10",
|
||||
ghost: "text-warning-600 hover:bg-warning-50 dark:text-warning-300 dark:hover:bg-warning-500/10",
|
||||
ring: "focus-visible:ring-warning-500",
|
||||
},
|
||||
danger: {
|
||||
solid: "bg-error-500 text-white hover:bg-error-600",
|
||||
outline: "text-error-600 ring-1 ring-error-200 hover:bg-error-50 dark:ring-error-500/40 dark:text-error-300 dark:hover:bg-error-500/10",
|
||||
ghost: "text-error-600 hover:bg-error-50 dark:text-error-300 dark:hover:bg-error-500/10",
|
||||
ring: "focus-visible:ring-error-500",
|
||||
},
|
||||
neutral: {
|
||||
solid: "bg-gray-700 text-white hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500",
|
||||
outline: "text-gray-700 ring-1 ring-gray-300 hover:bg-gray-100 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-800",
|
||||
ghost: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800",
|
||||
ring: "focus-visible:ring-gray-400",
|
||||
},
|
||||
};
|
||||
|
||||
const sizeClasses: Record<IconButtonSize, string> = {
|
||||
xs: "h-6 w-6 text-xs",
|
||||
sm: "h-8 w-8 text-sm",
|
||||
md: "h-10 w-10 text-base",
|
||||
lg: "h-12 w-12 text-lg",
|
||||
};
|
||||
|
||||
const iconSizeClasses: Record<IconButtonSize, string> = {
|
||||
xs: "[&>svg]:w-3 [&>svg]:h-3",
|
||||
sm: "[&>svg]:w-4 [&>svg]:h-4",
|
||||
md: "[&>svg]:w-5 [&>svg]:h-5",
|
||||
lg: "[&>svg]:w-6 [&>svg]:h-6",
|
||||
};
|
||||
|
||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
size = "md",
|
||||
variant = "ghost",
|
||||
tone = "neutral",
|
||||
shape = "rounded",
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
type = "button",
|
||||
title,
|
||||
"aria-label": ariaLabel,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const toneStyles = toneMap[tone];
|
||||
|
||||
const variantClasses: Record<IconButtonVariant, string> = {
|
||||
solid: toneStyles.solid,
|
||||
outline: clsx("bg-transparent transition-colors", toneStyles.outline),
|
||||
ghost: toneStyles.ghost,
|
||||
};
|
||||
|
||||
const baseClasses =
|
||||
"inline-flex items-center justify-center transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
const shapeClasses = shape === "circle" ? "rounded-full" : "rounded-lg";
|
||||
|
||||
const computedClass = twMerge(
|
||||
clsx(
|
||||
baseClasses,
|
||||
sizeClasses[size],
|
||||
iconSizeClasses[size],
|
||||
shapeClasses,
|
||||
variantClasses[variant],
|
||||
toneStyles.ring,
|
||||
className,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={computedClass}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
aria-label={ariaLabel || title}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
IconButton.displayName = "IconButton";
|
||||
|
||||
export default IconButton;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import Checkbox from "../../form/input/Checkbox";
|
||||
|
||||
interface ListProps {
|
||||
children: ReactNode;
|
||||
@@ -134,23 +135,13 @@ export const ListCheckboxItem: React.FC<ListCheckboxItemProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<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">
|
||||
<label className="flex items-center space-x-3 group cursor-pointer">
|
||||
<div className="relative w-5 h-5">
|
||||
<input
|
||||
id={id}
|
||||
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"
|
||||
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>
|
||||
<Checkbox
|
||||
id={id}
|
||||
label={label}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(val) => onChange?.(val)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { AngleLeftIcon, AngleRightIcon } from "../../../icons";
|
||||
import Select from "../../form/Select";
|
||||
|
||||
interface CompactPaginationProps {
|
||||
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">
|
||||
Show:
|
||||
</label>
|
||||
<select
|
||||
id="page-size"
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
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"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
</select>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '10', label: '10' },
|
||||
{ value: '20', label: '20' },
|
||||
{ value: '50', label: '50' },
|
||||
]}
|
||||
defaultValue={String(pageSize)}
|
||||
onChange={(val) => onPageSizeChange(Number(val))}
|
||||
/>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
per page
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '../button/Button';
|
||||
|
||||
export interface PricingPlan {
|
||||
id?: number;
|
||||
@@ -109,8 +110,10 @@ export default function PricingTable({
|
||||
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
||||
}}
|
||||
></span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
billingPeriod === 'monthly'
|
||||
@@ -119,9 +122,11 @@ export default function PricingTable({
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
size="sm"
|
||||
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 ${
|
||||
billingPeriod === 'annually'
|
||||
@@ -130,7 +135,7 @@ export default function PricingTable({
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{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">
|
||||
@@ -230,7 +235,10 @@ export default function PricingTable({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="md"
|
||||
onClick={() => handlePlanClick(plan)}
|
||||
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 ${
|
||||
@@ -240,7 +248,7 @@ export default function PricingTable({
|
||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
>
|
||||
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -304,7 +312,10 @@ export default function PricingTable({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="md"
|
||||
onClick={() => handlePlanClick(plan)}
|
||||
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 ${
|
||||
@@ -314,7 +325,7 @@ export default function PricingTable({
|
||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{plan.buttonText || (isHighlighted ? 'Choose This Plan' : 'Choose Starter')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -376,7 +387,10 @@ export default function PricingTable({
|
||||
>
|
||||
{plan.period || 'For a Lifetime'}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="md"
|
||||
onClick={() => handlePlanClick(plan)}
|
||||
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 ${
|
||||
@@ -388,7 +402,7 @@ export default function PricingTable({
|
||||
}`}
|
||||
>
|
||||
{plan.buttonText || (plan.disabled ? 'Current Plan' : 'Try for Free')}
|
||||
</button>
|
||||
</Button>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{plan.features.map((feature, idx) => {
|
||||
const isExcluded = feature.startsWith('!');
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { CheckIcon } from '../../../icons';
|
||||
import Button from '../button/Button';
|
||||
|
||||
export interface PricingPlan {
|
||||
id: number;
|
||||
@@ -66,26 +67,20 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
||||
<div className="flex justify-center mb-8">
|
||||
<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">
|
||||
<button
|
||||
<Button
|
||||
variant={billingPeriod === 'monthly' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={billingPeriod === 'annual' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{billingPeriod === 'annual' && (
|
||||
<span className="badge-success">
|
||||
@@ -202,13 +197,14 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
className={plan.highlighted ? 'btn-primary' : 'btn-outline'}
|
||||
<Button
|
||||
variant={plan.highlighted ? 'primary' : 'outline'}
|
||||
fullWidth
|
||||
onClick={() => onPlanSelect?.(plan)}
|
||||
disabled={plan.disabled}
|
||||
>
|
||||
{plan.buttonText}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user