"use client"; import { useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { KeySchemaBuilder } from "@/components/KeySchemaBuilder"; import { encrypt, normalizeInput, validateSelections, type KeySelection, type Language, type KeyType, } from "@/lib/crypto"; import { getPlanLimits, type UserPlan } from "@/lib/plans"; import { trackEvent } from "@/lib/analytics"; // Initial: name + custom_question in random order function initSelections(): KeySelection[] { const base: KeySelection[] = [ { type: "name", value: "" }, { type: "custom_question", question: "", value: "" }, ]; return Math.random() > 0.5 ? base : [base[1], base[0]]; } // ─── Asset Entry ────────────────────────────────────────── export interface AssetEntry { id: string; category: string; content: string; file?: { name: string; type: string; size: number; data: string; // base64 }; } // ─── VaultPayload (sent to server — no values / plaintext) ─ export interface KeySchemaItemSafe { type: KeyType; question?: string; } export interface VaultPayload { ciphertext: string; salt: string; iv: string; keySchema: KeySchemaItemSafe[]; keyLanguage: Language; summary: { entryCount: number; categories: string[]; }; } // ─── Asset Editor ───────────────────────────────────────── interface AssetEditorProps { entries: AssetEntry[]; onChange: (entries: AssetEntry[]) => void; userPlan?: UserPlan; } function CategorySelect({ value, onChange, options, t }: { value: string; onChange: (val: string) => void; options: string[]; t: (key: string) => string; }) { const [open, setOpen] = useState(false); const isCustom = !options.includes(value) && value !== "__custom__"; const displayValue = isCustom ? value : (value === "__custom__" ? t("customCategory") : value); return (
{open && ( <>
setOpen(false)} />
{options.map((opt) => ( ))}
)}
); } function AssetEditor({ entries, onChange, userPlan = "free" }: AssetEditorProps) { const t = useTranslations("VaultForm"); const { fileUpload, maxFileSize } = getPlanLimits(userPlan); const PRESET_CATEGORIES = [ t("categories.privateKey"), t("categories.crypto"), t("categories.bank"), t("categories.hardware"), t("categories.passwords"), t("categories.property"), t("categories.stocks"), t("categories.other"), ]; function addEntry() { const id = Date.now().toString(); onChange([...entries, { id, category: PRESET_CATEGORIES[0], content: "" }]); } function removeEntry(id: string) { onChange(entries.filter((e) => e.id !== id)); } function moveUp(index: number) { if (index === 0) return; const next = [...entries]; [next[index - 1], next[index]] = [next[index], next[index - 1]]; onChange(next); } function moveDown(index: number) { if (index === entries.length - 1) return; const next = [...entries]; [next[index], next[index + 1]] = [next[index + 1], next[index]]; onChange(next); } function updateField(id: string, field: keyof AssetEntry, value: string) { onChange(entries.map((e) => (e.id === id ? { ...e, [field]: value } : e))); } function handleFileUpload(id: string, file: File) { // Check file size limit if (file.size > maxFileSize) { alert(t("fileTooLarge", { max: (maxFileSize / 1024 / 1024).toFixed(0) })); return; } const reader = new FileReader(); reader.onload = (e) => { const base64 = e.target?.result as string; const data = base64.split(',')[1]; // Remove data:mime;base64, prefix onChange(entries.map((entry) => entry.id === id ? { ...entry, file: { name: file.name, type: file.type, size: file.size, data, } } : entry )); }; reader.readAsDataURL(file); } function removeFile(id: string) { onChange(entries.map((entry) => entry.id === id ? { ...entry, file: undefined } : entry )); } function getPlaceholder(category: string): string { if (category.includes("Private Key") || category.includes("Mnemonic") || category.includes("私钥") || category.includes("助记词")) return t("placeholders.privateKey"); if (category.includes("Crypto") || category.includes("加密货币")) return t("placeholders.crypto"); if (category.includes("Bank") || category.includes("银行")) return t("placeholders.bank"); if (category.includes("Hardware") || category.includes("硬件")) return t("placeholders.hardware"); if (category.includes("Password") || category.includes("Login") || category.includes("账号")) return t("placeholders.passwords"); if (category.includes("Property") || category.includes("Safe") || category.includes("房产")) return t("placeholders.property"); if (category.includes("Stock") || category.includes("Fund") || category.includes("股票")) return t("placeholders.stocks"); return t("placeholders.default"); } return (
{entries.length === 0 && (
⌨️ {t("emptyHint")}
)} {entries.map((entry, i) => { const isCustom = !PRESET_CATEGORIES.includes(entry.category); return (
{/* Header / Controls */}
{String(i + 1).padStart(2, '0')} updateField(entry.id, "category", val === "__custom__" ? "" : val)} />
{/* Action buttons (Fade in on hover) */}
{/* Custom category input */} {isCustom && (
updateField(entry.id, "category", e.target.value)} className="w-full bg-transparent text-xs font-mono text-emerald-600 border-b border-dashed border-gray-200 pb-1 focus:outline-none focus:border-emerald-500/50 transition-colors" />
)} {/* Content Editor */}