نوشتن ایمیل
پیشنمایش تمامصفحهایمیلِ تازه با کمکِ هوش مصنوعی: موضوع، متن با درجِ متغیر در محلِ مکاننما، نوارِ قالببندی، گیرندهها با افزودن و حذف، نامِ قالب و کارتِ تولیدِ متن.
۱. نصب کامپوننتها
این بلاک از ۱۴ آیتم دیگ استفاده میکند: avatarbuttoncardchipcontrol-sizeiconsinputlabelpersianrich-text-editorselecttext-fieldtextareatoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/rich-text-editor.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/rich-text-editor.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/rich-text-editor.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/rich-text-editor.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
email-compose.tsx
"use client"
import * as React from "react"
import { AvatarGenderIcon, CopyIcon, PaperclipIcon, RefreshIcon, SparklesIcon, TrashIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Chip, ChipLabel } from "@/components/ui/chip"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RichTextEditor, textToHTML, type RichTextEditorHandle } from "@/components/ui/rich-text-editor"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
import { isValidEmail } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
type Recipient = { name: string; email: string; gender: "male" | "female"; photo?: string }
const VARIABLES = ["{{نام}}", "{{شرکت}}", "{{نام فرستنده}}"]
const INITIAL_BODY = `سلام {{نام}}،
ممنون که این مدت با {{شرکت}} همراه بودید. دستهبندی خودکارِ هزینهها حالا برای همهٔ حسابها فعال است و گزارشِ ماهانه بدونِ اکسل آماده میشود.
اگر بخواهید، یک جلسهٔ ۱۵ دقیقهای میگذاریم تا با هم راهاندازیاش کنیم.
با احترام،
{{نام فرستنده}}`
const GENERATED = `سلام {{نام}}،
میدانم جمعکردنِ فاکتورهای پراکنده آخرِ هر ماه چقدر وقت میگیرد. برای همین گزارشِ خودکارِ ماهانه را ساختیم: هر تراکنش خودش در دستهٔ درست مینشیند و گزارش آمادهٔ ارسال است.
اگر موافق باشید، همین هفته یک دمو ۱۵ دقیقهای برای {{شرکت}} میگذاریم.
با احترام،
{{نام فرستنده}}`
/**
* نوشتنِ ایمیل با کمکِ هوش مصنوعی: موضوع و متن در ستونِ اصلی با ویرایشگرِ قالبدار
* و متغیرهای شخصیسازی، و در ستونِ کناری گیرندهها، نامِ قالب و تولیدِ متن.
* «ارسال» تا گیرندهای نباشد غیرفعال است.
*/
export default function EmailComposeBlock() {
const [recipients, setRecipients] = React.useState<Recipient[]>([
{ name: "مهسا کریمی", email: "mahsa@example.com", gender: "female", photo: stockImages.personWoman1.src },
{ name: "آرش نوری", email: "arash@example.com", gender: "male", photo: stockImages.personMan2.src },
{ name: "لیلا احمدی", email: "leila@example.com", gender: "female" },
])
const [draft, setDraft] = React.useState("")
const [draftError, setDraftError] = React.useState("")
const [prompt, setPrompt] = React.useState("یک ایمیلِ دوستانه برای {{نام}} بنویس؛ دردِ فاکتورهای پراکنده را بگو، گزارشِ خودکار را راهحل معرفی کن و با دعوت به دمو تمام کن.")
const [generating, setGenerating] = React.useState(false)
const editorRef = React.useRef<RichTextEditorHandle>(null)
const addRecipient = (event: React.FormEvent) => {
event.preventDefault()
const email = draft.trim()
if (!isValidEmail(email)) {
setDraftError("یک ایمیلِ درست وارد کنید.")
return
}
setRecipients((list) => [...list, { name: email.split("@")[0], email, gender: "female" }])
setDraft("")
setDraftError("")
}
/** متغیر همانجا که مکاننما است درج میشود، نه انتهای متن. */
const insertVariable = (token: string) => editorRef.current?.insertText(token)
const generate = async () => {
setGenerating(true)
await new Promise((resolve) => setTimeout(resolve, 1200))
setGenerating(false)
editorRef.current?.setHTML(textToHTML(GENERATED))
toast.success("متنِ تازه جایگزین شد.")
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
<header className="flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<h1 className="text-2xl font-bold">ایمیل تازه</h1>
<div className="flex flex-wrap gap-2">
<Button variant="ghost">انصراف</Button>
<Button variant="outline" onClick={() => toast.success("پیشنویس ذخیره شد.")}>
ذخیرهٔ پیشنویس
</Button>
<Button disabled={recipients.length === 0} onClick={() => toast.success(`برای ${recipients.length.toLocaleString("fa-IR")} نفر فرستاده شد.`)}>
ارسال
</Button>
</div>
</header>
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]">
{/* نامه: موضوع، متن، متغیرها و نوارِ قالببندی. */}
<Card>
<CardContent className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<Label id="email-subject-label">موضوع</Label>
<Select defaultValue="s1">
<SelectTrigger aria-labelledby="email-subject-label" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="s1">گزارشِ ماهانه، بدونِ اکسل · {"{{نام}}"}</SelectItem>
<SelectItem value="s2">{"{{نام}}"}، یک ساعت از وقتتان را پس بگیرید</SelectItem>
<SelectItem value="s3">دعوت به دموی ۱۵ دقیقهای</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label id="email-body-label">متن</Label>
<RichTextEditor
ref={editorRef}
aria-labelledby="email-body-label"
defaultValue={textToHTML(INITIAL_BODY)}
placeholder="متنِ ایمیل را بنویسید…"
minRows={10}
toolbarEnd={
<Button variant="ghost" size="icon" aria-label="پیوستِ فایل">
<PaperclipIcon />
</Button>
}
/>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">درجِ متغیر:</span>
{VARIABLES.map((token) => (
<Chip key={token} asChild variant="soft">
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => insertVariable(token)}>
<ChipLabel>{token}</ChipLabel>
</button>
</Chip>
))}
</div>
</CardContent>
</Card>
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base">گیرندهها</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<form onSubmit={addRecipient} noValidate className="flex flex-col gap-2">
<div className="flex gap-2">
<Input
dir="ltr"
type="email"
aria-label="ایمیلِ گیرنده"
placeholder="name@example.com"
value={draft}
onChange={(event) => setDraft(event.target.value)}
aria-invalid={draftError ? true : undefined}
aria-describedby={draftError ? "email-recipient-error" : undefined}
className="flex-1"
/>
<Button type="submit" variant="outline" disabled={!draft.trim()}>
افزودن
</Button>
</div>
{draftError && (
<p id="email-recipient-error" role="alert" className="text-xs text-destructive">
{draftError}
</p>
)}
</form>
<ul className="flex flex-col gap-1">
{recipients.map((person) => (
<li key={person.email} className="flex items-center gap-3 py-1.5">
<Avatar size="sm" alt={person.name} src={person.photo} icon={<AvatarGenderIcon gender={person.gender} />} />
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">{person.name}</span>
<span className="truncate text-xs text-muted-foreground">
<bdi dir="ltr">{person.email}</bdi>
</span>
</span>
<Button
variant="ghost"
size="icon"
aria-label={`حذفِ ${person.name}`}
onClick={() => setRecipients((list) => list.filter((item) => item.email !== person.email))}
>
<TrashIcon />
</Button>
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardContent>
<TextField label="نامِ قالب" name="templateName" defaultValue="معرفی گزارشِ خودکار" />
</CardContent>
</Card>
{/* تولیدِ متن: دستور، تلاشِ دوباره، کپی و تولید. */}
<Card className="border-[color-mix(in_oklab,var(--brand-secondary)_40%,var(--border))]">
<CardHeader className="flex items-center gap-2">
<SparklesIcon aria-hidden className="size-5 text-brand-secondary" />
<CardTitle className="text-base">نوشتن با هوش مصنوعی</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Textarea aria-label="دستور برای تولیدِ متن" rows={5} value={prompt} onChange={(event) => setPrompt(event.target.value)} className="leading-7" />
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" aria-label="پیشنهادِ دیگر" onClick={generate} disabled={generating}>
<RefreshIcon />
</Button>
<Button
variant="ghost"
size="icon"
aria-label="کپی دستور"
onClick={() => {
void navigator.clipboard?.writeText(prompt)
toast.success("دستور کپی شد.")
}}
>
<CopyIcon />
</Button>
<Button className="ms-auto" onClick={generate} loading={generating} disabled={!prompt.trim()}>
<SparklesIcon />
تولید متن
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
</ControlSizeProvider>
)
}