548 lines
24 KiB
JavaScript
548 lines
24 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
||
import { useParams } from 'react-router-dom';
|
||
import apiClient from '../api/client';
|
||
|
||
const MAX_SMS_LENGTH = 160;
|
||
const PLACEHOLDER_TOKEN = '{#var#}';
|
||
const PLACEHOLDER_REGEX = /\{#var#\}/g;
|
||
|
||
function getVariantKey(slug, index) {
|
||
return `${slug}:${index}`;
|
||
}
|
||
|
||
function createVariantDraft(text = '') {
|
||
return {
|
||
originalText: text,
|
||
currentText: text,
|
||
validationStatus: 'idle',
|
||
why: '',
|
||
lastCheckedText: '',
|
||
};
|
||
}
|
||
|
||
function countPlaceholders(text = '') {
|
||
return (String(text).match(PLACEHOLDER_REGEX) || []).length;
|
||
}
|
||
|
||
function buildDraftsForVariants(slug, generatedVariants = []) {
|
||
const nextDrafts = {};
|
||
generatedVariants.forEach((variant, index) => {
|
||
nextDrafts[getVariantKey(slug, index)] = createVariantDraft(variant);
|
||
});
|
||
return nextDrafts;
|
||
}
|
||
|
||
function removeDraftsForSlug(drafts, slug) {
|
||
const nextDrafts = { ...drafts };
|
||
Object.keys(nextDrafts).forEach((key) => {
|
||
if (key.startsWith(`${slug}:`)) delete nextDrafts[key];
|
||
});
|
||
return nextDrafts;
|
||
}
|
||
|
||
function syncDraftsWithVariants(existingDrafts, variantsBySlug) {
|
||
const nextDrafts = {};
|
||
|
||
Object.entries(variantsBySlug).forEach(([slug, generatedVariants]) => {
|
||
generatedVariants.forEach((variant, index) => {
|
||
const key = getVariantKey(slug, index);
|
||
const existing = existingDrafts[key];
|
||
nextDrafts[key] = existing && existing.originalText === variant
|
||
? existing
|
||
: createVariantDraft(variant);
|
||
});
|
||
});
|
||
|
||
return nextDrafts;
|
||
}
|
||
|
||
function buildTemplateUiState(templates = []) {
|
||
const nextVariants = {};
|
||
const nextGenState = {};
|
||
|
||
templates.forEach((template) => {
|
||
if (!template?.eventSlug) return;
|
||
|
||
if (template.selectedTemplate) {
|
||
nextGenState[template.eventSlug] = 'selected';
|
||
return;
|
||
}
|
||
|
||
if (Array.isArray(template.generatedVariants) && template.generatedVariants.length > 0) {
|
||
nextVariants[template.eventSlug] = template.generatedVariants;
|
||
nextGenState[template.eventSlug] = 'done';
|
||
}
|
||
});
|
||
|
||
return { nextVariants, nextGenState };
|
||
}
|
||
|
||
export default function Events() {
|
||
const { businessId } = useParams();
|
||
const [events, setEvents] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [newLabel, setNewLabel] = useState('');
|
||
const [addingEvent, setAddingEvent] = useState(false);
|
||
const [showAddForm, setShowAddForm] = useState(false);
|
||
const [genState, setGenState] = useState({});
|
||
const [variants, setVariants] = useState({});
|
||
const [variantDrafts, setVariantDrafts] = useState({});
|
||
const [selectingVariantKey, setSelectingVariantKey] = useState('');
|
||
const [error, setError] = useState('');
|
||
const [readyToGenerate, setReadyToGenerate] = useState(false);
|
||
|
||
const loadEvents = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const [eventsRes, activeProfileRes, templatesRes] = await Promise.all([
|
||
apiClient.get(`/api/businesses/${businessId}/events`),
|
||
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })),
|
||
apiClient.get(`/api/businesses/${businessId}/templates`).catch(() => ({ data: { templates: [] } })),
|
||
]);
|
||
|
||
const templates = templatesRes.data.templates || [];
|
||
const { nextVariants, nextGenState } = buildTemplateUiState(templates);
|
||
|
||
setEvents(eventsRes.data.events || []);
|
||
setReadyToGenerate(!!activeProfileRes.data?.activeProfile?.rawCurl);
|
||
setVariants(nextVariants);
|
||
setGenState(nextGenState);
|
||
setVariantDrafts((currentDrafts) => syncDraftsWithVariants(currentDrafts, nextVariants));
|
||
} catch {
|
||
setError('Failed to load events');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [businessId]);
|
||
|
||
useEffect(() => {
|
||
loadEvents();
|
||
}, [loadEvents]);
|
||
|
||
async function handleAddEvent(e) {
|
||
e.preventDefault();
|
||
if (!newLabel.trim()) return;
|
||
setAddingEvent(true);
|
||
setError('');
|
||
try {
|
||
await apiClient.post(`/api/businesses/${businessId}/events`, { label: newLabel.trim() });
|
||
setNewLabel('');
|
||
setShowAddForm(false);
|
||
await loadEvents();
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to add event');
|
||
} finally {
|
||
setAddingEvent(false);
|
||
}
|
||
}
|
||
|
||
async function handleDelete(slug) {
|
||
try {
|
||
await apiClient.delete(`/api/businesses/${businessId}/events/${slug}`);
|
||
await loadEvents();
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to delete event');
|
||
}
|
||
}
|
||
|
||
async function handleGenerate(slug) {
|
||
if (!readyToGenerate) {
|
||
setError('Configure and activate a cURL profile before generating templates.');
|
||
return;
|
||
}
|
||
|
||
setGenState((state) => ({ ...state, [slug]: 'loading' }));
|
||
setError('');
|
||
|
||
try {
|
||
const res = await apiClient.post(`/api/businesses/${businessId}/events/${slug}/generate`);
|
||
const generatedVariants = res.data.variants || [];
|
||
|
||
setVariants((currentVariants) => ({ ...currentVariants, [slug]: generatedVariants }));
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...removeDraftsForSlug(currentDrafts, slug),
|
||
...buildDraftsForVariants(slug, generatedVariants),
|
||
}));
|
||
setGenState((state) => ({ ...state, [slug]: 'done' }));
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Generation failed');
|
||
setGenState((state) => ({ ...state, [slug]: 'error' }));
|
||
}
|
||
}
|
||
|
||
async function handleValidateEdit(slug, variantIndex) {
|
||
const variantKey = getVariantKey(slug, variantIndex);
|
||
const draft = variantDrafts[variantKey];
|
||
const editedTemplate = draft?.currentText || '';
|
||
|
||
if (!editedTemplate) return;
|
||
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...currentDrafts,
|
||
[variantKey]: {
|
||
...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)),
|
||
validationStatus: 'checking',
|
||
why: '',
|
||
lastCheckedText: '',
|
||
},
|
||
}));
|
||
setError('');
|
||
|
||
try {
|
||
const res = await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/validate-edit`, {
|
||
editedTemplate,
|
||
});
|
||
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...currentDrafts,
|
||
[variantKey]: {
|
||
...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)),
|
||
validationStatus: res.data?.approved ? 'approved' : 'rejected',
|
||
why: res.data?.why || '',
|
||
lastCheckedText: editedTemplate,
|
||
},
|
||
}));
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to validate edited template');
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...currentDrafts,
|
||
[variantKey]: {
|
||
...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)),
|
||
validationStatus: 'idle',
|
||
why: '',
|
||
lastCheckedText: '',
|
||
},
|
||
}));
|
||
}
|
||
}
|
||
|
||
async function handleSelect(slug, variant, variantIndex) {
|
||
const variantKey = getVariantKey(slug, variantIndex);
|
||
setSelectingVariantKey(variantKey);
|
||
setError('');
|
||
|
||
try {
|
||
await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
|
||
setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] }));
|
||
setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug));
|
||
setGenState((state) => ({ ...state, [slug]: 'selected' }));
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to select template');
|
||
} finally {
|
||
setSelectingVariantKey('');
|
||
}
|
||
}
|
||
|
||
function handleVariantChange(slug, variantIndex, nextText) {
|
||
const variantKey = getVariantKey(slug, variantIndex);
|
||
const originalText = variantDrafts[variantKey]?.originalText || variants[slug]?.[variantIndex] || '';
|
||
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...currentDrafts,
|
||
[variantKey]: {
|
||
originalText,
|
||
currentText: nextText,
|
||
validationStatus: 'idle',
|
||
why: '',
|
||
lastCheckedText: '',
|
||
},
|
||
}));
|
||
}
|
||
|
||
function handleRevertVariant(slug, variantIndex) {
|
||
const variantKey = getVariantKey(slug, variantIndex);
|
||
const originalText = variantDrafts[variantKey]?.originalText || variants[slug]?.[variantIndex] || '';
|
||
|
||
setVariantDrafts((currentDrafts) => ({
|
||
...currentDrafts,
|
||
[variantKey]: createVariantDraft(originalText),
|
||
}));
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center h-64">
|
||
<div className="w-8 h-8 border-2 border-indigo-200 border-t-indigo-600 rounded-full animate-spin" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="flex items-center justify-between pb-5 mb-6 border-b border-gray-200">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Events</h1>
|
||
<p className="text-sm text-gray-500 mt-1 font-medium">Generate SMS templates for each order event.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowAddForm((visible) => !visible)}
|
||
className="px-4 py-2 rounded-lg bg-white border border-gray-300 shadow-sm text-sm text-gray-700 font-semibold hover:bg-gray-50 transition"
|
||
>
|
||
{showAddForm ? 'Cancel' : '+ Add Event'}
|
||
</button>
|
||
</div>
|
||
|
||
{!readyToGenerate && (
|
||
<div className="mb-6 px-4 py-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-800 text-sm font-medium flex items-center gap-2">
|
||
<span>⚠️</span>
|
||
<span>Set up and activate a <strong>cURL profile</strong> before generating templates.</span>
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="mb-6 px-4 py-3 rounded-md bg-red-50 border border-red-200 text-red-700 font-medium text-sm flex items-center justify-between">
|
||
{error}
|
||
<button onClick={() => setError('')} className="text-red-500 hover:text-red-700 font-bold">×</button>
|
||
</div>
|
||
)}
|
||
|
||
{showAddForm && (
|
||
<form onSubmit={handleAddEvent} className="mb-8 flex gap-3 p-5 rounded-xl bg-gray-50 border border-gray-200 shadow-sm">
|
||
<input
|
||
value={newLabel}
|
||
onChange={(e) => setNewLabel(e.target.value)}
|
||
placeholder="Event name (e.g. Return Initiated)"
|
||
className="flex-1 px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 text-sm shadow-sm"
|
||
autoFocus
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={addingEvent || !newLabel.trim()}
|
||
className="px-6 py-2.5 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium transition shadow-sm disabled:opacity-50"
|
||
>
|
||
{addingEvent ? 'Adding…' : 'Add'}
|
||
</button>
|
||
</form>
|
||
)}
|
||
|
||
<div className="space-y-4">
|
||
{events.map((event) => {
|
||
const state = genState[event.slug] || 'idle';
|
||
const eventVariants = variants[event.slug] || [];
|
||
|
||
return (
|
||
<div key={event.slug} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-6 py-5 gap-4">
|
||
<div className="flex items-start gap-4">
|
||
{event.isDefault ? (
|
||
<div className="mt-0.5 w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center border border-gray-200 shrink-0" title="Default event">
|
||
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => handleDelete(event.slug)}
|
||
className="mt-0.5 w-6 h-6 rounded-full bg-red-50 hover:bg-red-100 flex items-center justify-center border border-red-100 text-red-500 transition shrink-0"
|
||
title="Delete event"
|
||
>
|
||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||
</button>
|
||
)}
|
||
<div>
|
||
<h3 className="text-base font-bold text-gray-900 tracking-tight">{event.label}</h3>
|
||
<p className="text-xs text-gray-500 font-mono mt-0.5">{event.slug}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
{state === 'selected' && (
|
||
<span className="text-xs font-semibold px-2.5 py-1 rounded-md bg-green-50 border border-green-200 text-green-700">
|
||
✓ Template Selected
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={() => handleGenerate(event.slug)}
|
||
disabled={state === 'loading' || !readyToGenerate}
|
||
className={`px-4 py-2 rounded-lg text-sm font-medium transition shadow-sm flex items-center gap-2 disabled:opacity-50 ${
|
||
state === 'done' || state === 'selected'
|
||
? 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'
|
||
: 'bg-indigo-50 border border-indigo-200 text-indigo-700 hover:bg-indigo-100'
|
||
}`}
|
||
>
|
||
{state === 'loading' ? (
|
||
<><span className="w-4 h-4 border-2 border-indigo-300 border-t-indigo-600 rounded-full animate-spin" /> Generating…</>
|
||
) : state === 'done' || state === 'selected' ? (
|
||
<>↺ Regenerate</>
|
||
) : (
|
||
<>⚡ Generate Template</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{eventVariants.length > 0 && (
|
||
<div className="border-t border-gray-100 bg-gray-50/50 px-6 py-5 space-y-4">
|
||
<p className="text-xs text-gray-500 font-bold uppercase tracking-wider">Review, edit, and choose a variant</p>
|
||
<div className="grid gap-4">
|
||
{eventVariants.map((variant, index) => {
|
||
const variantKey = getVariantKey(event.slug, index);
|
||
const draft = variantDrafts[variantKey] || createVariantDraft(variant);
|
||
const currentText = draft.currentText;
|
||
const originalText = draft.originalText;
|
||
const validationStatus = draft.validationStatus;
|
||
const currentMatchesCheckedText = draft.lastCheckedText === currentText;
|
||
const isEdited = currentText !== originalText;
|
||
const placeholderCount = countPlaceholders(currentText);
|
||
const originalPlaceholderCount = countPlaceholders(originalText);
|
||
const placeholderMismatch = placeholderCount !== originalPlaceholderCount;
|
||
const tooLong = currentText.length > MAX_SMS_LENGTH;
|
||
const isSelectingThis = selectingVariantKey === variantKey;
|
||
const isSelectingAnotherVariant = !!selectingVariantKey
|
||
&& selectingVariantKey !== variantKey
|
||
&& selectingVariantKey.startsWith(`${event.slug}:`);
|
||
const canRunCheck = isEdited && !tooLong && !placeholderMismatch && validationStatus !== 'checking';
|
||
const canUseEdited = isEdited
|
||
&& validationStatus === 'approved'
|
||
&& currentMatchesCheckedText
|
||
&& !tooLong
|
||
&& !placeholderMismatch;
|
||
|
||
return (
|
||
<div
|
||
key={variantKey}
|
||
className={`rounded-xl border bg-white p-5 shadow-sm transition ${
|
||
isSelectingThis
|
||
? 'border-indigo-300 ring-2 ring-indigo-100'
|
||
: 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||
<span className={`text-[11px] font-bold px-2 py-1 rounded-full border ${
|
||
isEdited
|
||
? 'bg-amber-50 border-amber-200 text-amber-700'
|
||
: 'bg-gray-50 border-gray-200 text-gray-600'
|
||
}`}>
|
||
{isEdited ? 'Edited Draft' : 'Original Draft'}
|
||
</span>
|
||
|
||
{validationStatus === 'checking' && (
|
||
<span className="text-[11px] font-bold px-2 py-1 rounded-full border bg-blue-50 border-blue-200 text-blue-700">
|
||
Checking edit…
|
||
</span>
|
||
)}
|
||
|
||
{validationStatus === 'approved' && currentMatchesCheckedText && (
|
||
<span className="text-[11px] font-bold px-2 py-1 rounded-full border bg-green-50 border-green-200 text-green-700">
|
||
Edit passed check
|
||
</span>
|
||
)}
|
||
|
||
{validationStatus === 'rejected' && currentMatchesCheckedText && (
|
||
<span className="text-[11px] font-bold px-2 py-1 rounded-full border bg-red-50 border-red-200 text-red-700">
|
||
Needs changes
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<textarea
|
||
value={currentText}
|
||
onChange={(e) => handleVariantChange(event.slug, index, e.target.value)}
|
||
rows={4}
|
||
className={`w-full rounded-xl border px-4 py-3 text-sm text-gray-800 font-mono leading-relaxed resize-y focus:outline-none focus:ring-2 ${
|
||
isEdited
|
||
? 'border-amber-200 bg-amber-50/40 focus:ring-amber-200 focus:border-amber-300'
|
||
: 'border-gray-200 bg-gray-50 focus:ring-indigo-100 focus:border-indigo-300'
|
||
}`}
|
||
/>
|
||
|
||
<div className="flex flex-wrap items-center justify-between gap-3 mt-3">
|
||
<span className={`text-xs font-semibold px-2.5 py-1 rounded-md border ${
|
||
tooLong
|
||
? 'bg-red-50 border-red-200 text-red-700'
|
||
: 'bg-gray-100 border-gray-200 text-gray-600'
|
||
}`}>
|
||
{currentText.length} / {MAX_SMS_LENGTH}
|
||
</span>
|
||
|
||
{isEdited && (
|
||
<button
|
||
onClick={() => handleRevertVariant(event.slug, index)}
|
||
className="text-xs px-3 py-2 rounded-md bg-white border border-gray-300 text-gray-700 font-semibold hover:bg-gray-50 transition"
|
||
>
|
||
Revert to original
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{isEdited && (
|
||
<div className="mt-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
|
||
<p className="text-[11px] font-bold uppercase tracking-wider text-gray-500 mb-2">Original generated version</p>
|
||
<p className="text-sm text-gray-600 font-mono leading-relaxed">{originalText}</p>
|
||
</div>
|
||
)}
|
||
|
||
{placeholderMismatch && (
|
||
<div className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||
Keep the same number of <span className="font-mono">{PLACEHOLDER_TOKEN}</span> placeholders.
|
||
This draft has {placeholderCount}, while the generated version has {originalPlaceholderCount}.
|
||
</div>
|
||
)}
|
||
|
||
{tooLong && (
|
||
<div className="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||
Shorten this template to {MAX_SMS_LENGTH} characters or less before checking or using the edited version.
|
||
</div>
|
||
)}
|
||
|
||
{validationStatus === 'rejected' && currentMatchesCheckedText && draft.why && (
|
||
<div className="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||
<span className="font-semibold">Why it did not pass:</span> {draft.why}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex flex-wrap items-center gap-2 mt-4">
|
||
{!isEdited ? (
|
||
<button
|
||
onClick={() => handleSelect(event.slug, currentText, index)}
|
||
disabled={isSelectingThis || isSelectingAnotherVariant}
|
||
className="text-xs px-4 py-2 rounded-md bg-indigo-600 hover:bg-indigo-700 text-white font-bold transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{isSelectingThis ? 'Selecting…' : 'Use this template'}
|
||
</button>
|
||
) : (
|
||
<>
|
||
<button
|
||
onClick={() => handleSelect(event.slug, originalText, index)}
|
||
disabled={isSelectingThis || isSelectingAnotherVariant}
|
||
className="text-xs px-4 py-2 rounded-md bg-white border border-gray-300 text-gray-700 font-bold hover:bg-gray-50 transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{isSelectingThis ? 'Selecting…' : 'Use original'}
|
||
</button>
|
||
|
||
{canUseEdited ? (
|
||
<button
|
||
onClick={() => handleSelect(event.slug, currentText, index)}
|
||
disabled={isSelectingThis || isSelectingAnotherVariant}
|
||
className="text-xs px-4 py-2 rounded-md bg-green-600 hover:bg-green-700 text-white font-bold transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{isSelectingThis ? 'Selecting…' : 'Use edited version'}
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={() => handleValidateEdit(event.slug, index)}
|
||
disabled={!canRunCheck || isSelectingThis || isSelectingAnotherVariant}
|
||
className="text-xs px-4 py-2 rounded-md bg-indigo-600 hover:bg-indigo-700 text-white font-bold transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||
>
|
||
{validationStatus === 'checking'
|
||
? 'Checking…'
|
||
: validationStatus === 'rejected' && currentMatchesCheckedText
|
||
? 'Check again'
|
||
: 'Check edit'}
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|