modcomms/frontend/components/Settings.tsx
michael 532d7541d6 Implement Barclays design system UI update
- Update Tailwind config with new color tokens (primary-blue, active-blue,
  electric-violet, lime, grey-100/300/700/900, success, warning, error)
- Add Inter font from Google Fonts as Barclays Effra alternative
- Update Sidebar with primary-blue background and white active state
- Update Hero with electric-violet accent and pill-shaped buttons
- Update all tables with lime (#C3FB5A) header backgrounds
- Implement alternating row colors (white/grey-100) on tables
- Update status badges: In Progress (amber), Completed (green)
- Update tabs with active-blue underline styling
- Apply 10px border radius to cards and containers
- Update button styling to pill-shaped with active-blue
- Update input/dropdown borders to grey-700 with 2px
- Update selected state highlighting to info-light (#E7F0FB)
- Update FeedbackReport RAG status colors to new design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:50:46 -06:00

519 lines
28 KiB
TypeScript
Executable file

import React, { useState, useEffect } from 'react';
import type { DropdownOptions } from '../App';
import apiService from '../services/apiService';
import { TrashIcon } from './icons/TrashIcon';
import { UserIcon } from './icons/UserIcon';
import { PlusIcon } from './icons/PlusIcon';
import { ChevronDownIcon } from './icons/ChevronDownIcon';
import { ClipboardIcon } from './icons/ClipboardIcon';
import { CopyGenAIIcon } from './icons/CopyGenAIIcon';
interface ManagementCardProps {
title: string;
items: string[];
onAdd: (item: string) => void;
onRemove: (item: string) => void;
disabled?: boolean;
placeholder?: string;
}
const ManagementCard: React.FC<ManagementCardProps> = ({ title, items, onAdd, onRemove, disabled = false, placeholder }) => {
const [newItem, setNewItem] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (newItem.trim()) {
onAdd(newItem.trim());
setNewItem('');
}
};
return (
<div className="bg-white rounded-[10px] shadow-md p-6 border border-grey-300 flex flex-col h-full">
<h2 className="text-xl font-bold text-primary-blue mb-4">{title}</h2>
<form onSubmit={handleSubmit} className="flex gap-2 mb-4">
<input
type="text"
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
placeholder={placeholder || `New ${title.slice(0, -1)}...`}
className="flex-grow p-2 border-2 border-grey-700 rounded-[10px] focus:ring-2 focus:ring-active-blue focus:border-active-blue transition disabled:bg-grey-100 disabled:cursor-not-allowed text-black-title"
disabled={disabled}
/>
<button
type="submit"
className="bg-active-blue text-white font-semibold py-2 px-6 rounded-full hover:bg-active-blue/90 transition-colors duration-300 disabled:bg-grey-700 disabled:cursor-not-allowed"
disabled={!newItem.trim() || disabled}
>
Add
</button>
</form>
<div className="flex-1 overflow-y-auto pr-2 -mr-2">
{items.length > 0 ? (
<ul className="space-y-2">
{items.map((item) => (
<li
key={item}
className="flex items-center justify-between bg-grey-100 p-2.5 rounded-[10px] border border-grey-300 group hover:border-active-blue/30 transition-colors"
>
<span className="text-black-title">{item}</span>
<button
onClick={() => onRemove(item)}
className={`text-grey-700 hover:text-error transition-opacity ${disabled ? 'opacity-0 cursor-not-allowed' : 'opacity-0 group-hover:opacity-100'}`}
title={`Remove ${item}`}
disabled={disabled}
>
<TrashIcon className="h-5 w-5" />
</button>
</li>
))}
</ul>
) : (
<div className="text-center py-8 text-grey-700 bg-grey-100 rounded-[10px]">
No items found.
</div>
)}
</div>
</div>
);
};
// --- USER MANAGEMENT ---
interface User {
id: string;
name: string;
email: string;
agency: string;
}
const INITIAL_USERS: User[] = [
{ id: '1', name: "Steve O'Donoghue", email: "steveodonoghue@oliver.agency", agency: "OLIVER Agency" },
{ id: '2', name: "Jane Doe", email: "jane.doe@barclays.com", agency: "Barclays" },
{ id: '3', name: "Sarah Jenkins", email: "sarah.jenkins@mindshare.com", agency: "Mindshare" },
];
// Fallback agencies if API fails
const DEFAULT_AGENCIES = ["OLIVER Agency", "Barclays", "Mindshare", "Zenith", "Unassigned"];
const UsersTab: React.FC = () => {
const [users, setUsers] = useState<User[]>(() => {
const saved = localStorage.getItem('barclays_modcomms_users');
return saved ? JSON.parse(saved) : INITIAL_USERS;
});
const [agencies, setAgencies] = useState<string[]>(DEFAULT_AGENCIES);
const [newName, setNewName] = useState('');
const [newEmail, setNewEmail] = useState('');
const [newAgency, setNewAgency] = useState('');
// Load agencies from API
useEffect(() => {
const loadAgencies = async () => {
try {
const response = await apiService.getAgencies();
const agencyNames = response.map(a => a.name);
setAgencies(agencyNames.length > 0 ? agencyNames : DEFAULT_AGENCIES);
if (agencyNames.length > 0 && !newAgency) {
setNewAgency(agencyNames[0]);
}
} catch (error) {
console.error('Failed to load agencies:', error);
setAgencies(DEFAULT_AGENCIES);
}
};
loadAgencies();
}, []);
useEffect(() => {
localStorage.setItem('barclays_modcomms_users', JSON.stringify(users));
}, [users]);
const handleAddUser = (e: React.FormEvent) => {
e.preventDefault();
if (newName && newEmail) {
const newUser: User = {
id: Date.now().toString(),
name: newName,
email: newEmail,
agency: newAgency
};
setUsers([...users, newUser]);
setNewName('');
setNewEmail('');
}
};
const handleAgencyChange = (userId: string, newAgency: string) => {
setUsers(users.map(u => u.id === userId ? { ...u, agency: newAgency } : u));
};
const handleDeleteUser = (userId: string) => {
if (window.confirm('Are you sure you want to remove this user?')) {
setUsers(users.filter(u => u.id !== userId));
}
};
return (
<div className="space-y-8">
{/* Add User Section */}
<div className="bg-white rounded-[10px] shadow-md p-6 border border-grey-300">
<h3 className="text-lg font-bold text-primary-blue mb-4 flex items-center gap-2">
<PlusIcon className="h-5 w-5 text-active-blue" />
Add New User
</h3>
<form onSubmit={handleAddUser} className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div>
<label className="block text-xs font-medium text-black-title mb-1">Name</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="w-full p-2 border-2 border-grey-700 rounded-[10px] focus:ring-2 focus:ring-active-blue focus:border-active-blue text-black-title"
placeholder="e.g. John Smith"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-black-title mb-1">Email</label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
className="w-full p-2 border-2 border-grey-700 rounded-[10px] focus:ring-2 focus:ring-active-blue focus:border-active-blue text-black-title"
placeholder="user@example.com"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-black-title mb-1">Agency</label>
<div className="relative">
<select
value={newAgency}
onChange={(e) => setNewAgency(e.target.value)}
className="w-full p-2 border-2 border-grey-700 rounded-[10px] focus:ring-2 focus:ring-active-blue focus:border-active-blue appearance-none text-black-title"
>
{agencies.map(a => <option key={a} value={a}>{a}</option>)}
</select>
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-grey-700 pointer-events-none" />
</div>
</div>
<button
type="submit"
className="bg-active-blue text-white font-semibold py-2 px-6 rounded-full hover:bg-active-blue/90 transition-colors"
>
Add User
</button>
</form>
</div>
{/* User List */}
<div className="bg-white rounded-[10px] shadow-md overflow-hidden border border-grey-300">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-lime">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-black-title uppercase tracking-wider">Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-black-title uppercase tracking-wider">Email</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-black-title uppercase tracking-wider">Assigned Agency</th>
<th scope="col" className="relative px-6 py-3"><span className="sr-only">Actions</span></th>
</tr>
</thead>
<tbody className="divide-y divide-grey-300">
{users.map((user, index) => (
<tr key={user.id} className={index % 2 === 0 ? 'bg-white' : 'bg-grey-100'}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-info-light flex items-center justify-center text-active-blue">
<UserIcon className="h-4 w-4" />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-black-title">{user.name}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-black-title">
{user.email}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-black-title">
<div className="relative max-w-xs">
<select
value={user.agency}
onChange={(e) => handleAgencyChange(user.id, e.target.value)}
className="w-full bg-white border-2 border-active-blue text-black-title py-1.5 pl-3 pr-8 rounded-[10px] text-sm focus:outline-none focus:ring-2 focus:ring-active-blue appearance-none"
>
{agencies.map(a => <option key={a} value={a}>{a}</option>)}
</select>
<ChevronDownIcon className="absolute right-2 top-1/2 -translate-y-1/2 h-3 w-3 text-active-blue pointer-events-none" />
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleDeleteUser(user.id)}
className="text-grey-700 hover:text-error transition-colors"
title="Remove User"
>
<TrashIcon className="h-5 w-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
// --- SETTINGS COMPONENT ---
interface SettingsProps {
options: DropdownOptions;
onAddCampaign: (value: string) => void;
onRemoveCampaign: (value: string) => void;
onAddChannel: (value: string) => void;
onRemoveChannel: (value: string) => void;
onAddSubChannel: (channel: string, value: string) => void;
onRemoveSubChannel: (channel: string, value: string) => void;
onAddProofType: (channel: string, subChannel: string, value: string) => void;
onRemoveProofType: (channel: string, subChannel: string, value: string) => void;
onNavigate: (view: string) => void;
}
type Tab = 'Campaigns' | 'Channels' | 'SubChannels' | 'ProofTypes' | 'Users' | 'Beta';
export const Settings: React.FC<SettingsProps> = ({
options,
onAddCampaign,
onRemoveCampaign,
onAddChannel,
onRemoveChannel,
onAddSubChannel,
onRemoveSubChannel,
onAddProofType,
onRemoveProofType,
onNavigate,
}) => {
const [activeTab, setActiveTab] = useState<Tab>('Channels');
const [selectedChannel, setSelectedChannel] = useState<string>('');
const [selectedSubChannel, setSelectedSubChannel] = useState<string>('');
// Update selected channel if it disappears
useEffect(() => {
if (selectedChannel && !options.channels[selectedChannel]) {
setSelectedChannel('');
setSelectedSubChannel('');
}
}, [options.channels, selectedChannel]);
// Update selected sub-channel if it disappears
useEffect(() => {
if (selectedChannel && selectedSubChannel) {
const subChannels = options.channels[selectedChannel] || {};
if (!subChannels[selectedSubChannel]) {
setSelectedSubChannel('');
}
}
}, [options.channels, selectedChannel, selectedSubChannel]);
return (
<div className="p-4 sm:p-6 lg:p-8 h-full bg-grey-100 flex flex-col">
<header className="mb-6 flex-shrink-0">
<h1 className="text-3xl lg:text-4xl font-bold text-primary-blue">Settings</h1>
<p className="text-base lg:text-lg text-primary-blue mt-1">
Configure application defaults and user access.
</p>
</header>
<div className="flex-1 flex flex-col min-h-0">
{/* Tabs Header */}
<div className="border-b border-grey-300 bg-white px-4 rounded-t-[10px] shadow-sm flex-shrink-0">
<nav className="-mb-px flex space-x-8 overflow-x-auto" aria-label="Tabs">
{[
{ id: 'Campaigns', label: 'Campaigns' },
{ id: 'Channels', label: 'Channels' },
{ id: 'SubChannels', label: 'Sub-channels' },
{ id: 'ProofTypes', label: 'Proof Types' },
{ id: 'Users', label: 'Users' },
{ id: 'Beta', label: '[Beta]' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as Tab)}
className={`
whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors
${activeTab === tab.id
? 'border-active-blue text-active-blue'
: 'border-transparent text-black-title hover:text-active-blue hover:border-grey-300'
}
`}
aria-current={activeTab === tab.id ? 'page' : undefined}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tabs Content */}
<div className="flex-1 bg-transparent pt-6 overflow-y-auto">
{activeTab === 'Campaigns' && (
<div className="max-w-3xl">
<ManagementCard
title="Campaigns"
items={options.campaigns}
onAdd={onAddCampaign}
onRemove={onRemoveCampaign}
placeholder="e.g. Q4 Marketing"
/>
</div>
)}
{activeTab === 'Channels' && (
<div className="max-w-3xl">
<ManagementCard
title="Channels"
items={Object.keys(options.channels)}
onAdd={onAddChannel}
onRemove={onRemoveChannel}
placeholder="e.g. Social, OOH"
/>
</div>
)}
{activeTab === 'SubChannels' && (
<div className="max-w-3xl space-y-4">
<div className="bg-white p-4 rounded-[10px] border border-grey-300 shadow-sm">
<label className="block text-sm font-medium text-black-title mb-2">Select Parent Channel</label>
<div className="relative">
<select
value={selectedChannel}
onChange={(e) => setSelectedChannel(e.target.value)}
className={`w-full p-2 border-2 rounded-[10px] focus:ring-2 focus:ring-active-blue appearance-none text-black-title ${selectedChannel ? 'bg-info-light border-active-blue' : 'border-active-blue'}`}
>
<option value="" disabled>Select Channel</option>
{Object.keys(options.channels).map(c => <option key={c} value={c}>{c}</option>)}
</select>
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-active-blue pointer-events-none"/>
</div>
</div>
<ManagementCard
title={selectedChannel ? `Sub-Channels for ${selectedChannel}` : "Sub-Channels"}
items={selectedChannel ? Object.keys(options.channels[selectedChannel]) : []}
onAdd={(val) => onAddSubChannel(selectedChannel, val)}
onRemove={(val) => onRemoveSubChannel(selectedChannel, val)}
disabled={!selectedChannel}
placeholder="e.g. Meta, Video"
/>
</div>
)}
{activeTab === 'ProofTypes' && (
<div className="max-w-3xl space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-[10px] border border-grey-300 shadow-sm">
<label className="block text-sm font-medium text-black-title mb-2">Select Channel</label>
<div className="relative">
<select
value={selectedChannel}
onChange={(e) => {
setSelectedChannel(e.target.value);
setSelectedSubChannel('');
}}
className={`w-full p-2 border-2 rounded-[10px] focus:ring-2 focus:ring-active-blue appearance-none text-black-title ${selectedChannel ? 'bg-info-light border-active-blue' : 'border-active-blue'}`}
>
<option value="" disabled>Select Channel</option>
{Object.keys(options.channels).map(c => <option key={c} value={c}>{c}</option>)}
</select>
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-active-blue pointer-events-none"/>
</div>
</div>
<div className="bg-white p-4 rounded-[10px] border border-grey-300 shadow-sm">
<label className="block text-sm font-medium text-black-title mb-2">Select Sub-Channel</label>
<div className="relative">
<select
value={selectedSubChannel}
onChange={(e) => setSelectedSubChannel(e.target.value)}
className={`w-full p-2 border-2 rounded-[10px] focus:ring-2 focus:ring-active-blue appearance-none text-black-title disabled:bg-grey-100 disabled:border-grey-300 ${selectedSubChannel ? 'bg-info-light border-active-blue' : 'border-active-blue'}`}
disabled={!selectedChannel}
>
<option value="" disabled>Select Sub-Channel</option>
{selectedChannel && Object.keys(options.channels[selectedChannel]).map(sc => (
<option key={sc} value={sc}>{sc}</option>
))}
</select>
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-active-blue pointer-events-none"/>
</div>
</div>
</div>
<ManagementCard
title={selectedSubChannel ? `Proof Types for ${selectedSubChannel}` : "Proof Types"}
items={
(selectedChannel && selectedSubChannel)
? (options.channels[selectedChannel][selectedSubChannel] || [])
: []
}
onAdd={(val) => onAddProofType(selectedChannel, selectedSubChannel, val)}
onRemove={(val) => onRemoveProofType(selectedChannel, selectedSubChannel, val)}
disabled={!selectedChannel || !selectedSubChannel}
placeholder="e.g. In-feed 1x1, 300x600"
/>
</div>
)}
{activeTab === 'Users' && (
<UsersTab />
)}
{activeTab === 'Beta' && (
<div className="max-w-3xl space-y-6">
<div className="bg-white rounded-[10px] shadow-md p-6 border border-grey-300">
<h2 className="text-xl font-bold text-primary-blue mb-2 flex items-center gap-2">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-electric-violet opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-electric-violet"></span>
</span>
Beta Features
</h2>
<p className="text-black-title mb-6">
Explore experimental features currently in development. These tools are functional but may change over time.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={() => onNavigate('WIP Reviewer')}
className="flex flex-col items-start p-4 rounded-[10px] border border-grey-300 hover:border-active-blue hover:bg-info-light transition-all duration-300 group text-left"
>
<div className="p-2 rounded-[10px] bg-info-light text-active-blue mb-3 group-hover:scale-110 transition-transform">
<ClipboardIcon className="h-6 w-6" />
</div>
<h3 className="font-bold text-primary-blue mb-1">WIP Reviewer</h3>
<p className="text-sm text-grey-900">Early-stage feedback on assets before they are finalized.</p>
</button>
<button
onClick={() => onNavigate('CopyGenAI')}
className="flex flex-col items-start p-4 rounded-[10px] border border-grey-300 hover:border-electric-violet hover:bg-info-light transition-all duration-300 group text-left"
>
<div className="p-2 rounded-[10px] bg-electric-violet/10 text-electric-violet mb-3 group-hover:scale-110 transition-transform">
<CopyGenAIIcon className="h-6 w-6" />
</div>
<h3 className="font-bold text-primary-blue mb-1">CopyGenAI</h3>
<p className="text-sm text-grey-900">Generate and refine marketing copy with AI assistance.</p>
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};