Files
Recordly/src/components/video-editor/AddCustomFontDialog.tsx
T

187 lines
4.8 KiB
TypeScript

import { Plus } from "@phosphor-icons/react";
import { useState } from "react";
import { toast } from "@/components/ui/toast";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
addCustomFont,
type CustomFont,
DuplicateFontError,
generateFontId,
isValidGoogleFontsUrl,
parseFontFamilyFromImport,
} from "@/lib/customFonts";
import { useScopedT } from "../../contexts/I18nContext";
interface AddCustomFontDialogProps {
onFontAdded?: (font: CustomFont) => void;
}
export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) {
const t = useScopedT("dialogs");
const [open, setOpen] = useState(false);
const [importUrl, setImportUrl] = useState("");
const [fontName, setFontName] = useState("");
const [loading, setLoading] = useState(false);
const resetForm = () => {
setImportUrl("");
setFontName("");
setLoading(false);
};
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen);
if (!nextOpen) {
resetForm();
}
};
const handleImportUrlChange = (url: string) => {
setImportUrl(url);
// Auto-extract font name if valid Google Fonts URL
if (isValidGoogleFontsUrl(url)) {
const extracted = parseFontFamilyFromImport(url);
if (extracted && !fontName) {
setFontName(extracted);
}
}
};
const handleAdd = async () => {
const normalizedImportUrl = importUrl.trim();
const normalizedFontName = fontName.trim();
// Validate inputs
if (!normalizedImportUrl) {
toast.error(t("addFont.enterUrl"));
return;
}
if (!isValidGoogleFontsUrl(normalizedImportUrl)) {
toast.error(t("addFont.invalidUrl"));
return;
}
if (!normalizedFontName) {
toast.error(t("addFont.enterName"));
return;
}
setLoading(true);
try {
// Extract font family from URL
const fontFamily = parseFontFamilyFromImport(normalizedImportUrl);
if (!fontFamily) {
toast.error(t("addFont.extractFailed"));
return;
}
// Create custom font object
const newFont: CustomFont = {
id: generateFontId(normalizedFontName),
name: normalizedFontName,
fontFamily: fontFamily,
importUrl: normalizedImportUrl,
};
// Add font (this will load and verify it) - throws if it fails
await addCustomFont(newFont);
// Notify parent
if (onFontAdded) {
onFontAdded(newFont);
}
toast.success(t("addFont.addSuccess", undefined, { name: normalizedFontName }));
// Reset and close
handleOpenChange(false);
} catch (error) {
console.error("Failed to add custom font:", error);
if (error instanceof DuplicateFontError) {
toast.error(t("addFont.addFailed"), {
description: t("addFont.alreadyAdded", "This font has already been added."),
});
return;
}
const errorMessage = error instanceof Error ? error.message : "Failed to load font";
toast.error(t("addFont.addFailed"), {
description: errorMessage.includes("timeout")
? t("addFont.loadTimeout")
: t("addFont.loadFailed"),
});
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="w-full h-9 text-xs">
<Plus className="w-3 h-3 mr-1" />
{t("addFont.title")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("addFont.heading")}</DialogTitle>
<DialogDescription className="text-muted-foreground">
{t("addFont.description")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 mt-4">
<div className="space-y-2">
<Label htmlFor="import-url" className="text-foreground">
{t("addFont.urlLabel")}
</Label>
<Input
id="import-url"
placeholder={t("addFont.urlPlaceholder")}
value={importUrl}
onChange={(e) => handleImportUrlChange(e.target.value)}
/>
<p className="text-xs text-muted-foreground">{t("addFont.urlHelp")}</p>
</div>
<div className="space-y-2">
<Label htmlFor="font-name" className="text-foreground">
{t("addFont.nameLabel")}
</Label>
<Input
id="font-name"
placeholder={t("addFont.namePlaceholder")}
value={fontName}
onChange={(e) => setFontName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">{t("addFont.nameHelp")}</p>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={() => handleOpenChange(false)}>
{t("addFont.cancel")}
</Button>
<Button onClick={handleAdd} disabled={loading}>
{loading ? t("addFont.adding") : t("addFont.addFont")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}