178 lines
6.1 KiB
TypeScript
178 lines
6.1 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useEffect, useMemo, useState } from 'react'
|
||
|
|
import { trpc } from '@/lib/trpc/client'
|
||
|
|
import { toast } from 'sonner'
|
||
|
|
import {
|
||
|
|
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||
|
|
} from '@/components/ui/dialog'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Input } from '@/components/ui/input'
|
||
|
|
import { Label } from '@/components/ui/label'
|
||
|
|
import { Textarea } from '@/components/ui/textarea'
|
||
|
|
import { Mail, Send, Eye } from 'lucide-react'
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
open: boolean
|
||
|
|
onClose: () => void
|
||
|
|
projectId: string
|
||
|
|
projectTitle: string
|
||
|
|
}
|
||
|
|
|
||
|
|
function useDebounced<T>(value: T, delayMs: number): T {
|
||
|
|
const [debounced, setDebounced] = useState(value)
|
||
|
|
useEffect(() => {
|
||
|
|
const t = setTimeout(() => setDebounced(value), delayMs)
|
||
|
|
return () => clearTimeout(t)
|
||
|
|
}, [value, delayMs])
|
||
|
|
return debounced
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ProjectEmailDialog({ open, onClose, projectId, projectTitle }: Props) {
|
||
|
|
const initialBody = useMemo(() => `Hello ${projectTitle} team,\n\n`, [projectTitle])
|
||
|
|
const [subject, setSubject] = useState('')
|
||
|
|
const [body, setBody] = useState(initialBody)
|
||
|
|
const [showPreview, setShowPreview] = useState(false)
|
||
|
|
|
||
|
|
// Reset state whenever the dialog opens for a new project
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
setSubject('')
|
||
|
|
setBody(initialBody)
|
||
|
|
setShowPreview(false)
|
||
|
|
}
|
||
|
|
}, [open, initialBody])
|
||
|
|
|
||
|
|
const debouncedSubject = useDebounced(subject, 300)
|
||
|
|
const debouncedBody = useDebounced(body, 300)
|
||
|
|
|
||
|
|
const recipientPreview = trpc.message.previewRecipients.useQuery(
|
||
|
|
{ recipientType: 'PROJECT_TEAM', recipientFilter: { projectId } },
|
||
|
|
{ enabled: open }
|
||
|
|
)
|
||
|
|
|
||
|
|
const emailPreview = trpc.message.previewEmail.useQuery(
|
||
|
|
{ subject: debouncedSubject, body: debouncedBody },
|
||
|
|
{ enabled: showPreview && debouncedSubject.length > 0 && debouncedBody.length > 0 }
|
||
|
|
)
|
||
|
|
|
||
|
|
const sendTestMutation = trpc.message.sendTest.useMutation({
|
||
|
|
onSuccess: ({ to }) => toast.success(`Test email sent to ${to}`),
|
||
|
|
onError: (e) => toast.error(e.message),
|
||
|
|
})
|
||
|
|
|
||
|
|
const sendMutation = trpc.message.send.useMutation({
|
||
|
|
onSuccess: () => {
|
||
|
|
toast.success(`Email sent to ${recipientPreview.data?.totalApplicants ?? 0} team members`)
|
||
|
|
onClose()
|
||
|
|
},
|
||
|
|
onError: (e) => toast.error(e.message),
|
||
|
|
})
|
||
|
|
|
||
|
|
const recipientCount = recipientPreview.data?.totalApplicants ?? 0
|
||
|
|
const canSend = subject.length > 0 && body.length > 0 && recipientCount > 0
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<Mail className="h-5 w-5" />
|
||
|
|
Email Team — {projectTitle}
|
||
|
|
</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Compose a custom email to all members of this project's team.
|
||
|
|
{recipientPreview.isLoading
|
||
|
|
? ' Loading recipients…'
|
||
|
|
: ` Will be sent to ${recipientCount} team member${recipientCount === 1 ? '' : 's'}.`}
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="email-subject">Subject</Label>
|
||
|
|
<Input
|
||
|
|
id="email-subject"
|
||
|
|
value={subject}
|
||
|
|
onChange={(e) => setSubject(e.target.value)}
|
||
|
|
placeholder="Subject of your email"
|
||
|
|
maxLength={500}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="email-body">Body</Label>
|
||
|
|
<Textarea
|
||
|
|
id="email-body"
|
||
|
|
value={body}
|
||
|
|
onChange={(e) => setBody(e.target.value)}
|
||
|
|
rows={10}
|
||
|
|
className="font-mono text-sm"
|
||
|
|
placeholder={initialBody}
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
The greeting is pre-filled — edit freely. The full email is wrapped in the standard
|
||
|
|
MOPC styled template when sent. Click "Show preview" to see exactly what recipients will see.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => setShowPreview((s) => !s)}
|
||
|
|
disabled={subject.length === 0 || body.length === 0}
|
||
|
|
>
|
||
|
|
<Eye className="mr-2 h-4 w-4" />
|
||
|
|
{showPreview ? 'Hide preview' : 'Show preview'}
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => sendTestMutation.mutate({ subject, body })}
|
||
|
|
disabled={!canSend || sendTestMutation.isPending}
|
||
|
|
>
|
||
|
|
<Send className="mr-2 h-4 w-4" />
|
||
|
|
{sendTestMutation.isPending ? 'Sending…' : 'Send test to me'}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{showPreview && emailPreview.data && (
|
||
|
|
<div className="border rounded-md overflow-hidden">
|
||
|
|
<div className="bg-muted text-xs px-3 py-2 border-b">Email preview</div>
|
||
|
|
<iframe
|
||
|
|
title="Email preview"
|
||
|
|
srcDoc={emailPreview.data.html}
|
||
|
|
className="w-full h-96 bg-white"
|
||
|
|
sandbox=""
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter>
|
||
|
|
<Button variant="ghost" onClick={onClose}>Cancel</Button>
|
||
|
|
<Button
|
||
|
|
onClick={() =>
|
||
|
|
sendMutation.mutate({
|
||
|
|
recipientType: 'PROJECT_TEAM',
|
||
|
|
recipientFilter: { projectId },
|
||
|
|
subject,
|
||
|
|
body,
|
||
|
|
deliveryChannels: ['EMAIL'],
|
||
|
|
linkType: 'NONE',
|
||
|
|
})
|
||
|
|
}
|
||
|
|
disabled={!canSend || sendMutation.isPending}
|
||
|
|
>
|
||
|
|
<Send className="mr-2 h-4 w-4" />
|
||
|
|
{sendMutation.isPending ? 'Sending…' : `Send to ${recipientCount}`}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
)
|
||
|
|
}
|