بلاک‌ها · پنل مدیریت

ایمیلِ تازه با کمکِ هوش مصنوعی: موضوع، متن با درجِ متغیر در محلِ مکان‌نما، نوارِ قالب‌بندی، گیرنده‌ها با افزودن و حذف، نامِ قالب و کارتِ تولیدِ متن.

۱. نصب کامپوننت‌ها

این بلاک از ۱۴ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}