componenets standardization 1

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

View File

@@ -7,6 +7,8 @@ import { EventInput, DateSelectArg, EventClickArg } from "@fullcalendar/core";
import { Modal } from "../components/ui/modal";
import { useModal } from "../hooks/useModal";
import PageMeta from "../components/common/PageMeta";
import Button from "../components/ui/button/Button";
import InputField from "../components/form/input/InputField";
interface CalendarEvent extends EventInput {
extendedProps: {
@@ -166,12 +168,11 @@ const Calendar: React.FC = () => {
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Event Title
</label>
<input
<InputField
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
@@ -220,12 +221,11 @@ const Calendar: React.FC = () => {
Enter Start Date
</label>
<div className="relative">
<input
<InputField
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
@@ -235,31 +235,32 @@ const Calendar: React.FC = () => {
Enter End Date
</label>
<div className="relative">
<input
<InputField
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
<Button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
variant="outline"
tone="neutral"
size="md"
>
Close
</button>
<button
</Button>
<Button
onClick={handleAddOrUpdateEvent}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
variant="primary"
tone="brand"
size="md"
>
{selectedEvent ? "Update Changes" : "Add Event"}
</button>
</Button>
</div>
</div>
</Modal>

View File

@@ -8,6 +8,8 @@ import { Pagination } from '../components/ui/pagination/Pagination';
import { Card, CardImage, CardTitle, CardDescription, CardAction, CardIcon } from '../components/ui/card/Card';
import ChartTab from '../components/common/ChartTab';
import PageMeta from '../components/common/PageMeta';
import InputField from '../components/form/input/InputField';
import TextArea from '../components/form/input/TextArea';
export default function Components() {
// Alert modals state
@@ -257,9 +259,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Emirhan"
/>
</div>
@@ -267,9 +268,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Boruch"
/>
</div>
@@ -279,9 +279,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="emirhanboruch55@gmail.com"
/>
</div>
@@ -289,9 +288,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone
</label>
<input
<InputField
type="tel"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="+09 363 398 46"
/>
</div>
@@ -300,9 +298,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Bio
</label>
<textarea
<TextArea
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Team Manager"
/>
</div>
@@ -449,34 +446,34 @@ function ButtonGroupsShowcase() {
<div className="space-y-6">
{/* Default Button Group */}
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
<Button variant="ghost" className="rounded-r-none border-r-0">
Left
</button>
<button className="px-4 py-2 text-sm font-medium text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="rounded-none border-x border-gray-300 dark:border-gray-700">
Center
</button>
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="rounded-l-none border-l-0">
Right
</button>
</Button>
</div>
{/* Icon Button Group */}
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
<button className="p-2 text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
<Button variant="ghost" className="p-2 rounded-r-none border-r-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
</button>
<button className="p-2 text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="p-2 rounded-none border-x border-gray-300 dark:border-gray-700">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1z" clipRule="evenodd" />
</svg>
</button>
<button className="p-2 text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="p-2 rounded-l-none border-l-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 10a1 1 0 011 1h12a1 1 0 110-2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
</button>
</Button>
</div>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import Button from '../../components/ui/button/Button';
import { linkerApi } from '../../api/linker.api';
import { fetchContent, Content as ContentType } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
@@ -170,10 +171,11 @@ export default function LinkerContentList() {
{item.linker_version || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
<Button
onClick={() => handleLink(item.id)}
disabled={isProcessing || processing === -1}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
variant="primary"
size="sm"
>
{isProcessing ? (
<>
@@ -186,7 +188,7 @@ export default function LinkerContentList() {
Add Links
</>
)}
</button>
</Button>
</td>
</tr>
);
@@ -202,20 +204,22 @@ export default function LinkerContentList() {
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
<Button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
variant="outline"
size="sm"
>
Previous
</button>
<button
</Button>
<Button
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={currentPage * pageSize >= totalCount}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
variant="outline"
size="sm"
>
Next
</button>
</Button>
</div>
</div>
)}

View File

@@ -12,6 +12,9 @@ import { OptimizationScores } from '../../components/optimizer/OptimizationScore
import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import Button from '../../components/ui/button/Button';
export default function OptimizerContentSelector() {
const navigate = useNavigate();
@@ -153,21 +156,21 @@ export default function OptimizerContentSelector() {
/>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<select
value={entryPoint}
onChange={(e) => setEntryPoint(e.target.value as EntryPoint)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="auto">Auto-detect</option>
<option value="writer">From Writer</option>
<option value="wordpress">From WordPress</option>
<option value="external">From External</option>
<option value="manual">Manual</option>
</select>
<button
<Select
options={[
{ value: 'auto', label: 'Auto-detect' },
{ value: 'writer', label: 'From Writer' },
{ value: 'wordpress', label: 'From WordPress' },
{ value: 'external', label: 'From External' },
{ value: 'manual', label: 'Manual' },
]}
defaultValue={entryPoint}
onChange={(val) => setEntryPoint(val as EntryPoint)}
/>
<Button
variant="primary"
onClick={handleBatchOptimize}
disabled={selectedIds.length === 0 || processing.length > 0}
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{processing.length > 0 ? (
<>
@@ -180,7 +183,7 @@ export default function OptimizerContentSelector() {
Optimize Selected ({selectedIds.length})
</>
)}
</button>
</Button>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-6">
@@ -202,11 +205,9 @@ export default function OptimizerContentSelector() {
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="px-6 py-3 text-left">
<input
type="checkbox"
<Checkbox
checked={selectedIds.length === filteredContent.length && filteredContent.length > 0}
onChange={toggleSelectAll}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
@@ -238,11 +239,9 @@ export default function OptimizerContentSelector() {
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-brand-50 dark:bg-brand-900/20' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
<Checkbox
checked={isSelected}
onChange={() => toggleSelection(item.id)}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
@@ -266,10 +265,11 @@ export default function OptimizerContentSelector() {
{item.optimizer_version || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
<Button
variant="primary"
size="sm"
onClick={() => handleOptimize(item.id)}
disabled={isProcessing || processing.length > 0}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isProcessing ? (
<>
@@ -282,7 +282,7 @@ export default function OptimizerContentSelector() {
Optimize
</>
)}
</button>
</Button>
</td>
</tr>
);
@@ -298,20 +298,22 @@ export default function OptimizerContentSelector() {
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Previous
</button>
<button
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={currentPage * pageSize >= totalCount}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Next
</button>
</Button>
</div>
</div>
)}

View File

@@ -1,6 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, Link } from "react-router-dom";
import { useAuthStore } from "../store/authStore";
import InputField from "../components/form/input/InputField";
import TextArea from "../components/form/input/TextArea";
import Label from "../components/form/Label";
import Button from "../components/ui/button/Button";
const PLAN_COPY: Record<string, { name: string; price: string; content: string }> = {
starter: { name: "Starter", price: "$49/mo", content: "50 content pieces/month" },
@@ -76,42 +80,39 @@ export default function Payment() {
)}
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-800">
Contact email
<input
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="you@example.com"
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
/>
</label>
<label className="block text-sm font-medium text-gray-800">
Notes (optional)
<textarea
<InputField
label="Contact email"
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="you@example.com"
/>
<div>
<Label className="mb-2">Notes (optional)</Label>
<TextArea
value={note}
onChange={(e) => setNote(e.target.value)}
onChange={(value) => setNote(value)}
placeholder="Company name, billing contact, or questions"
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
rows={3}
/>
</label>
</div>
</div>
<div className="flex items-center justify-between">
<Link to="/signup" className="text-sm text-gray-600 hover:text-gray-800">
Prefer the free plan? Start your trial
</Link>
<a
href={mailtoHref || "#"}
onClick={handleRequest}
className={`inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold text-white ${
contactEmail.trim() ? "bg-brand-600 hover:bg-brand-700" : "bg-brand-400 cursor-not-allowed"
}`}
aria-disabled={!contactEmail.trim()}
<Button
onClick={() => {
if (mailtoHref && contactEmail.trim()) {
window.location.href = mailtoHref;
}
}}
disabled={!contactEmail.trim()}
variant="primary"
>
Request payment instructions
</a>
</Button>
</div>
{error && <p className="text-sm text-error-600">{error}</p>}
</div>

View File

@@ -233,10 +233,10 @@ export default function ClusterDetail() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant="ghost"
onClick={() => handleTabChange('articles')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'articles'
? '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'
@@ -244,11 +244,11 @@ export default function ClusterDetail() {
>
<FileIcon className="w-4 h-4 inline mr-2" />
Articles
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('pages')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'pages'
? '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'
@@ -256,11 +256,11 @@ export default function ClusterDetail() {
>
<PageIcon className="w-4 h-4 inline mr-2" />
Pages
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('products')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'products'
? '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'
@@ -268,11 +268,11 @@ export default function ClusterDetail() {
>
<GridIcon className="w-4 h-4 inline mr-2" />
Products
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('taxonomy')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'taxonomy'
? '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'
@@ -280,7 +280,7 @@ export default function ClusterDetail() {
>
<TagIcon className="w-4 h-4 inline mr-2" />
Taxonomy
</button>
</Button>
</div>
</div>

View File

@@ -127,26 +127,28 @@ const CreditsAndBilling: React.FC = () => {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<button
<Button
variant="ghost"
onClick={() => setActiveTab('overview')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
activeTab === 'overview'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Overview
</button>
<button
</Button>
<Button
variant="ghost"
onClick={() => setActiveTab('transactions')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
activeTab === 'transactions'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Transactions ({transactions.length})
</button>
</Button>
</nav>
</div>

View File

@@ -5,6 +5,8 @@ import { useSettingsStore } from '../../store/settingsStore';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
export default function GeneralSettings() {
const toast = useToast();
@@ -49,13 +51,10 @@ export default function GeneralSettings() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="records_per_page">Records Per Page</Label>
<input
<InputField
id="records_per_page"
type="number"
min="5"
max="100"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={tableSettings.records_per_page}
value={tableSettings.records_per_page.toString()}
onChange={(e) => setTableSettings({
...tableSettings,
records_per_page: parseInt(e.target.value) || 20
@@ -65,10 +64,9 @@ export default function GeneralSettings() {
<div>
<Label htmlFor="default_sort">Default Sort Field</Label>
<input
<InputField
id="default_sort"
type="text"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={tableSettings.default_sort}
onChange={(e) => setTableSettings({
...tableSettings,
@@ -79,18 +77,17 @@ export default function GeneralSettings() {
<div>
<Label htmlFor="default_sort_direction">Default Sort Direction</Label>
<select
id="default_sort_direction"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
<Select
options={[
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
]}
value={tableSettings.default_sort_direction}
onChange={(e) => setTableSettings({
onChange={(value) => setTableSettings({
...tableSettings,
default_sort_direction: e.target.value as 'asc' | 'desc'
default_sort_direction: value as 'asc' | 'desc'
})}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
/>
</div>
</div>
</div>

View File

@@ -5,6 +5,8 @@ import FormModal, { FormField } from '../../components/common/FormModal';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Alert from '../../components/ui/alert/Alert';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import {
fetchSites,
createSite,
@@ -475,21 +477,20 @@ export default function Sites() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Industry
</label>
<select
<Select
options={[
{ value: '', label: 'Select an industry...' },
...industries.map((industry) => ({
value: industry.slug,
label: industry.name,
})),
]}
value={selectedIndustry}
onChange={(e) => {
setSelectedIndustry(e.target.value);
onChange={(value) => {
setSelectedIndustry(value);
setSelectedSectors([]); // Reset sectors when industry changes
}}
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
>
<option value="">Select an industry...</option>
{industries.map((industry) => (
<option key={industry.slug} value={industry.slug}>
{industry.name}
</option>
))}
</select>
/>
{selectedIndustry && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{industries.find(i => i.slug === selectedIndustry)?.description}
@@ -504,15 +505,14 @@ export default function Sites() {
</label>
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
{getIndustrySectors().map((sector) => (
<label
<div
key={sector.slug}
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
>
<input
type="checkbox"
<Checkbox
checked={selectedSectors.includes(sector.slug)}
onChange={(e) => {
if (e.target.checked) {
onChange={(checked) => {
if (checked) {
if (selectedSectors.length >= 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
@@ -522,17 +522,18 @@ export default function Sites() {
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
}
}}
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
label={
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
{sector.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{sector.description}
</div>
</div>
}
/>
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
{sector.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{sector.description}
</div>
</div>
</label>
</div>
))}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">

View File

@@ -9,6 +9,8 @@ import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { SearchIcon } from '../../icons';
@@ -140,63 +142,50 @@ export default function SiteContentManager() {
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
<InputField
placeholder="Search content..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white"
className="pl-10"
/>
</div>
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(e.target.value);
<Select
options={STATUS_OPTIONS}
defaultValue={statusFilter}
onChange={(value) => {
setStatusFilter(value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
/>
<select
value={sourceFilter}
onChange={(e) => {
setSourceFilter(e.target.value);
<Select
options={SOURCE_OPTIONS}
defaultValue={sourceFilter}
onChange={(value) => {
setSourceFilter(value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{SOURCE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
/>
<select
value={`${sortBy}-${sortDirection}`}
onChange={(e) => {
const [field, direction] = e.target.value.split('-');
<Select
options={[
{ value: 'created_at-desc', label: 'Newest First' },
{ value: 'created_at-asc', label: 'Oldest First' },
{ value: 'updated_at-desc', label: 'Recently Updated' },
{ value: 'title-asc', label: 'Title A-Z' },
{ value: 'title-desc', label: 'Title Z-A' },
]}
defaultValue={`${sortBy}-${sortDirection}`}
onChange={(value) => {
const [field, direction] = value.split('-');
setSortBy(field as typeof sortBy);
setSortDirection(direction as 'asc' | 'desc');
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
<option value="created_at-desc">Newest First</option>
<option value="created_at-asc">Oldest First</option>
<option value="updated_at-desc">Recently Updated</option>
<option value="title-asc">Title A-Z</option>
<option value="title-desc">Title Z-A</option>
</select>
/>
</div>
</Card>

View File

@@ -263,9 +263,11 @@ export default function SiteDashboard() {
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common site management tasks">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<button
<Button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<PageIcon className="h-6 w-6" />
@@ -275,11 +277,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View and edit pages</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/content`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
<FileIcon className="h-6 w-6" />
@@ -289,11 +293,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View and edit content</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
<PlugInIcon className="h-6 w-6" />
@@ -303,11 +309,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">Manage connections</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/sync`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
<BoltIcon className="h-6 w-6" />
@@ -317,11 +325,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View sync status</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/deploy`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<ArrowUpIcon className="h-6 w-6" />
@@ -331,11 +341,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">Deploy to production</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/publishing-queue`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
<ClockIcon className="h-6 w-6" />
@@ -345,7 +357,7 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View scheduled content</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
</button>
</Button>
</div>
</ComponentCard>

View File

@@ -13,6 +13,8 @@ import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import Alert from '../../components/ui/alert/Alert';
import Switch from '../../components/form/switch/Switch';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import ViewToggle from '../../components/common/ViewToggle';
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
import {
@@ -605,40 +607,38 @@ export default function SiteList() {
>
<div className="flex flex-nowrap gap-3 items-center justify-between w-full">
<div className="flex flex-nowrap gap-3 items-center flex-1 min-w-0 w-full">
<input
type="text"
placeholder="Search sites..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 min-w-[200px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
<select
value={siteTypeFilter}
onChange={(e) => setSiteTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{SITE_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={hostingTypeFilter}
onChange={(e) => setHostingTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{HOSTING_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="flex-1 min-w-[200px]">
<InputField
type="text"
placeholder="Search sites..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={SITE_TYPES}
placeholder="Show All Types"
defaultValue={siteTypeFilter}
onChange={(val) => setSiteTypeFilter(val)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={HOSTING_TYPES}
placeholder="Show All Hosting"
defaultValue={hostingTypeFilter}
onChange={(val) => setHostingTypeFilter(val)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={STATUS_OPTIONS}
placeholder="Show All Status"
defaultValue={statusFilter}
onChange={(val) => setStatusFilter(val)}
/>
</div>
</div>
{hasActiveFilters && (
<Button

View File

@@ -76,7 +76,8 @@ const DraggablePageItem: React.FC<{
e.stopPropagation();
onSelect(page.id);
}}
className="cursor-pointer"
className="cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label={isSelected ? 'Deselect page' : 'Select page'}
>
{isSelected ? (
<CheckLineIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
@@ -365,8 +366,9 @@ export default function PageManager() {
<Card className="p-6">
<div className="mb-4 flex items-center justify-between">
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
@@ -376,7 +378,7 @@ export default function PageManager() {
<div className="w-5 h-5 border-2 border-gray-400 rounded" />
)}
<span>Select All</span>
</button>
</Button>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag and drop to reorder pages
</p>

View File

@@ -11,6 +11,7 @@ import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import TextArea from '../../components/form/input/TextArea';
import InputField from '../../components/form/input/InputField';
import SelectDropdown from '../../components/form/SelectDropdown';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
@@ -266,51 +267,39 @@ export default function PostEditor() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant={activeTab === 'content' ? 'primary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('content')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'content'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<FileTextIcon className="w-4 h-4 inline mr-2" />
<FileTextIcon className="w-4 h-4" />
Content
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'taxonomy' ? 'primary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('taxonomy')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'taxonomy'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<TagIcon className="w-4 h-4 inline mr-2" />
<TagIcon className="w-4 h-4" />
Taxonomy & Cluster
</button>
</Button>
{content.id && (
<button
type="button"
<Button
variant={activeTab === 'validation' ? 'primary' : 'ghost'}
size="sm"
onClick={() => {
setActiveTab('validation');
loadValidation();
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'validation'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<CheckCircleIcon className="w-4 h-4 inline mr-2" />
<CheckCircleIcon className="w-4 h-4" />
Validation
{validationResult && !validationResult.is_valid && (
<span className="ml-2 px-2 py-0.5 text-xs bg-error-100 dark:bg-error-900 text-error-600 dark:text-error-400 rounded-full">
{validationResult.validation_errors.length}
</span>
)}
</button>
</Button>
)}
</div>
</div>
@@ -322,12 +311,12 @@ export default function PostEditor() {
<div className="space-y-4">
<div>
<Label>Title *</Label>
<input
<InputField
type="text"
value={content.title}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="Enter post title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>

View File

@@ -10,6 +10,8 @@ import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import IconButton from '../../components/ui/button/IconButton';
import { ButtonGroup, ButtonGroupItem } from '../../components/ui/button-group/ButtonGroup';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchContent, Content } from '../../services/api';
import {
@@ -277,30 +279,22 @@ export default function PublishingQueue() {
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{queueItems.length} items in queue
</h2>
<div className="flex items-center gap-2 bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
<button
<ButtonGroup>
<ButtonGroupItem
isActive={viewMode === 'list'}
onClick={() => setViewMode('list')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'list'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<ListIcon className="w-4 h-4" />
<ListIcon className="w-4 h-4 mr-1.5" />
List
</button>
<button
</ButtonGroupItem>
<ButtonGroupItem
isActive={viewMode === 'calendar'}
onClick={() => setViewMode('calendar')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'calendar'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<CalendarIcon className="w-4 h-4" />
<CalendarIcon className="w-4 h-4 mr-1.5" />
Calendar
</button>
</div>
</ButtonGroupItem>
</ButtonGroup>
</div>
{/* Queue Content */}
@@ -361,27 +355,30 @@ export default function PublishingQueue() {
{/* Actions */}
<div className="flex items-center gap-1">
<button
<IconButton
icon={<EyeIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleViewContent(item)}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title="View content"
>
<EyeIcon className="w-4 h-4" />
</button>
<button
/>
<IconButton
icon={item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handlePauseItem(item)}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title={item.isPaused ? 'Resume' : 'Pause'}
>
{item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
</button>
<button
/>
<IconButton
icon={<TrashBinIcon className="w-4 h-4" />}
variant="ghost"
tone="danger"
size="sm"
onClick={() => handleRemoveFromQueue(item)}
className="p-2 text-error-500 hover:text-error-700 dark:text-error-400 dark:hover:text-error-300 rounded-lg hover:bg-error-50 dark:hover:bg-error-900/20"
title="Remove from queue"
>
<TrashBinIcon className="w-4 h-4" />
</button>
/>
</div>
</div>
))}

View File

@@ -9,10 +9,14 @@ import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import IconButton from '../../components/ui/button/IconButton';
import Label from '../../components/form/Label';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import SelectDropdown from '../../components/form/SelectDropdown';
import Checkbox from '../../components/form/input/Checkbox';
import TextArea from '../../components/form/input/TextArea';
import Switch from '../../components/form/switch/Switch';
import { useToast } from '../../components/ui/toast/ToastContainer';
import {
fetchAPI,
@@ -23,7 +27,7 @@ import {
} from '../../services/api';
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '../../icons';
import Badge from '../../components/ui/badge/Badge';
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
@@ -554,10 +558,11 @@ export default function SiteSettings() {
{/* Site Selector - Only show if more than 1 site */}
{!sitesLoading && sites.length > 1 && (
<div className="relative inline-block">
<button
<Button
ref={siteSelectorRef}
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-brand-200 rounded-lg hover:bg-brand-50 hover:border-brand-300 dark:bg-gray-800 dark:text-gray-300 dark:border-brand-700/50 dark:hover:bg-brand-500/10 dark:hover:border-brand-600/50 transition-colors"
variant="outline"
className="flex items-center gap-2"
aria-label="Switch site"
>
<span className="flex items-center gap-2">
@@ -565,7 +570,7 @@ export default function SiteSettings() {
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
</span>
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
</button>
</Button>
<Dropdown
isOpen={isSiteSelectorOpen}
onClose={() => setIsSiteSelectorOpen(false)}
@@ -605,13 +610,13 @@ export default function SiteSettings() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant="ghost"
onClick={() => {
setActiveTab('general');
navigate(`/sites/${siteId}/settings`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'general'
? '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'
@@ -619,14 +624,14 @@ export default function SiteSettings() {
>
<GridIcon className="w-4 h-4 inline mr-2" />
General
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('integrations');
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'integrations'
? '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'
@@ -634,14 +639,14 @@ export default function SiteSettings() {
>
<PlugInIcon className="w-4 h-4 inline mr-2" />
Integrations
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('publishing');
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'publishing'
? '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'
@@ -649,15 +654,15 @@ export default function SiteSettings() {
>
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
Publishing
</button>
</Button>
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
<button
type="button"
<Button
variant="ghost"
onClick={() => {
setActiveTab('content-types');
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'content-types'
? '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'
@@ -665,7 +670,7 @@ export default function SiteSettings() {
>
<FileIcon className="w-4 h-4 inline mr-2" />
Content Types
</button>
</Button>
)}
</div>
</div>
@@ -696,17 +701,15 @@ export default function SiteSettings() {
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
<Switch
label=""
checked={publishingSettings.auto_approval_enabled}
onChange={(e) => {
const newSettings = { ...publishingSettings, auto_approval_enabled: e.target.checked };
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_approval_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_approval_enabled: e.target.checked });
savePublishingSettings({ auto_approval_enabled: checked });
}}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
</label>
</div>
</div>
@@ -721,17 +724,15 @@ export default function SiteSettings() {
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
<Switch
label=""
checked={publishingSettings.auto_publish_enabled}
onChange={(e) => {
const newSettings = { ...publishingSettings, auto_publish_enabled: e.target.checked };
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_publish_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_publish_enabled: e.target.checked });
savePublishingSettings({ auto_publish_enabled: checked });
}}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
</label>
</div>
</div>
@@ -745,7 +746,7 @@ export default function SiteSettings() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label>Daily Limit</Label>
<input
<InputField
type="number"
min="1"
max="50"
@@ -754,14 +755,12 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ daily_publish_limit: publishingSettings.daily_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
</div>
<div>
<Label>Weekly Limit</Label>
<input
<InputField
type="number"
min="1"
max="200"
@@ -770,14 +769,12 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ weekly_publish_limit: publishingSettings.weekly_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
</div>
<div>
<Label>Monthly Limit</Label>
<input
<InputField
type="number"
min="1"
max="500"
@@ -786,8 +783,6 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ monthly_publish_limit: publishingSettings.monthly_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
</div>
@@ -810,9 +805,11 @@ export default function SiteSettings() {
{ value: 'sat', label: 'Sat' },
{ value: 'sun', label: 'Sun' },
].map((day) => (
<button
<Button
key={day.value}
type="button"
variant={(publishingSettings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
tone="brand"
size="sm"
onClick={() => {
const currentDays = publishingSettings.publish_days || [];
const newDays = currentDays.includes(day.value)
@@ -821,14 +818,9 @@ export default function SiteSettings() {
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
savePublishingSettings({ publish_days: newDays });
}}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
(publishingSettings.publish_days || []).includes(day.value)
? 'bg-brand-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{day.label}
</button>
</Button>
))}
</div>
</div>
@@ -842,7 +834,7 @@ export default function SiteSettings() {
<div className="space-y-3">
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
<div key={index} className="flex items-center gap-2">
<input
<InputField
type="time"
value={time}
onChange={(e) => {
@@ -850,40 +842,36 @@ export default function SiteSettings() {
newSlots[index] = e.target.value;
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
}}
onBlur={() => savePublishingSettings({ publish_time_slots: publishingSettings.publish_time_slots })}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
{(publishingSettings.publish_time_slots || []).length > 1 && (
<button
type="button"
<IconButton
icon={<CloseIcon className="w-5 h-5" />}
variant="ghost"
tone="danger"
size="sm"
title="Remove time slot"
onClick={() => {
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
savePublishingSettings({ publish_time_slots: newSlots });
}}
className="p-2 text-gray-400 hover:text-error-500 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
/>
)}
</div>
))}
<button
type="button"
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<PlusIcon className="w-4 h-4" />}
onClick={() => {
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
savePublishingSettings({ publish_time_slots: newSlots });
}}
className="text-sm text-brand-600 hover:text-brand-700 font-medium flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Time Slot
</button>
</Button>
</div>
</div>
@@ -1084,32 +1072,29 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Site Name</Label>
<input
<InputField
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Slug</Label>
<input
<InputField
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Site URL</Label>
<input
<InputField
type="text"
value={formData.site_url}
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1134,7 +1119,7 @@ export default function SiteSettings() {
<div>
<Checkbox
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
onChange={(checked) => setFormData({ ...formData, is_active: checked })}
label="Active"
/>
</div>
@@ -1150,13 +1135,12 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Meta Title</Label>
<input
<InputField
type="text"
value={formData.meta_title}
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
placeholder="SEO title (recommended: 50-60 characters)"
maxLength={60}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
max="60"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formData.meta_title.length}/60 characters
@@ -1180,12 +1164,11 @@ export default function SiteSettings() {
<div>
<Label>Meta Keywords (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.meta_keywords}
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
placeholder="keyword1, keyword2, keyword3"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Separate keywords with commas
@@ -1203,12 +1186,11 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>OG Title</Label>
<input
<InputField
type="text"
value={formData.og_title}
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
placeholder="Open Graph title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1225,12 +1207,11 @@ export default function SiteSettings() {
<div>
<Label>OG Image URL</Label>
<input
<InputField
type="url"
value={formData.og_image}
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
placeholder="https://example.com/image.jpg"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Recommended: 1200x630px image
@@ -1253,12 +1234,11 @@ export default function SiteSettings() {
<div>
<Label>OG Site Name</Label>
<input
<InputField
type="text"
value={formData.og_site_name}
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
placeholder="Site name for social sharing"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
@@ -1288,12 +1268,11 @@ export default function SiteSettings() {
<div>
<Label>Schema Name</Label>
<input
<InputField
type="text"
value={formData.schema_name}
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
placeholder="Organization name"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1310,34 +1289,31 @@ export default function SiteSettings() {
<div>
<Label>Schema URL</Label>
<input
<InputField
type="url"
value={formData.schema_url}
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Schema Logo URL</Label>
<input
<InputField
type="url"
value={formData.schema_logo}
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
placeholder="https://example.com/logo.png"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Same As URLs (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.schema_same_as}
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
placeholder="https://facebook.com/page, https://twitter.com/page"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Social media profiles and other related URLs
@@ -1359,21 +1335,18 @@ export default function SiteSettings() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Industry
</label>
<select
value={selectedIndustry}
onChange={(e) => {
setSelectedIndustry(e.target.value);
<Select
options={industries.map((industry) => ({
value: industry.slug,
label: industry.name,
}))}
placeholder="Select an industry..."
defaultValue={selectedIndustry}
onChange={(value) => {
setSelectedIndustry(value);
setSelectedSectors([]);
}}
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
>
<option value="">Select an industry...</option>
{industries.map((industry) => (
<option key={industry.slug} value={industry.slug}>
{industry.name}
</option>
))}
</select>
/>
{selectedIndustry && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{industries.find(i => i.slug === selectedIndustry)?.description}
@@ -1388,15 +1361,14 @@ export default function SiteSettings() {
</label>
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
{getIndustrySectors().map((sector) => (
<label
<div
key={sector.slug}
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
>
<input
type="checkbox"
<Checkbox
checked={selectedSectors.includes(sector.slug)}
onChange={(e) => {
if (e.target.checked) {
onChange={(checked) => {
if (checked) {
if (selectedSectors.length >= 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
@@ -1406,7 +1378,6 @@ export default function SiteSettings() {
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
}
}}
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
@@ -1416,7 +1387,7 @@ export default function SiteSettings() {
{sector.description}
</div>
</div>
</label>
</div>
))}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
@@ -1447,13 +1418,12 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Meta Title</Label>
<input
<InputField
type="text"
value={formData.meta_title}
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
placeholder="SEO title (recommended: 50-60 characters)"
maxLength={60}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
max="60"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formData.meta_title.length}/60 characters
@@ -1477,12 +1447,11 @@ export default function SiteSettings() {
<div>
<Label>Meta Keywords (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.meta_keywords}
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
placeholder="keyword1, keyword2, keyword3"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Separate keywords with commas
@@ -1498,12 +1467,11 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>OG Title</Label>
<input
<InputField
type="text"
value={formData.og_title}
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
placeholder="Open Graph title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1520,12 +1488,11 @@ export default function SiteSettings() {
<div>
<Label>OG Image URL</Label>
<input
<InputField
type="url"
value={formData.og_image}
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
placeholder="https://example.com/image.jpg"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Recommended: 1200x630px image
@@ -1542,18 +1509,17 @@ export default function SiteSettings() {
{ value: 'product', label: 'Product' },
]}
value={formData.og_type}
onChange={(e) => setFormData({ ...formData, og_type: e.target.value })}
onChange={(value) => setFormData({ ...formData, og_type: value })}
/>
</div>
<div>
<Label>OG Site Name</Label>
<input
<InputField
type="text"
value={formData.og_site_name}
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
placeholder="Site name for social sharing"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
@@ -1575,18 +1541,17 @@ export default function SiteSettings() {
{ value: 'NGO', label: 'NGO' },
]}
value={formData.schema_type}
onChange={(e) => setFormData({ ...formData, schema_type: e.target.value })}
onChange={(value) => setFormData({ ...formData, schema_type: value })}
/>
</div>
<div>
<Label>Schema Name</Label>
<input
<InputField
type="text"
value={formData.schema_name}
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
placeholder="Organization name"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1603,34 +1568,31 @@ export default function SiteSettings() {
<div>
<Label>Schema URL</Label>
<input
<InputField
type="url"
value={formData.schema_url}
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Schema Logo URL</Label>
<input
<InputField
type="url"
value={formData.schema_logo}
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
placeholder="https://example.com/logo.png"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Same As URLs (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.schema_same_as}
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
placeholder="https://facebook.com/page, https://twitter.com/page"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Social media profiles and other related URLs

View File

@@ -287,9 +287,10 @@ export default function SyncDashboard() {
{/* Mismatches Section */}
{totalMismatches > 0 && (
<Card className="p-6 mb-6">
<button
<Button
onClick={() => setShowMismatches(!showMismatches)}
className="w-full flex items-center justify-between mb-4"
variant="ghost"
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Mismatches ({totalMismatches})
@@ -299,7 +300,7 @@ export default function SyncDashboard() {
) : (
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
)}
</button>
</Button>
{showMismatches && mismatches && (
<div className="space-y-4">
@@ -412,9 +413,10 @@ export default function SyncDashboard() {
{/* Sync Logs */}
<Card className="p-6">
<button
<Button
onClick={() => setShowLogs(!showLogs)}
className="w-full flex items-center justify-between mb-4"
variant="ghost"
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Sync History ({logs.length})
@@ -424,7 +426,7 @@ export default function SyncDashboard() {
) : (
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
)}
</button>
</Button>
{showLogs && (
<div className="space-y-2">

View File

@@ -0,0 +1,737 @@
/**
* UI Elements Showcase Page
*
* Single source of truth for all UI components from components/ui/
* This page is non-indexable and serves as documentation/reference
*
* Route: /ui-elements
*/
import React, { useState } from 'react';
import PageMeta from '../components/common/PageMeta';
// ============================================================================
// UI COMPONENTS - All imports from components/ui/ (SINGLE SOURCE OF TRUTH)
// ============================================================================
// Accordion
import { Accordion, AccordionItem } from '../components/ui/accordion/Accordion';
// Alert
import Alert from '../components/ui/alert/Alert';
import AlertModal from '../components/ui/alert/AlertModal';
// Avatar
import Avatar from '../components/ui/avatar/Avatar';
// Badge
import Badge from '../components/ui/badge/Badge';
// Breadcrumb
import { Breadcrumb } from '../components/ui/breadcrumb/Breadcrumb';
// Button
import Button from '../components/ui/button/Button';
import ButtonWithTooltip from '../components/ui/button/ButtonWithTooltip';
import IconButton from '../components/ui/button/IconButton';
// Button Group
import { ButtonGroup, ButtonGroupItem } from '../components/ui/button-group/ButtonGroup';
// Card
import { Card, CardImage, CardTitle, CardContent, CardDescription, CardAction, CardIcon, HorizontalCard } from '../components/ui/card/Card';
// DataView
import { DataView, DataViewHeader, DataViewToolbar, DataViewEmptyState } from '../components/ui/dataview/DataView';
// Dropdown
import { Dropdown } from '../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
// List
import { List, ListItem, ListDot, ListIcon, ListCheckboxItem, ListRadioItem } from '../components/ui/list/List';
// Modal
import { Modal } from '../components/ui/modal';
// Pagination
import { Pagination } from '../components/ui/pagination/Pagination';
import { CompactPagination } from '../components/ui/pagination/CompactPagination';
// Progress
import { ProgressBar } from '../components/ui/progress/ProgressBar';
// Ribbon
import { Ribbon } from '../components/ui/ribbon/Ribbon';
// Spinner
import { Spinner } from '../components/ui/spinner/Spinner';
// Table
import { Table, TableHeader, TableBody, TableRow, TableCell } from '../components/ui/table';
// Tabs
import { Tabs, TabList, Tab, TabPanel } from '../components/ui/tabs/Tabs';
// Toast
import { useToast } from '../components/ui/toast/ToastContainer';
// Tooltip
import { Tooltip } from '../components/ui/tooltip/Tooltip';
import { EnhancedTooltip } from '../components/ui/tooltip/EnhancedTooltip';
// Icons for demos
import {
CheckCircleIcon,
CloseIcon,
PlusIcon,
ArrowRightIcon,
GridIcon,
FileIcon,
BoltIcon,
} from '../icons';
// Component categories for navigation
const CATEGORIES = [
{ id: 'buttons', label: 'Buttons' },
{ id: 'badges', label: 'Badges' },
{ id: 'cards', label: 'Cards' },
{ id: 'alerts', label: 'Alerts' },
{ id: 'modals', label: 'Modals' },
{ id: 'tables', label: 'Tables' },
{ id: 'tabs', label: 'Tabs' },
{ id: 'accordion', label: 'Accordion' },
{ id: 'dropdown', label: 'Dropdown' },
{ id: 'pagination', label: 'Pagination' },
{ id: 'progress', label: 'Progress' },
{ id: 'spinner', label: 'Spinner' },
{ id: 'avatar', label: 'Avatar' },
{ id: 'breadcrumb', label: 'Breadcrumb' },
{ id: 'list', label: 'List' },
{ id: 'tooltip', label: 'Tooltip' },
{ id: 'ribbon', label: 'Ribbon' },
{ id: 'toast', label: 'Toast' },
{ id: 'dataview', label: 'DataView' },
];
// Section wrapper component
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
return (
<section id={id} className="mb-12 scroll-mt-24">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
{title}
</h2>
<div className="space-y-6">
{children}
</div>
</section>
);
}
// Demo box component
function DemoBox({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<p className="text-xs font-mono text-gray-500 dark:text-gray-400 mb-3">{label}</p>
<div className="flex flex-wrap items-center gap-3">
{children}
</div>
</div>
);
}
export default function UIElements() {
const toast = useToast();
// Modal states
const [showModal, setShowModal] = useState(false);
const [showAlertModal, setShowAlertModal] = useState(false);
// Dropdown state
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = React.useRef<HTMLButtonElement>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// Tabs state
const [activeTab, setActiveTab] = useState('tab1');
// List state
const [checkboxChecked, setCheckboxChecked] = useState(false);
const [radioValue, setRadioValue] = useState('option1');
// Button group state
const [activeButton, setActiveButton] = useState(0);
return (
<>
<PageMeta
title="UI Elements | IGNY8 Design System"
description="Component library reference - Single source of truth for all UI components"
noIndex={true}
/>
<div className="flex gap-8">
{/* Sticky Navigation */}
<nav className="hidden lg:block w-48 shrink-0">
<div className="sticky top-24 space-y-1">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">Components</h3>
{CATEGORIES.map(cat => (
<a
key={cat.id}
href={`#${cat.id}`}
className="block py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
>
{cat.label}
</a>
))}
</div>
</nav>
{/* Main Content */}
<div className="flex-1 min-w-0">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
UI Elements Library
</h1>
<p className="text-gray-600 dark:text-gray-400">
Single source of truth for all UI components. All components are imported from <code className="text-sm bg-gray-100 dark:bg-gray-800 px-1 rounded">components/ui/</code>
</p>
</div>
{/* ================================================================ */}
{/* BUTTONS */}
{/* ================================================================ */}
<Section id="buttons" title="Button">
<DemoBox label="variants: primary | secondary | outline | ghost | gradient">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="gradient">Gradient</Button>
</DemoBox>
<DemoBox label="tones: brand | success | warning | danger | neutral">
<Button tone="brand">Brand</Button>
<Button tone="success">Success</Button>
<Button tone="warning">Warning</Button>
<Button tone="danger">Danger</Button>
<Button tone="neutral">Neutral</Button>
</DemoBox>
<DemoBox label="sizes: xs | sm | md | lg | xl | 2xl">
<Button size="xs">XS</Button>
<Button size="sm">SM</Button>
<Button size="md">MD</Button>
<Button size="lg">LG</Button>
<Button size="xl">XL</Button>
</DemoBox>
<DemoBox label="shapes: rounded | pill">
<Button shape="rounded">Rounded</Button>
<Button shape="pill">Pill</Button>
</DemoBox>
<DemoBox label="with icons: startIcon | endIcon">
<Button startIcon={<PlusIcon className="w-4 h-4" />}>Add Item</Button>
<Button endIcon={<ArrowRightIcon className="w-4 h-4" />}>Continue</Button>
</DemoBox>
<DemoBox label="states: disabled | fullWidth">
<Button disabled>Disabled</Button>
</DemoBox>
<DemoBox label="ButtonWithTooltip (shows tooltip when disabled)">
<ButtonWithTooltip disabled tooltip="This action is not available">
Disabled with Tooltip
</ButtonWithTooltip>
</DemoBox>
<DemoBox label="ButtonGroup">
<ButtonGroup>
<ButtonGroupItem isActive={activeButton === 0} onClick={() => setActiveButton(0)}>Day</ButtonGroupItem>
<ButtonGroupItem isActive={activeButton === 1} onClick={() => setActiveButton(1)}>Week</ButtonGroupItem>
<ButtonGroupItem isActive={activeButton === 2} onClick={() => setActiveButton(2)}>Month</ButtonGroupItem>
</ButtonGroup>
</DemoBox>
<DemoBox label="IconButton - variants: solid | outline | ghost">
<IconButton icon={<PlusIcon />} variant="solid" tone="brand" title="Add" />
<IconButton icon={<PlusIcon />} variant="outline" tone="brand" title="Add" />
<IconButton icon={<PlusIcon />} variant="ghost" tone="brand" title="Add" />
</DemoBox>
<DemoBox label="IconButton - sizes: xs | sm | md | lg">
<IconButton icon={<CloseIcon />} size="xs" title="Close" />
<IconButton icon={<CloseIcon />} size="sm" title="Close" />
<IconButton icon={<CloseIcon />} size="md" title="Close" />
<IconButton icon={<CloseIcon />} size="lg" title="Close" />
</DemoBox>
<DemoBox label="IconButton - shapes: rounded | circle">
<IconButton icon={<CheckCircleIcon />} shape="rounded" variant="solid" tone="success" title="Approve" />
<IconButton icon={<CheckCircleIcon />} shape="circle" variant="solid" tone="success" title="Approve" />
</DemoBox>
<DemoBox label="IconButton - tones">
<IconButton icon={<BoltIcon />} variant="ghost" tone="brand" title="Brand" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="success" title="Success" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="warning" title="Warning" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="danger" title="Danger" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="neutral" title="Neutral" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* BADGES */}
{/* ================================================================ */}
<Section id="badges" title="Badge">
<DemoBox label="variants: solid | soft | outline | light">
<Badge variant="solid" tone="brand">Solid</Badge>
<Badge variant="soft" tone="brand">Soft</Badge>
<Badge variant="outline" tone="brand">Outline</Badge>
<Badge variant="light" tone="brand">Light</Badge>
</DemoBox>
<DemoBox label="tones: brand | success | warning | danger | info | neutral">
<Badge tone="brand">Brand</Badge>
<Badge tone="success">Success</Badge>
<Badge tone="warning">Warning</Badge>
<Badge tone="danger">Danger</Badge>
<Badge tone="info">Info</Badge>
<Badge tone="neutral">Neutral</Badge>
</DemoBox>
<DemoBox label="sizes: xs | sm | md">
<Badge size="xs">XS</Badge>
<Badge size="sm">SM</Badge>
<Badge size="md">MD</Badge>
</DemoBox>
<DemoBox label="with icons: startIcon | endIcon">
<Badge startIcon={<CheckCircleIcon className="w-3 h-3" />} tone="success">Completed</Badge>
<Badge endIcon={<CloseIcon className="w-3 h-3" />} tone="danger">Remove</Badge>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* CARDS */}
{/* ================================================================ */}
<Section id="cards" title="Card">
<DemoBox label="variants: surface | panel | frosted | borderless | gradient">
<Card variant="surface" className="w-48">
<CardContent>Surface Card</CardContent>
</Card>
<Card variant="panel" className="w-48">
<CardContent>Panel Card</CardContent>
</Card>
</DemoBox>
<DemoBox label="padding: none | sm | md | lg">
<Card padding="sm" className="w-48">
<CardContent>Small Padding</CardContent>
</Card>
<Card padding="lg" className="w-48">
<CardContent>Large Padding</CardContent>
</Card>
</DemoBox>
<DemoBox label="Card with components">
<Card className="w-64">
<CardIcon><FileIcon className="w-8 h-8 text-brand-500" /></CardIcon>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description text goes here</CardDescription>
<CardAction>View More</CardAction>
</Card>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* ALERTS */}
{/* ================================================================ */}
<Section id="alerts" title="Alert">
<DemoBox label="variants: success | error | warning | info">
<div className="space-y-3 w-full">
<Alert variant="success" title="Success" message="Operation completed successfully!" />
<Alert variant="error" title="Error" message="Something went wrong. Please try again." />
<Alert variant="warning" title="Warning" message="Please review before continuing." />
<Alert variant="info" title="Info" message="Here's some useful information." />
</div>
</DemoBox>
<DemoBox label="AlertModal">
<Button onClick={() => setShowAlertModal(true)}>Open Alert Modal</Button>
<AlertModal
isOpen={showAlertModal}
onClose={() => setShowAlertModal(false)}
variant="warning"
title="Confirm Action"
message="Are you sure you want to proceed?"
isConfirmation={true}
onConfirm={() => setShowAlertModal(false)}
/>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* MODALS */}
{/* ================================================================ */}
<Section id="modals" title="Modal">
<DemoBox label="Basic Modal">
<Button onClick={() => setShowModal(true)}>Open Modal</Button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<div className="p-6">
<h3 className="text-lg font-semibold mb-4">Modal Title</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
This is modal content. You can put any React components here.
</p>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setShowModal(false)}>Cancel</Button>
<Button onClick={() => setShowModal(false)}>Confirm</Button>
</div>
</div>
</Modal>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TABLES */}
{/* ================================================================ */}
<Section id="tables" title="Table">
<DemoBox label="Table components">
<Table className="w-full">
<TableHeader>
<TableRow>
<TableCell isHeader>Name</TableCell>
<TableCell isHeader>Status</TableCell>
<TableCell isHeader>Date</TableCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Item One</TableCell>
<TableCell><Badge tone="success">Active</Badge></TableCell>
<TableCell>Jan 1, 2024</TableCell>
</TableRow>
<TableRow>
<TableCell>Item Two</TableCell>
<TableCell><Badge tone="warning">Pending</Badge></TableCell>
<TableCell>Jan 2, 2024</TableCell>
</TableRow>
</TableBody>
</Table>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TABS */}
{/* ================================================================ */}
<Section id="tabs" title="Tabs">
<DemoBox label="Tabs component">
<div className="w-full">
<Tabs defaultTab="tab1" onChange={setActiveTab}>
<TabList>
<Tab tabId="tab1">Tab One</Tab>
<Tab tabId="tab2">Tab Two</Tab>
<Tab tabId="tab3">Tab Three</Tab>
</TabList>
<TabPanel tabId="tab1">
<div className="py-4">Content for Tab One</div>
</TabPanel>
<TabPanel tabId="tab2">
<div className="py-4">Content for Tab Two</div>
</TabPanel>
<TabPanel tabId="tab3">
<div className="py-4">Content for Tab Three</div>
</TabPanel>
</Tabs>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* ACCORDION */}
{/* ================================================================ */}
<Section id="accordion" title="Accordion">
<DemoBox label="Accordion component">
<div className="w-full">
<Accordion>
<AccordionItem title="Section One" defaultOpen>
Content for section one goes here.
</AccordionItem>
<AccordionItem title="Section Two">
Content for section two goes here.
</AccordionItem>
<AccordionItem title="Section Three">
Content for section three goes here.
</AccordionItem>
</Accordion>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* DROPDOWN */}
{/* ================================================================ */}
<Section id="dropdown" title="Dropdown">
<DemoBox label="Dropdown component">
<div className="relative">
<Button ref={dropdownRef} onClick={() => setDropdownOpen(!dropdownOpen)}>
Open Dropdown
</Button>
<Dropdown
isOpen={dropdownOpen}
onClose={() => setDropdownOpen(false)}
anchorRef={dropdownRef}
>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option One</DropdownItem>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Two</DropdownItem>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Three</DropdownItem>
</Dropdown>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* PAGINATION */}
{/* ================================================================ */}
<Section id="pagination" title="Pagination">
<DemoBox label="Pagination variants">
<Pagination
currentPage={currentPage}
totalPages={10}
onPageChange={setCurrentPage}
variant="text"
/>
</DemoBox>
<DemoBox label="CompactPagination (with page size)">
<CompactPagination
currentPage={currentPage}
totalPages={10}
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
/>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* PROGRESS */}
{/* ================================================================ */}
<Section id="progress" title="Progress">
<DemoBox label="ProgressBar colors">
<div className="space-y-3 w-full">
<ProgressBar value={75} color="primary" showLabel />
<ProgressBar value={60} color="success" showLabel />
<ProgressBar value={45} color="warning" showLabel />
<ProgressBar value={30} color="error" showLabel />
</div>
</DemoBox>
<DemoBox label="ProgressBar sizes: sm | md | lg">
<div className="space-y-3 w-full">
<ProgressBar value={50} size="sm" />
<ProgressBar value={50} size="md" />
<ProgressBar value={50} size="lg" />
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* SPINNER */}
{/* ================================================================ */}
<Section id="spinner" title="Spinner">
<DemoBox label="sizes: sm | md | lg">
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
</DemoBox>
<DemoBox label="colors: primary | success | error | warning | info">
<Spinner color="primary" />
<Spinner color="success" />
<Spinner color="error" />
<Spinner color="warning" />
<Spinner color="info" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* AVATAR */}
{/* ================================================================ */}
<Section id="avatar" title="Avatar">
<DemoBox label="sizes: xsmall | small | medium | large | xlarge | xxlarge">
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
<Avatar src="/images/user/user-01.jpg" size="small" />
<Avatar src="/images/user/user-01.jpg" size="medium" />
<Avatar src="/images/user/user-01.jpg" size="large" />
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
</DemoBox>
<DemoBox label="status: online | offline | busy | none">
<Avatar src="/images/user/user-01.jpg" status="online" />
<Avatar src="/images/user/user-01.jpg" status="offline" />
<Avatar src="/images/user/user-01.jpg" status="busy" />
<Avatar src="/images/user/user-01.jpg" status="none" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* BREADCRUMB */}
{/* ================================================================ */}
<Section id="breadcrumb" title="Breadcrumb">
<DemoBox label="Breadcrumb component">
<Breadcrumb items={[
{ label: 'Home', path: '/', icon: <GridIcon className="w-4 h-4" /> },
{ label: 'Category', path: '/category' },
{ label: 'Current Page' },
]} />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* LIST */}
{/* ================================================================ */}
<Section id="list" title="List">
<DemoBox label="List variants">
<List variant="unordered">
<ListItem><ListDot />Item One</ListItem>
<ListItem><ListDot />Item Two</ListItem>
<ListItem><ListDot />Item Three</ListItem>
</List>
</DemoBox>
<DemoBox label="ListCheckboxItem">
<List variant="checkbox">
<ListCheckboxItem
id="check1"
label="Checkbox Option"
checked={checkboxChecked}
onChange={setCheckboxChecked}
/>
</List>
</DemoBox>
<DemoBox label="ListRadioItem">
<List variant="radio">
<ListRadioItem
id="radio1"
name="radioGroup"
value="option1"
label="Option One"
checked={radioValue === 'option1'}
onChange={setRadioValue}
/>
<ListRadioItem
id="radio2"
name="radioGroup"
value="option2"
label="Option Two"
checked={radioValue === 'option2'}
onChange={setRadioValue}
/>
</List>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TOOLTIP */}
{/* ================================================================ */}
<Section id="tooltip" title="Tooltip">
<DemoBox label="placements: top | bottom | left | right">
<Tooltip text="Top tooltip" placement="top">
<Button variant="outline" size="sm">Top</Button>
</Tooltip>
<Tooltip text="Bottom tooltip" placement="bottom">
<Button variant="outline" size="sm">Bottom</Button>
</Tooltip>
<Tooltip text="Left tooltip" placement="left">
<Button variant="outline" size="sm">Left</Button>
</Tooltip>
<Tooltip text="Right tooltip" placement="right">
<Button variant="outline" size="sm">Right</Button>
</Tooltip>
</DemoBox>
<DemoBox label="EnhancedTooltip (supports ReactNode content)">
<EnhancedTooltip
content={
<div>
<strong>Rich Content</strong>
<p className="text-xs">With multiple lines</p>
</div>
}
>
<Button variant="outline" size="sm">Hover Me</Button>
</EnhancedTooltip>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* RIBBON */}
{/* ================================================================ */}
<Section id="ribbon" title="Ribbon">
<DemoBox label="Ribbon variants">
<Ribbon text="New" variant="rounded" color="primary">
<Card className="w-48 h-24">
<CardContent>Card with ribbon</CardContent>
</Card>
</Ribbon>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TOAST */}
{/* ================================================================ */}
<Section id="toast" title="Toast">
<DemoBox label="Toast notifications (via useToast hook)">
<Button onClick={() => toast.success('Success', 'Operation completed!')}>
Show Success
</Button>
<Button onClick={() => toast.error('Error', 'Something went wrong!')} tone="danger">
Show Error
</Button>
<Button onClick={() => toast.warning('Warning', 'Please review!')} tone="warning">
Show Warning
</Button>
<Button onClick={() => toast.info('Info', 'Here is some information')} variant="outline">
Show Info
</Button>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* DATAVIEW */}
{/* ================================================================ */}
<Section id="dataview" title="DataView">
<DemoBox label="DataView container">
<DataView className="w-full">
<DataViewHeader
title="Data View Title"
description="Description of the data view"
actions={<Button size="sm">Action</Button>}
/>
<DataViewToolbar>
<Button size="sm" variant="outline">Filter</Button>
<Button size="sm" variant="outline">Sort</Button>
</DataViewToolbar>
<div className="p-4 text-gray-500">Data content goes here...</div>
</DataView>
</DemoBox>
<DemoBox label="DataViewEmptyState">
<DataViewEmptyState
title="No Data Found"
description="Try adjusting your filters or add new items"
action={<Button size="sm">Add Item</Button>}
/>
</DemoBox>
</Section>
</div>
</div>
</>
);
}

View File

@@ -12,6 +12,9 @@ import {
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useToast } from '../../components/ui/toast/ToastContainer';
@@ -323,40 +326,30 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
<InputField
type="text"
name="name"
label="Account Name"
value={accountForm.name}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
<InputField
type="text"
label="Account Slug"
value={settings?.slug || ''}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
disabled
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
<InputField
type="email"
name="billing_email"
label="Billing Email"
value={accountForm.billing_email}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</Card>
@@ -366,77 +359,59 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
<InputField
type="text"
name="billing_address_line1"
label="Address Line 1"
value={accountForm.billing_address_line1}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
<InputField
type="text"
name="billing_address_line2"
label="Address Line 2"
value={accountForm.billing_address_line2}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
<InputField
type="text"
name="billing_city"
label="City"
value={accountForm.billing_city}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
<InputField
type="text"
name="billing_state"
label="State/Province"
value={accountForm.billing_state}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
<InputField
type="text"
name="billing_postal_code"
label="Postal Code"
value={accountForm.billing_postal_code}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
<InputField
type="text"
name="billing_country"
label="Country"
value={accountForm.billing_country}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="US, GB, IN, etc."
/>
</div>
@@ -447,15 +422,12 @@ export default function AccountSettingsPage() {
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID / VAT Number
</label>
<input
<InputField
type="text"
name="tax_id"
label="Tax ID / VAT Number"
value={accountForm.tax_id}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="Optional"
/>
</div>
@@ -485,47 +457,35 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={profileForm.firstName}
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={profileForm.lastName}
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
label="Email"
value={profileForm.email}
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone Number (optional)
</label>
<input
<InputField
type="tel"
label="Phone Number (optional)"
value={profileForm.phone}
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
@@ -538,33 +498,33 @@ export default function AccountSettingsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
</label>
<select
value={profileForm.timezone}
onChange={(e) => setProfileForm({ ...profileForm, timezone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
<option value="Europe/London">London</option>
<option value="Asia/Kolkata">India</option>
</select>
<Select
options={[
{ value: 'America/New_York', label: 'Eastern Time' },
{ value: 'America/Chicago', label: 'Central Time' },
{ value: 'America/Denver', label: 'Mountain Time' },
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
{ value: 'UTC', label: 'UTC' },
{ value: 'Europe/London', label: 'London' },
{ value: 'Asia/Kolkata', label: 'India' },
]}
defaultValue={profileForm.timezone}
onChange={(value) => setProfileForm({ ...profileForm, timezone: value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Language
</label>
<select
value={profileForm.language}
onChange={(e) => setProfileForm({ ...profileForm, language: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
<Select
options={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
]}
defaultValue={profileForm.language}
onChange={(value) => setProfileForm({ ...profileForm, language: value })}
/>
</div>
</div>
</Card>
@@ -582,11 +542,9 @@ export default function AccountSettingsPage() {
Get notified about important changes to your account
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profileForm.emailNotifications}
onChange={(e) => setProfileForm({ ...profileForm, emailNotifications: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
onChange={(checked) => setProfileForm({ ...profileForm, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
@@ -596,11 +554,9 @@ export default function AccountSettingsPage() {
Hear about new features and content tips
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profileForm.marketingEmails}
onChange={(e) => setProfileForm({ ...profileForm, marketingEmails: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
onChange={(checked) => setProfileForm({ ...profileForm, marketingEmails: checked })}
/>
</div>
</div>
@@ -769,39 +725,30 @@ export default function AccountSettingsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email *
</label>
<input
<InputField
type="email"
label="Email *"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="user@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={inviteForm.first_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={inviteForm.last_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
@@ -854,38 +801,29 @@ export default function AccountSettingsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password
</label>
<input
<InputField
type="password"
label="Current Password"
value={passwordForm.currentPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Enter current password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password
</label>
<input
<InputField
type="password"
label="New Password"
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Enter new password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password
</label>
<input
<InputField
type="password"
label="Confirm New Password"
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Confirm new password"
/>
</div>

View File

@@ -16,6 +16,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import SelectDropdown from '../../components/form/SelectDropdown';
import Label from '../../components/form/Label';
import Checkbox from '../../components/form/input/Checkbox';
import TextArea from '../../components/form/input/TextArea';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { BoxCubeIcon } from '../../icons';
@@ -357,11 +358,11 @@ export default function ContentSettingsPage() {
<div className="space-y-6">
<div>
<Label className="mb-2">Append to Every Prompt</Label>
<textarea
<TextArea
value={contentSettings.appendToPrompt}
onChange={(e) => setContentSettings({ ...contentSettings, appendToPrompt: e.target.value })}
onChange={(value) => setContentSettings({ ...contentSettings, appendToPrompt: value })}
placeholder="Add custom instructions that will be included with every content generation request..."
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800 min-h-[120px] resize-y"
rows={5}
/>
<p className="text-xs text-gray-500 mt-1">
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.

View File

@@ -15,6 +15,7 @@ import {
} from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Select from '../../components/form/Select';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useNotificationStore } from '../../store/notificationStore';
@@ -237,19 +238,19 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Severity
</label>
<select
value={filters.severity}
onChange={(e) =>
setFilters({ ...filters, severity: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
{ value: 'info', label: 'Info' },
{ value: 'success', label: 'Success' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' },
]}
defaultValue={filters.severity}
onChange={(value) =>
setFilters({ ...filters, severity: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
/>
</div>
{/* Type Filter */}
@@ -257,20 +258,19 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Type
</label>
<select
value={filters.notification_type}
onChange={(e) =>
setFilters({ ...filters, notification_type: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
...notificationTypes.map((type) => ({
value: type,
label: getTypeLabel(type),
})),
]}
defaultValue={filters.notification_type}
onChange={(value) =>
setFilters({ ...filters, notification_type: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
{notificationTypes.map((type) => (
<option key={type} value={type}>
{getTypeLabel(type)}
</option>
))}
</select>
/>
</div>
{/* Read Status Filter */}
@@ -278,17 +278,17 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
value={filters.is_read}
onChange={(e) =>
setFilters({ ...filters, is_read: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
{ value: 'false', label: 'Unread' },
{ value: 'true', label: 'Read' },
]}
defaultValue={filters.is_read}
onChange={(value) =>
setFilters({ ...filters, is_read: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="false">Unread</option>
<option value="true">Read</option>
</select>
/>
</div>
</div>

View File

@@ -6,6 +6,9 @@
import { useState, useEffect } from 'react';
import { AlertCircleIcon, CheckIcon, CreditCardIcon, Building2Icon, WalletIcon, Loader2Icon, ZapIcon } from '../../icons';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import TextArea from '../../components/form/input/TextArea';
import Label from '../../components/form/Label';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import {
@@ -245,33 +248,26 @@ export default function PurchaseCreditsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Transaction Reference / ID *
</label>
<input
<InputField
label="Transaction Reference / ID *"
type="text"
required
value={manualPaymentData.transaction_reference}
onChange={(e) =>
setManualPaymentData({ ...manualPaymentData, transaction_reference: e.target.value })
}
placeholder="Enter transaction ID or reference number"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Additional Notes (Optional)
</label>
<textarea
<Label className="mb-2">Additional Notes (Optional)</Label>
<TextArea
value={manualPaymentData.notes}
onChange={(e) =>
setManualPaymentData({ ...manualPaymentData, notes: e.target.value })
onChange={(value) =>
setManualPaymentData({ ...manualPaymentData, notes: value })
}
placeholder="Any additional information..."
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
/>
</div>

View File

@@ -6,6 +6,10 @@
import { useState } from 'react';
import { SaveIcon, UserIcon, MailIcon, LockIcon, Loader2Icon } from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
export default function ProfileSettingsPage() {
const [saving, setSaving] = useState(false);
@@ -38,14 +42,15 @@ export default function ProfileSettingsPage() {
Update your personal settings - Your name, preferences, and notification choices
</p>
</div>
<button
<Button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50"
variant="primary"
tone="brand"
>
{saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
{saving ? 'Saving...' : '✓ Save My Settings'}
</button>
</Button>
</div>
<div className="space-y-6">
@@ -53,47 +58,35 @@ export default function ProfileSettingsPage() {
<h2 className="text-lg font-semibold mb-4">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={profile.firstName}
onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={profile.lastName}
onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
label="Email"
value={profile.email}
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone Number (optional)
</label>
<input
<InputField
type="tel"
label="Phone Number (optional)"
value={profile.phone}
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
</div>
@@ -103,34 +96,30 @@ export default function ProfileSettingsPage() {
<h2 className="text-lg font-semibold mb-4">How You Like It</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
</label>
<select
value={profile.timezone}
onChange={(e) => setProfile({ ...profile, timezone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
</select>
<Select
label="Your Timezone"
options={[
{ value: 'America/New_York', label: 'Eastern Time' },
{ value: 'America/Chicago', label: 'Central Time' },
{ value: 'America/Denver', label: 'Mountain Time' },
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
{ value: 'UTC', label: 'UTC' },
]}
defaultValue={profile.timezone}
onChange={(val) => setProfile({ ...profile, timezone: val })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Language
</label>
<select
value={profile.language}
onChange={(e) => setProfile({ ...profile, language: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
<Select
label="Language"
options={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
]}
defaultValue={profile.language}
onChange={(val) => setProfile({ ...profile, language: val })}
/>
</div>
</div>
</Card>
@@ -148,11 +137,9 @@ export default function ProfileSettingsPage() {
Get notified about important changes to your account
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profile.emailNotifications}
onChange={(e) => setProfile({ ...profile, emailNotifications: e.target.checked })}
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
onChange={(checked) => setProfile({ ...profile, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
@@ -162,11 +149,9 @@ export default function ProfileSettingsPage() {
Hear about new features and content tips
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profile.marketingEmails}
onChange={(e) => setProfile({ ...profile, marketingEmails: e.target.checked })}
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
onChange={(checked) => setProfile({ ...profile, marketingEmails: checked })}
/>
</div>
</div>
@@ -177,9 +162,9 @@ export default function ProfileSettingsPage() {
<LockIcon className="w-5 h-5" />
Security
</h2>
<button className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800">
<Button variant="outline" tone="neutral">
Change Password
</button>
</Button>
</Card>
</div>
</div>