i18n complete: all 16 components translated (CZ/HE/RU/UA)
- Custom i18n provider with React Context + localStorage - Hebrew RTL support (dir=rtl on html) - All pages + components use t() calls - FullCalendar + dates locale-aware - Language selector in Settings wired to context Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import {
|
||||
getGoals,
|
||||
getGoal,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
|
||||
export default function GoalsPage() {
|
||||
const { token } = useAuth();
|
||||
const { t, locale } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [goals, setGoals] = useState<Goal[]>([]);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
@@ -36,6 +38,8 @@ export default function GoalsPage() {
|
||||
const [formDate, setFormDate] = useState("");
|
||||
const [formGroup, setFormGroup] = useState("");
|
||||
|
||||
const dateLocale = locale === "ua" ? "uk-UA" : locale === "cs" ? "cs-CZ" : locale === "he" ? "he-IL" : "ru-RU";
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
@@ -47,12 +51,12 @@ export default function GoalsPage() {
|
||||
setGoals(goalsRes.data || []);
|
||||
setGroups(groupsRes.data || []);
|
||||
} catch (err) {
|
||||
console.error("Chyba pri nacitani:", err);
|
||||
setError("Nepodarilo se nacist data");
|
||||
console.error("Load error:", err);
|
||||
setError(t("common.error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
}, [token, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
@@ -77,8 +81,8 @@ export default function GoalsPage() {
|
||||
setShowForm(false);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error("Chyba pri vytvareni:", err);
|
||||
setError("Nepodarilo se vytvorit cil");
|
||||
console.error("Create error:", err);
|
||||
setError(t("common.error"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +94,7 @@ export default function GoalsPage() {
|
||||
setPlanResult(null);
|
||||
setReport(null);
|
||||
} catch (err) {
|
||||
console.error("Chyba pri nacitani cile:", err);
|
||||
console.error("Load goal error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +105,12 @@ export default function GoalsPage() {
|
||||
try {
|
||||
const res = await generateGoalPlan(token, goalId);
|
||||
setPlanResult(res.data);
|
||||
// Reload goal to get updated plan
|
||||
const updated = await getGoal(token, goalId);
|
||||
setSelectedGoal(updated.data);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error("Chyba pri generovani planu:", err);
|
||||
setError("Nepodarilo se vygenerovat plan. Zkuste to znovu.");
|
||||
console.error("Plan error:", err);
|
||||
setError(t("common.error"));
|
||||
} finally {
|
||||
setAiLoading(null);
|
||||
}
|
||||
@@ -121,8 +124,8 @@ export default function GoalsPage() {
|
||||
const res = await getGoalReport(token, goalId);
|
||||
setReport(res.data);
|
||||
} catch (err) {
|
||||
console.error("Chyba pri ziskavani reportu:", err);
|
||||
setError("Nepodarilo se ziskat report. Zkuste to znovu.");
|
||||
console.error("Report error:", err);
|
||||
setError(t("common.error"));
|
||||
} finally {
|
||||
setAiLoading(null);
|
||||
}
|
||||
@@ -137,25 +140,25 @@ export default function GoalsPage() {
|
||||
setSelectedGoal({ ...selectedGoal, progress_pct: pct });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Chyba pri aktualizaci:", err);
|
||||
console.error("Update error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(goalId: string) {
|
||||
if (!token) return;
|
||||
if (!confirm("Opravdu chcete smazat tento cil?")) return;
|
||||
if (!confirm(t("tasks.confirmDelete"))) return;
|
||||
try {
|
||||
await deleteGoal(token, goalId);
|
||||
setSelectedGoal(null);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error("Chyba pri mazani:", err);
|
||||
console.error("Delete error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: string | null) {
|
||||
if (!d) return "Bez terminu";
|
||||
return new Date(d).toLocaleDateString("cs-CZ", { day: "numeric", month: "short", year: "numeric" });
|
||||
if (!d) return t("tasks.noDue");
|
||||
return new Date(d).toLocaleDateString(dateLocale, { day: "numeric", month: "short", year: "numeric" });
|
||||
}
|
||||
|
||||
function progressColor(pct: number) {
|
||||
@@ -171,12 +174,12 @@ export default function GoalsPage() {
|
||||
<div className="space-y-4 pb-24 sm:pb-8 px-4 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold dark:text-white">Cile</h1>
|
||||
<h1 className="text-xl font-bold dark:text-white">{t("goals.title")}</h1>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{showForm ? "Zrusit" : "+ Novy cil"}
|
||||
{showForm ? t("tasks.form.cancel") : `+ ${t("goals.add")}`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +187,7 @@ export default function GoalsPage() {
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-red-700 dark:text-red-300 text-sm">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="ml-2 underline">Zavrrit</button>
|
||||
<button onClick={() => setError(null)} className="ml-2 underline">{t("tasks.close")}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -192,19 +195,18 @@ export default function GoalsPage() {
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreate} className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nazev cile</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("tasks.form.title")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formTitle}
|
||||
onChange={(e) => setFormTitle(e.target.value)}
|
||||
placeholder="Napr. Naucit se TypeScript"
|
||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-h-[44px]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Termin</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("tasks.form.dueDate")}</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formDate}
|
||||
@@ -213,13 +215,13 @@ export default function GoalsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Skupina</label>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("tasks.form.group")}</label>
|
||||
<select
|
||||
value={formGroup}
|
||||
onChange={(e) => setFormGroup(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-2 focus:ring-blue-500 min-h-[44px]"
|
||||
>
|
||||
<option value="">-- Bez skupiny --</option>
|
||||
<option value="">{t("tasks.form.noGroup")}</option>
|
||||
{groups.map((g) => (
|
||||
<option key={g.id} value={g.id}>{g.icon ? g.icon + " " : ""}{g.name}</option>
|
||||
))}
|
||||
@@ -230,7 +232,7 @@ export default function GoalsPage() {
|
||||
type="submit"
|
||||
className="w-full py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
Vytvorit cil
|
||||
{t("goals.add")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
@@ -243,8 +245,7 @@ export default function GoalsPage() {
|
||||
) : goals.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="text-5xl mb-4 opacity-50">🎯</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">Zadne cile</p>
|
||||
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">Vytvorte svuj prvni cil</p>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">{t("goals.title")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -296,7 +297,7 @@ export default function GoalsPage() {
|
||||
<div>
|
||||
<h2 className="text-lg font-bold dark:text-white">{selectedGoal.title}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatDate(selectedGoal.target_date)} | Progres: {selectedGoal.progress_pct}%
|
||||
{formatDate(selectedGoal.target_date)} | {t("goals.progress")}: {selectedGoal.progress_pct}%
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -312,7 +313,7 @@ export default function GoalsPage() {
|
||||
{/* Progress slider */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Progres: {selectedGoal.progress_pct}%
|
||||
{t("goals.progress")}: {selectedGoal.progress_pct}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
@@ -334,14 +335,14 @@ export default function GoalsPage() {
|
||||
{aiLoading === "plan" ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
Generuji plan...
|
||||
{t("common.loading")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
Generovat plan (AI)
|
||||
{t("goals.plan")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -353,14 +354,14 @@ export default function GoalsPage() {
|
||||
{aiLoading === "report" ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
Generuji report...
|
||||
{t("common.loading")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Report (AI)
|
||||
{t("goals.report")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -370,22 +371,22 @@ export default function GoalsPage() {
|
||||
{planResult && (
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-3">
|
||||
<h3 className="font-semibold text-purple-800 dark:text-purple-300 mb-2">
|
||||
Vygenerovany plan ({planResult.tasks_created} ukolu vytvoreno)
|
||||
{t("goals.plan")} ({planResult.tasks_created})
|
||||
</h3>
|
||||
{planResult.plan.weeks ? (
|
||||
<div className="space-y-2">
|
||||
{(planResult.plan.weeks as Array<{ week_number: number; focus: string; tasks: Array<{ title: string; duration_hours?: number; day_of_week?: string }> }>).map((week, i) => (
|
||||
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg p-2">
|
||||
<p className="text-sm font-medium text-purple-700 dark:text-purple-300">
|
||||
Tyden {week.week_number}: {week.focus}
|
||||
#{week.week_number}: {week.focus}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-0.5">
|
||||
{(week.tasks || []).map((t, j) => (
|
||||
{(week.tasks || []).map((wt, j) => (
|
||||
<li key={j} className="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
||||
<span className="w-1 h-1 bg-purple-400 rounded-full flex-shrink-0" />
|
||||
{t.title}
|
||||
{t.duration_hours && <span className="text-gray-400 ml-1">({t.duration_hours}h)</span>}
|
||||
{t.day_of_week && <span className="text-gray-400 ml-1">[{t.day_of_week}]</span>}
|
||||
{wt.title}
|
||||
{wt.duration_hours && <span className="text-gray-400 ml-1">({wt.duration_hours}h)</span>}
|
||||
{wt.day_of_week && <span className="text-gray-400 ml-1">[{wt.day_of_week}]</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -404,9 +405,9 @@ export default function GoalsPage() {
|
||||
{report && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-semibold text-green-800 dark:text-green-300">AI Report</h3>
|
||||
<h3 className="font-semibold text-green-800 dark:text-green-300">{t("goals.report")}</h3>
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
{report.stats.done}/{report.stats.total} splneno ({report.stats.pct}%)
|
||||
{report.stats.done}/{report.stats.total} ({report.stats.pct}%)
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{report.report}</p>
|
||||
@@ -416,19 +417,19 @@ export default function GoalsPage() {
|
||||
{/* Existing plan */}
|
||||
{selectedGoal.plan && Object.keys(selectedGoal.plan).length > 0 && !planResult && (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3">
|
||||
<h3 className="font-semibold text-gray-700 dark:text-gray-300 mb-2">Ulozeny plan</h3>
|
||||
<h3 className="font-semibold text-gray-700 dark:text-gray-300 mb-2">{t("goals.plan")}</h3>
|
||||
{(selectedGoal.plan as Record<string, unknown>).weeks ? (
|
||||
<div className="space-y-2">
|
||||
{((selectedGoal.plan as Record<string, unknown>).weeks as Array<{ week_number: number; focus: string; tasks: Array<{ title: string; duration_hours?: number; day_of_week?: string }> }>).map((week, i) => (
|
||||
<div key={i} className="bg-white dark:bg-gray-900 rounded-lg p-2">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Tyden {week.week_number}: {week.focus}
|
||||
#{week.week_number}: {week.focus}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-0.5">
|
||||
{(week.tasks || []).map((t, j) => (
|
||||
{(week.tasks || []).map((wt, j) => (
|
||||
<li key={j} className="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
||||
<span className="w-1 h-1 bg-gray-400 rounded-full flex-shrink-0" />
|
||||
{t.title}
|
||||
{wt.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -447,7 +448,7 @@ export default function GoalsPage() {
|
||||
{selectedGoal.tasks && selectedGoal.tasks.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Souvisejici ukoly ({selectedGoal.tasks.length})
|
||||
{t("nav.tasks")} ({selectedGoal.tasks.length})
|
||||
</h3>
|
||||
<div className="space-y-1 max-h-48 overflow-y-auto">
|
||||
{(selectedGoal.tasks as Array<{ id: string; title: string; status: string }>).map((task) => (
|
||||
@@ -461,7 +462,7 @@ export default function GoalsPage() {
|
||||
"bg-gray-400"
|
||||
}`} />
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">{task.title}</span>
|
||||
<span className="text-xs text-gray-400 ml-auto flex-shrink-0">{task.status}</span>
|
||||
<span className="text-xs text-gray-400 ml-auto flex-shrink-0">{t(`tasks.status.${task.status}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -473,7 +474,7 @@ export default function GoalsPage() {
|
||||
onClick={() => handleDelete(selectedGoal.id)}
|
||||
className="w-full py-2 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
Smazat cil
|
||||
{t("tasks.delete")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user