mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
A focused cleanup PR that addresses the high-signal findings from a self-review of PRs #148-153. Every change is local to the templates surface; no schema or wire-format changes. Safety ------ * Delete now goes through a confirmation Dialog (mirrors the existing tag-manager pattern). Copy explicitly tells the user when Meta will be hit, so a slip on the trash icon doesn't take the template off Meta as well as locally. * AUTHENTICATION submit is now blocked by a persistent in-dialog banner + disabled submit button. The disappearing toast that used to fire on submit is gone — defensive `return` remains in handleSubmit as a second line of defense. * updateButton uses a per-variant switch + ButtonPatch union instead of the partial-spread + `as TemplateButton` cast. Each variant's spread keeps its discriminant pinned; TS enforces exhaustiveness. * New template-row-guard.ts surfaces malformed Supabase rows at the route boundary instead of letting buildSendComponents crash deep in the stack with an opaque TypeError. * UUID regex check on /[id] PATCH+DELETE so non-UUID payloads return a clean 400 before hitting Supabase. Submit-route cleanup -------------------- * Two near-identical upsert blocks (error path + success path) collapsed into a buildUpsertRow + upsertTemplateRow helper. Adding a column now touches one place, not two. UX copy + feedback ------------------ * Submit success toast sets the right expectation: "typical review time is 24 hours. Status updates automatically." Old copy just said "status will update once approved." * Header media URL helper text is now actually useful: HTTPS requirement, ~24h lifetime, per-format size/format recommendations (JPEG/PNG ≥800×418 ≤5 MB for images, MP4 ≤16 MB for video, etc). * Truncated-sync toast switches from `warning` (which auto-dismisses fast) to `error` with `duration: 10000` and concrete copy: "Synced the first 2000 templates only — your account has more. Sync again to continue." * Edit and Resubmit buttons gain visible text labels alongside their icons + better aria-label/title pairs. A lone rotate icon wasn't self-explanatory. * Broadcast picker drops the redundant "Approved" chip — every template in that list is APPROVED by filter, so the chip carried zero information. Unused templateStatusConfig import removed. State + a11y ------------ * handleSubmit now awaits fetchTemplates BEFORE closing the dialog so re-opening immediately doesn't see a stale list. * Header content / media URL / sample are preserved across format switches in the New Template form. The payload builder is already format-aware, so orphan values on hidden fields are harmless — and keeping them lets users switch formats to compare without losing typing. * Button-type Select gains the same null-guard the header Select got in PR #148 (per @base-ui semantics, onValueChange can fire with null on deselect). * Dynamic body-sample inputs get id + aria-label so each one is individually addressable (WCAG A — screen-reader announcement beat "edit text"). Header text + header sample inputs too. Deliberately NOT here --------------------- * The 940-line template-manager component is not split. It's cohesive (one feature surface) and splitting for size alone would be over-engineering. * extractVariableIndices in the buttons loop is not memoized. Regex on a short URL string is sub-microsecond; a real profiler would never flag it. * No JS heap secret-clearing for access tokens. Strings are immutable in JS — there's no way to actually clear one. Tests: 256/256 pass (no new tests required — pure refactor + copy + new UI elements). Lint 0 errors. Typecheck clean. Build succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { createClient } from '@/lib/supabase/client';
|
|
import { MessageTemplate } from '@/types';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Loader2, FileText, ArrowRight } from 'lucide-react';
|
|
|
|
const categoryColors: Record<string, string> = {
|
|
Marketing: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
|
|
Utility: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
|
|
Authentication: 'bg-orange-500/10 text-orange-400 border-orange-500/20',
|
|
};
|
|
|
|
interface Step1Props {
|
|
selectedTemplate: MessageTemplate | null;
|
|
onSelect: (template: MessageTemplate) => void;
|
|
onNext: () => void;
|
|
onBack: () => void;
|
|
}
|
|
|
|
export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack }: Step1Props) {
|
|
const [templates, setTemplates] = useState<MessageTemplate[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function fetchTemplates() {
|
|
try {
|
|
const supabase = createClient();
|
|
// Only APPROVED templates can be sent via Meta — anything else
|
|
// would 400 at broadcast time. Hide them rather than letting
|
|
// the user pick a template that will fail.
|
|
const { data, error: fetchError } = await supabase
|
|
.from('message_templates')
|
|
.select('*')
|
|
.eq('status', 'APPROVED')
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (fetchError) throw fetchError;
|
|
setTemplates(data ?? []);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load templates');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchTemplates();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex h-64 flex-col items-center justify-center gap-2">
|
|
<p className="text-sm text-red-400">{error}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white">Choose a Template</h2>
|
|
<p className="mt-1 text-sm text-slate-400">
|
|
Select an approved message template for your broadcast.
|
|
</p>
|
|
</div>
|
|
|
|
{templates.length === 0 ? (
|
|
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900/50">
|
|
<FileText className="mb-2 h-8 w-8 text-slate-600" />
|
|
<p className="text-sm text-slate-400">No templates available.</p>
|
|
<p className="mt-1 text-xs text-slate-500">Create a template in Settings first.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{templates.map((template) => {
|
|
const isSelected = selectedTemplate?.id === template.id;
|
|
const catColor = categoryColors[template.category] ?? categoryColors.Utility;
|
|
|
|
return (
|
|
<button
|
|
key={template.id}
|
|
onClick={() => onSelect(template)}
|
|
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
|
|
isSelected
|
|
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
|
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700 hover:bg-slate-900'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<h3 className="text-sm font-medium text-white">{template.name}</h3>
|
|
<span
|
|
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${catColor}`}
|
|
>
|
|
{template.category}
|
|
</span>
|
|
</div>
|
|
<p className="line-clamp-3 text-xs text-slate-400">{template.body_text}</p>
|
|
<div className="flex items-center gap-2 text-[10px] text-slate-500">
|
|
<span>{template.language ?? 'en_US'}</span>
|
|
{/* Status is omitted on purpose — every template
|
|
shown here is already filtered to APPROVED,
|
|
so the chip carried no information. */}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between border-t border-slate-800 pt-4">
|
|
<Button variant="outline" onClick={onBack} className="border-slate-700 text-slate-300">
|
|
Back
|
|
</Button>
|
|
<Button
|
|
onClick={onNext}
|
|
disabled={!selectedTemplate}
|
|
className="bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
>
|
|
Next
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|