Refactor for i18n, CMS integration, and project slugs; enhance admin & analytics

Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
Cursor Agent
2026-01-12 14:36:10 +00:00
parent 0349c686fa
commit 12245eec8e
55 changed files with 4573 additions and 753 deletions

View File

@@ -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