wacrm/src/components/broadcasts/step4-schedule-send.tsx
Arnas Donauskas 4aa329b527
feat(theme): add light/dark mode toggle (#243)
Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.

- globals.css: split neutrals (mode) from accent (theme); add light
  palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
  (no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
  tokens across the app so every surface flips with mode. Flow canvas
  and message-bubble timestamps made mode-aware too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:29:46 +03:00

237 lines
7.8 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 { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { ArrowLeft, Send, Loader2, Users, Save } from 'lucide-react';
interface AudienceConfig {
type: string;
tagIds?: string[];
csvContacts?: { phone: string; name?: string }[];
}
interface Step4Props {
name: string;
onNameChange: (name: string) => void;
template: MessageTemplate;
audience: AudienceConfig;
onSend: () => void;
onSaveDraft?: () => void;
onBack: () => void;
isProcessing: boolean;
progress: number;
}
export function Step4ScheduleSend({
name,
onNameChange,
template,
audience,
onSend,
onSaveDraft,
onBack,
isProcessing,
progress,
}: Step4Props) {
const [showConfirm, setShowConfirm] = useState(false);
const [estimatedReach, setEstimatedReach] = useState<number>(0);
const [loadingReach, setLoadingReach] = useState(true);
useEffect(() => {
async function calculateReach() {
setLoadingReach(true);
try {
const supabase = createClient();
if (audience.type === 'all') {
const { count } = await supabase
.from('contacts')
.select('*', { count: 'exact', head: true });
setEstimatedReach(count ?? 0);
} else if (audience.type === 'tags' && audience.tagIds && audience.tagIds.length > 0) {
const { data: contactTags } = await supabase
.from('contact_tags')
.select('contact_id')
.in('tag_id', audience.tagIds);
const uniqueIds = new Set((contactTags ?? []).map((ct) => ct.contact_id));
setEstimatedReach(uniqueIds.size);
} else if (audience.type === 'csv' && audience.csvContacts) {
setEstimatedReach(audience.csvContacts.length);
} else {
setEstimatedReach(0);
}
} finally {
setLoadingReach(false);
}
}
calculateReach();
}, [audience]);
const audienceLabel =
audience.type === 'all'
? 'All Contacts'
: audience.type === 'tags'
? `Tags (${audience.tagIds?.length ?? 0} selected)`
: audience.type === 'csv'
? 'CSV Upload'
: 'Custom';
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-foreground">Review & Send</h2>
<p className="mt-1 text-sm text-muted-foreground">
Name your broadcast, review the details, and send.
</p>
</div>
{/* Broadcast Name */}
<div>
<label className="mb-1.5 block text-sm font-medium text-foreground">Broadcast Name</label>
<Input
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder="e.g. Summer Sale Announcement"
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/>
</div>
{/* Summary Card */}
<div className="rounded-xl border border-border bg-card/50 p-4 space-y-3">
<p className="text-sm font-medium text-foreground">Summary</p>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-xs text-muted-foreground">Template</p>
<p className="text-foreground">{template.name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Audience</p>
<p className="text-foreground">{audienceLabel}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Estimated Reach</p>
<div className="flex items-center gap-1.5">
{loadingReach ? (
<Loader2 className="h-3 w-3 animate-spin text-primary" />
) : (
<>
<Users className="h-3.5 w-3.5 text-primary" />
<p className="font-medium text-foreground">{estimatedReach.toLocaleString()}</p>
</>
)}
</div>
</div>
<div>
<p className="text-xs text-muted-foreground">Language</p>
<p className="text-foreground">{template.language ?? 'en_US'}</p>
</div>
</div>
</div>
{/* Processing overlay */}
{isProcessing && (
<div className="rounded-xl border border-primary/20 bg-primary/5 p-4">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<p className="text-sm font-medium text-foreground">Sending broadcast...</p>
</div>
<span className="text-xs font-medium text-primary">{progress}%</span>
</div>
<div className="h-1.5 w-full rounded-full bg-muted">
<div
className="h-1.5 rounded-full bg-primary transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-border pt-4">
<Button
variant="outline"
onClick={onBack}
disabled={isProcessing}
className="border-border text-muted-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<div className="flex items-center gap-2">
{onSaveDraft && (
<Button
variant="outline"
onClick={onSaveDraft}
disabled={!name.trim() || isProcessing}
className="border-border text-muted-foreground hover:bg-muted disabled:opacity-50"
>
<Save className="h-4 w-4" />
Save as Draft
</Button>
)}
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
<DialogTrigger
render={
<Button
disabled={!name.trim() || isProcessing}
className="bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
/>
}
>
<Send className="h-4 w-4" />
Send Broadcast
</DialogTrigger>
<DialogContent className="border-border bg-popover sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-popover-foreground">Confirm Broadcast</DialogTitle>
<DialogDescription className="text-muted-foreground">
You are about to send this broadcast to{' '}
<span className="font-medium text-popover-foreground">{estimatedReach.toLocaleString()}</span>{' '}
contacts using the{' '}
<span className="font-medium text-popover-foreground">{template.name}</span> template.
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowConfirm(false)}
className="border-border text-muted-foreground"
>
Cancel
</Button>
<Button
onClick={() => {
setShowConfirm(false);
onSend();
}}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
<Send className="h-4 w-4" />
Confirm & Send
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
);
}