Refactor for i18n, CMS integration, and project slugs; enhance admin & analytics
Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
@@ -50,6 +50,7 @@ interface Project {
|
||||
function EditorPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get("id");
|
||||
const initialLocale = searchParams.get("locale") || "en";
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
@@ -58,6 +59,8 @@ function EditorPageContent() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(!projectId);
|
||||
const [editLocale, setEditLocale] = useState(initialLocale);
|
||||
const [baseTexts, setBaseTexts] = useState<{ title: string; description: string } | null>(null);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [_isTyping, setIsTyping] = useState(false);
|
||||
const [history, setHistory] = useState<typeof formData[]>([]);
|
||||
@@ -90,6 +93,10 @@ function EditorPageContent() {
|
||||
);
|
||||
|
||||
if (foundProject) {
|
||||
setBaseTexts({
|
||||
title: foundProject.title || "",
|
||||
description: foundProject.description || "",
|
||||
});
|
||||
const initialData = {
|
||||
title: foundProject.title || "",
|
||||
description: foundProject.description || "",
|
||||
@@ -127,6 +134,30 @@ function EditorPageContent() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadTranslation = useCallback(async (id: string, locale: string) => {
|
||||
if (!id || !locale || locale === "en") return;
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${id}/translation?locale=${encodeURIComponent(locale)}`, {
|
||||
headers: {
|
||||
"x-admin-request": "true",
|
||||
"x-session-token": sessionStorage.getItem("admin_session_token") || "",
|
||||
},
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
const tr = data.translation as { title?: string; description?: string } | null;
|
||||
if (tr?.title && tr?.description) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
title: tr.title || prev.title,
|
||||
description: tr.description || prev.description,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// ignore translation load failures
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check authentication and load project
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
@@ -141,6 +172,7 @@ function EditorPageContent() {
|
||||
// Load project if editing
|
||||
if (projectId) {
|
||||
await loadProject(projectId);
|
||||
await loadTranslation(projectId, editLocale);
|
||||
} else {
|
||||
setIsCreating(true);
|
||||
// Initialize history for new project
|
||||
@@ -182,7 +214,7 @@ function EditorPageContent() {
|
||||
};
|
||||
|
||||
init();
|
||||
}, [projectId, loadProject]);
|
||||
}, [projectId, loadProject, loadTranslation, editLocale]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
@@ -205,9 +237,13 @@ function EditorPageContent() {
|
||||
const method = projectId ? "PUT" : "POST";
|
||||
|
||||
// Prepare data for saving - only include fields that exist in the database schema
|
||||
const saveTitle = editLocale === "en" ? formData.title.trim() : (baseTexts?.title || formData.title.trim());
|
||||
const saveDescription =
|
||||
editLocale === "en" ? formData.description.trim() : (baseTexts?.description || formData.description.trim());
|
||||
|
||||
const saveData = {
|
||||
title: formData.title.trim(),
|
||||
description: formData.description.trim(),
|
||||
title: saveTitle,
|
||||
description: saveDescription,
|
||||
content: formData.content.trim(),
|
||||
category: formData.category,
|
||||
tags: formData.tags,
|
||||
@@ -251,6 +287,27 @@ function EditorPageContent() {
|
||||
|
||||
// Show success toast (smaller, smoother)
|
||||
showSuccess("Saved", `"${savedProject.title}" saved`);
|
||||
|
||||
// Save translation if editing a non-default locale
|
||||
if (projectId && editLocale !== "en") {
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/translation`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-request": "true",
|
||||
"x-session-token": sessionStorage.getItem("admin_session_token") || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
locale: editLocale,
|
||||
title: formData.title.trim(),
|
||||
description: formData.description.trim(),
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// ignore translation save failures
|
||||
}
|
||||
}
|
||||
|
||||
// Update project ID if it was a new project
|
||||
if (!projectId && savedProject.id) {
|
||||
@@ -275,7 +332,7 @@ function EditorPageContent() {
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [projectId, formData, showSuccess, showError]);
|
||||
}, [projectId, formData, showSuccess, showError, editLocale, baseTexts]);
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
@@ -645,6 +702,34 @@ function EditorPageContent() {
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-300 mb-2">
|
||||
Language
|
||||
</label>
|
||||
<div className="custom-select">
|
||||
<select
|
||||
value={editLocale}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setEditLocale(next);
|
||||
if (projectId) {
|
||||
// Update URL for deep-linking and reload translation
|
||||
const newUrl = `/editor?id=${projectId}&locale=${encodeURIComponent(next)}`;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
loadTranslation(projectId, next);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="en">English (default)</option>
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
{editLocale !== "en" && (
|
||||
<p className="text-xs text-stone-400 mt-2">
|
||||
Title/description are saved as a translation. Other fields are global.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-300 mb-2">
|
||||
Category
|
||||
|
||||
Reference in New Issue
Block a user