first commit
This commit is contained in:
165
components/mail/ComposeModal.tsx
Normal file
165
components/mail/ComposeModal.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import type { MailMessage } from "@/app/dashboard/mail/page";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
|
||||
interface AttachmentFile {
|
||||
file: File;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
replyTo: MailMessage | null;
|
||||
onClose: () => void;
|
||||
onSent: () => void;
|
||||
}) {
|
||||
const [to, setTo] = useState(replyTo ? replyTo.from[0]?.address ?? "" : "");
|
||||
const [cc, setCc] = useState("");
|
||||
const [subject, setSubject] = useState(replyTo ? `Re: ${replyTo.subject.replace(/^Re:\s*/i, "")}` : "");
|
||||
const [body, setBody] = useState(replyTo ? `\n\n---\n${replyTo.text?.slice(0, 500) ?? ""}` : "");
|
||||
const [attachments, setAttachments] = useState<AttachmentFile[]>([]);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addFiles = useCallback((files: FileList | null) => {
|
||||
if (!files) return;
|
||||
const newFiles = Array.from(files).map((f) => ({ file: f, name: f.name, size: f.size }));
|
||||
setAttachments((prev) => [...prev, ...newFiles]);
|
||||
}, []);
|
||||
|
||||
const removeAttachment = (index: number) => {
|
||||
setAttachments((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
addFiles(e.dataTransfer.files);
|
||||
}, [addFiles]);
|
||||
|
||||
const handleSend = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSending(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
if (attachments.length > 0) {
|
||||
// Use FormData for attachments
|
||||
const formData = new FormData();
|
||||
formData.append("to", to);
|
||||
if (cc) formData.append("cc", cc);
|
||||
formData.append("subject", subject);
|
||||
formData.append("text", body);
|
||||
formData.append("html", `<pre style="font-family:inherit;white-space:pre-wrap">${body.replace(/</g, "<")}</pre>`);
|
||||
attachments.forEach((att) => {
|
||||
formData.append("attachments", att.file, att.name);
|
||||
});
|
||||
|
||||
const res = await fetch("/api/mail/send", { method: "POST", body: formData });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Gönderilemedi");
|
||||
} else {
|
||||
// JSON for simple messages
|
||||
const res = await fetch("/api/mail/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
to, cc: cc || undefined, subject, text: body,
|
||||
html: `<pre style="font-family:inherit;white-space:pre-wrap">${body.replace(/</g, "<")}</pre>`,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Gönderilemedi");
|
||||
}
|
||||
onSent();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
setSending(false);
|
||||
};
|
||||
|
||||
const totalSize = attachments.reduce((sum, a) => sum + a.size, 0);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal" style={{ maxWidth: 620 }}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">{replyTo ? "Yanıtla" : "Yeni Mail"}</h2>
|
||||
<button className="modal-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<form onSubmit={handleSend}>
|
||||
<div className="modal-body form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">Alıcı</label>
|
||||
<input type="email" className="input" placeholder="alici@domain.com" value={to}
|
||||
onChange={(e) => setTo(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">CC (isteğe bağlı)</label>
|
||||
<input type="text" className="input" placeholder="cc@domain.com" value={cc}
|
||||
onChange={(e) => setCc(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Konu</label>
|
||||
<input type="text" className="input" value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Mesaj</label>
|
||||
<textarea className="input" rows={8} value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
style={{ resize: "vertical", fontFamily: "inherit" }} />
|
||||
</div>
|
||||
|
||||
{/* Attachment zone */}
|
||||
<div
|
||||
className={`compose-dropzone ${dragOver ? "active" : ""}`}
|
||||
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }}
|
||||
/>
|
||||
<span style={{ fontSize: 20 }}>📎</span>
|
||||
<span>Dosya sürükleyin veya tıklayın</span>
|
||||
</div>
|
||||
|
||||
{/* Attachment list */}
|
||||
{attachments.length > 0 && (
|
||||
<div className="compose-attachments">
|
||||
{attachments.map((att, i) => (
|
||||
<div key={i} className="compose-att-item">
|
||||
<span>📎 {att.name}</span>
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{formatBytes(att.size)}</span>
|
||||
<button type="button" className="att-remove" onClick={() => removeAttachment(i)}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4 }}>
|
||||
Toplam: {formatBytes(totalSize)} — {attachments.length} dosya
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>İptal</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={sending}>
|
||||
{sending ? <span className="spinner" /> : <SendIcon />} Gönder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SendIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" x2="11" y1="2" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>; }
|
||||
66
components/mail/FolderList.tsx
Normal file
66
components/mail/FolderList.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
import type { MailFolder } from "@/app/dashboard/mail/page";
|
||||
|
||||
const FOLDER_ICONS: Record<string, string> = {
|
||||
"\\Inbox": "📥",
|
||||
"\\Sent": "📤",
|
||||
"\\Drafts": "📝",
|
||||
"\\Trash": "🗑️",
|
||||
"\\Junk": "⚠️",
|
||||
"\\Archive": "📦",
|
||||
};
|
||||
|
||||
const FOLDER_LABELS: Record<string, string> = {
|
||||
INBOX: "Gelen Kutusu",
|
||||
Sent: "Gönderilenler",
|
||||
Drafts: "Taslaklar",
|
||||
Trash: "Çöp Kutusu",
|
||||
Junk: "Spam",
|
||||
Archive: "Arşiv",
|
||||
};
|
||||
|
||||
function getFolderIcon(folder: MailFolder): string {
|
||||
if (folder.specialUse && FOLDER_ICONS[folder.specialUse]) return FOLDER_ICONS[folder.specialUse];
|
||||
const lower = folder.path.toLowerCase();
|
||||
if (lower === "inbox") return "📥";
|
||||
if (lower.includes("sent")) return "📤";
|
||||
if (lower.includes("draft")) return "📝";
|
||||
if (lower.includes("trash")) return "🗑️";
|
||||
if (lower.includes("junk") || lower.includes("spam")) return "⚠️";
|
||||
if (lower.includes("archive")) return "📦";
|
||||
return "📁";
|
||||
}
|
||||
|
||||
function getFolderLabel(folder: MailFolder): string {
|
||||
return FOLDER_LABELS[folder.name] ?? FOLDER_LABELS[folder.path] ?? folder.name;
|
||||
}
|
||||
|
||||
export default function FolderList({ folders, active, onSelect }: {
|
||||
folders: MailFolder[];
|
||||
active: string;
|
||||
onSelect: (path: string) => void;
|
||||
}) {
|
||||
const sorted = [...folders].sort((a, b) => {
|
||||
if (a.path === "INBOX") return -1;
|
||||
if (b.path === "INBOX") return 1;
|
||||
if (a.specialUse && !b.specialUse) return -1;
|
||||
if (!a.specialUse && b.specialUse) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="folder-list">
|
||||
{sorted.map((f) => (
|
||||
<button
|
||||
key={f.path}
|
||||
className={`folder-item ${active === f.path ? "active" : ""}`}
|
||||
onClick={() => onSelect(f.path)}
|
||||
>
|
||||
<span className="folder-icon">{getFolderIcon(f)}</span>
|
||||
<span className="folder-name">{getFolderLabel(f)}</span>
|
||||
{f.unseen > 0 && <span className="folder-badge">{f.unseen}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
components/mail/MailLogin.tsx
Normal file
60
components/mail/MailLogin.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function MailLogin({ onSuccess }: { onSuccess: (email: string) => void }) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
const res = await fetch("/api/mail/auth", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setLoading(false);
|
||||
if (res.ok) {
|
||||
onSuccess(email);
|
||||
} else {
|
||||
setError(data.error || "Bağlantı başarısız");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", minHeight: "60vh" }}>
|
||||
<div className="card" style={{ maxWidth: 420, width: "100%" }}>
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8 }}>📧</div>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: "var(--text-primary)" }}>Mail Hesabına Bağlan</h2>
|
||||
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginTop: 4 }}>
|
||||
Mailcow mail hesabınızın bilgilerini girin
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">E-posta Adresi</label>
|
||||
<input type="email" className="input" placeholder="info@domain.com" value={email}
|
||||
onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Şifre</label>
|
||||
<input type="password" className="input" placeholder="Mail hesabı şifresi" value={password}
|
||||
onChange={(e) => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" style={{ width: "100%" }} disabled={loading}>
|
||||
{loading ? <span className="spinner" /> : "Bağlan"}
|
||||
</button>
|
||||
<p style={{ fontSize: 11, color: "var(--text-muted)", textAlign: "center", marginTop: 8 }}>
|
||||
Şifreniz sunucuda saklanmaz, sadece oturum süresince kullanılır.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
components/mail/MessageList.tsx
Normal file
65
components/mail/MessageList.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import type { MailEnvelope } from "@/app/dashboard/mail/page";
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = new Date();
|
||||
const d = new Date(dateStr);
|
||||
const diff = now.getTime() - d.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "şimdi";
|
||||
if (mins < 60) return `${mins}dk`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}sa`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 7) return `${days}g`;
|
||||
return d.toLocaleDateString("tr-TR", { day: "numeric", month: "short" });
|
||||
}
|
||||
|
||||
function senderName(msg: MailEnvelope): string {
|
||||
const f = msg.from[0];
|
||||
return f?.name || f?.address || "Bilinmeyen";
|
||||
}
|
||||
|
||||
export default function MessageList({ messages, loading, selectedUid, onSelect, onDelete }: {
|
||||
messages: MailEnvelope[];
|
||||
loading: boolean;
|
||||
selectedUid: number | null;
|
||||
onSelect: (uid: number) => void;
|
||||
onDelete: (uid: number) => void;
|
||||
}) {
|
||||
if (loading) {
|
||||
return <div className="empty-state" style={{ padding: 40 }}><span className="spinner" style={{ width: 20, height: 20 }} /></div>;
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="empty-state" style={{ padding: 40 }}>
|
||||
<div style={{ fontSize: 13, color: "var(--text-muted)" }}>Bu klasörde mail yok</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message-list-inner">
|
||||
{messages.map((m) => (
|
||||
<div
|
||||
key={m.uid}
|
||||
className={`message-row ${selectedUid === m.uid ? "selected" : ""} ${!m.seen ? "unread" : ""}`}
|
||||
onClick={() => onSelect(m.uid)}
|
||||
>
|
||||
<div className="message-avatar">
|
||||
{senderName(m)[0]?.toUpperCase() ?? "?"}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-top">
|
||||
<span className="message-sender">{senderName(m)}</span>
|
||||
<span className="message-time">{timeAgo(m.date)}</span>
|
||||
</div>
|
||||
<div className="message-subject">{m.subject}</div>
|
||||
</div>
|
||||
{m.hasAttachments && <span className="message-attach" title="Ek var">📎</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
components/mail/MessageView.tsx
Normal file
147
components/mail/MessageView.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
import type { MailMessage } from "@/app/dashboard/mail/page";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
|
||||
function getFileIcon(contentType: string, filename: string): string {
|
||||
if (contentType.startsWith("image/")) return "🖼️";
|
||||
if (contentType === "application/pdf") return "📄";
|
||||
if (contentType.includes("zip") || contentType.includes("rar") || contentType.includes("tar")) return "📦";
|
||||
if (contentType.includes("word") || filename.endsWith(".doc") || filename.endsWith(".docx")) return "📝";
|
||||
if (contentType.includes("sheet") || contentType.includes("excel") || filename.endsWith(".xls")) return "📊";
|
||||
if (contentType.includes("presentation") || filename.endsWith(".ppt")) return "📑";
|
||||
if (contentType.startsWith("video/")) return "🎬";
|
||||
if (contentType.startsWith("audio/")) return "🎵";
|
||||
return "📎";
|
||||
}
|
||||
|
||||
function canPreview(contentType: string): boolean {
|
||||
return contentType.startsWith("image/") || contentType === "application/pdf";
|
||||
}
|
||||
|
||||
export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
message: MailMessage;
|
||||
onReply: () => void;
|
||||
onDelete: () => void;
|
||||
folder: string;
|
||||
}) {
|
||||
const from = message.from[0];
|
||||
const date = new Date(message.date).toLocaleString("tr-TR", {
|
||||
day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
|
||||
const downloadUrl = (filename: string) =>
|
||||
`/api/mail/messages/${message.uid}/attachments?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(filename)}`;
|
||||
|
||||
const handleAttachment = (att: { filename: string; contentType: string }, preview: boolean) => {
|
||||
const url = downloadUrl(att.filename);
|
||||
if (preview && canPreview(att.contentType)) {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = att.filename;
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="message-view">
|
||||
{/* Header */}
|
||||
<div className="message-view-header">
|
||||
<h2 className="message-view-subject">{message.subject}</h2>
|
||||
<div className="message-view-actions">
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title="Yanıtla">
|
||||
<ReplyIcon /> Yanıtla
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title="Sil">
|
||||
<TrashIcon /> Sil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="message-view-meta">
|
||||
<div className="message-view-avatar">{(from?.name || from?.address)?.[0]?.toUpperCase() ?? "?"}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{from?.name || from?.address}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>
|
||||
{from?.address}
|
||||
{message.to.length > 0 && <> → {message.to.map((t) => t.address).join(", ")}</>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{date}</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{message.attachments.length > 0 && (
|
||||
<div className="message-attachments">
|
||||
<div style={{ width: "100%", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", marginBottom: 6 }}>
|
||||
📎 {message.attachments.length} ek
|
||||
</div>
|
||||
{message.attachments.map((att, i) => (
|
||||
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
|
||||
<span>{getFileIcon(att.contentType, att.filename)}</span>
|
||||
<span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{att.filename}
|
||||
</span>
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{formatBytes(att.size)}</span>
|
||||
<span className="attachment-actions">
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
|
||||
title="İndir"
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
{canPreview(att.contentType) && (
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, true); }}
|
||||
title="Önizle"
|
||||
>
|
||||
👁
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className="message-view-body">
|
||||
{message.html ? (
|
||||
<iframe
|
||||
srcDoc={sanitizeHtml(message.html)}
|
||||
sandbox="allow-same-origin"
|
||||
style={{ width: "100%", border: "none", minHeight: 400 }}
|
||||
onLoad={(e) => {
|
||||
const iframe = e.target as HTMLIFrameElement;
|
||||
if (iframe.contentDocument) {
|
||||
iframe.style.height = iframe.contentDocument.body.scrollHeight + 20 + "px";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre style={{ whiteSpace: "pre-wrap", fontFamily: "inherit", fontSize: 13, color: "var(--text-secondary)" }}>
|
||||
{message.text}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
const style = `<style>
|
||||
body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; color: #c9d1d9; background: transparent; margin: 0; padding: 8px; line-height: 1.6; }
|
||||
a { color: #58a6ff; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>`;
|
||||
let clean = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
||||
clean = clean.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, "");
|
||||
return `<!DOCTYPE html><html><head>${style}</head><body>${clean}</body></html>`;
|
||||
}
|
||||
|
||||
function ReplyIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>; }
|
||||
function TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }
|
||||
Reference in New Issue
Block a user