modcomms/frontend/components/CreateCampaignModal.tsx
michael 8038e4014e Make Workfront Campaign ID optional and propagate to proofs
The Workfront Campaign ID field is now optional when creating campaigns.
When provided, it is used as the base for proof workfront IDs instead of
generating random ones.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 09:49:23 -06:00

193 lines
9.3 KiB
TypeScript
Executable file

import React, { useState, useEffect } from 'react';
import { XIcon } from './icons/XIcon';
import apiService from '../services/apiService';
interface CreateCampaignModalProps {
isOpen: boolean;
onClose: () => void;
onAddCampaign: (campaignData: {
name: string;
workfrontId: string;
clientLead: string;
agencyLead: string;
brandGuidelines: string;
}) => void;
brandGuidelines?: string[];
}
export const CreateCampaignModal: React.FC<CreateCampaignModalProps> = ({ isOpen, onClose, onAddCampaign, brandGuidelines: brandGuidelineOptions = [] }) => {
const [name, setName] = useState('');
const [selectedBrandGuideline, setSelectedBrandGuideline] = useState('');
const [workfrontId, setWorkfrontId] = useState('');
const [clientLead, setClientLead] = useState('');
const [agencyLead, setAgencyLead] = useState('');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Reset form when modal opens
if (isOpen) {
setName('');
setSelectedBrandGuideline('');
setWorkfrontId('');
setClientLead('');
setAgencyLead('');
setError(null);
}
}, [isOpen]);
const validateWorkfrontId = (id: string): boolean => {
// Empty is valid (field is optional)
if (id.length === 0) {
setError(null);
return true;
}
const isValid = /^#WF_\d+$/.test(id);
if (!isValid) {
setError("Workfront Campaign ID must be in the format '#WF_12345'");
} else {
setError(null);
}
return isValid;
};
const handleWorkfrontIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newId = e.target.value;
setWorkfrontId(newId);
validateWorkfrontId(newId);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !clientLead.trim() || !agencyLead.trim() || !selectedBrandGuideline.trim()) {
return;
}
// Validate workfrontId format only if provided
if (workfrontId.trim() && !/^#WF_\d+$/.test(workfrontId)) {
setError("Workfront Campaign ID must be in the format '#WF_12345'");
return;
}
setError(null);
onAddCampaign({ name, workfrontId: workfrontId.trim(), clientLead, agencyLead, brandGuidelines: selectedBrandGuideline });
onClose();
};
if (!isOpen) return null;
const isFormInvalid = !name.trim() || !clientLead.trim() || !agencyLead.trim() || !selectedBrandGuideline.trim();
return (
<div
className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50 transition-opacity duration-300"
onClick={onClose}
aria-modal="true"
role="dialog"
>
<div
className="bg-white rounded-lg shadow-xl p-6 sm:p-8 w-full max-w-lg transform transition-all"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-brand-dark-blue">Create New Campaign</h2>
<button onClick={onClose} className="p-1 rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-800 transition-colors">
<XIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit} noValidate>
<div className="space-y-4">
<div>
<label htmlFor="campaign-name" className="block text-sm font-medium text-gray-700">Campaign Name</label>
<input
type="text"
id="campaign-name"
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900"
required
/>
</div>
<div>
<label htmlFor="brand-guidelines" className="block text-sm font-medium text-gray-700">Brand Guidelines</label>
<select
id="brand-guidelines"
value={selectedBrandGuideline}
onChange={(e) => setSelectedBrandGuideline(e.target.value)}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900"
required
>
<option value="" disabled>Select brand guidelines</option>
{brandGuidelineOptions.map((brand) => (
<option key={brand} value={brand}>{brand}</option>
))}
</select>
</div>
<div>
<label htmlFor="workfront-id" className="block text-sm font-medium text-gray-700">Workfront Campaign ID <span className="text-gray-400 font-normal">(Optional)</span></label>
<input
type="text"
id="workfront-id"
value={workfrontId}
onChange={handleWorkfrontIdChange}
className={`mt-1 block w-full p-2 border rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900 placeholder:text-gray-400 ${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300'}`}
placeholder="#WF_12345"
aria-invalid={!!error}
aria-describedby="workfront-id-error"
/>
{error && <p id="workfront-id-error" className="mt-1 text-sm text-red-600">{error}</p>}
</div>
<div>
<label htmlFor="client-lead" className="block text-sm font-medium text-gray-700">Client Lead</label>
<input
type="text"
id="client-lead"
value={clientLead}
onChange={(e) => setClientLead(e.target.value)}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Agency</label>
<input
type="text"
value="OLIVER Agency"
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-600 cursor-not-allowed"
disabled
/>
</div>
<div>
<label htmlFor="agency-lead" className="block text-sm font-medium text-gray-700">Agency Lead</label>
<input
type="text"
id="agency-lead"
value={agencyLead}
onChange={(e) => setAgencyLead(e.target.value)}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-accent focus:border-brand-accent transition bg-white text-gray-900"
required
/>
</div>
</div>
<div className="mt-8 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="bg-gray-200 text-gray-800 font-semibold py-2 px-4 rounded-md hover:bg-gray-300 transition-colors duration-300"
>
Cancel
</button>
<button
type="submit"
className="bg-brand-accent text-white font-semibold py-2 px-4 rounded-md hover:bg-brand-dark-blue transition-colors duration-300 disabled:bg-gray-400 disabled:cursor-not-allowed"
disabled={isFormInvalid || !!error}
>
Create Campaign
</button>
</div>
</form>
</div>
</div>
);
};