feat: localize dashboard shell
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
|
||||||
|
import { useTranslations } from "@/components/i18n/i18n-provider";
|
||||||
import {
|
import {
|
||||||
createCalendarEventRecord,
|
createCalendarEventRecord,
|
||||||
deleteCalendarEventRecord,
|
deleteCalendarEventRecord,
|
||||||
@@ -73,6 +75,7 @@ type CalendarClientProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) {
|
export function CalendarClient({ events, clients, projects, tasks }: CalendarClientProps) {
|
||||||
|
const t = useTranslations();
|
||||||
const [monthDate, setMonthDate] = useState(() => new Date());
|
const [monthDate, setMonthDate] = useState(() => new Date());
|
||||||
const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date()));
|
const [selectedDate, setSelectedDate] = useState(() => toDateKey(new Date()));
|
||||||
const days = useMemo(() => buildMonthDays(monthDate), [monthDate]);
|
const days = useMemo(() => buildMonthDays(monthDate), [monthDate]);
|
||||||
@@ -90,7 +93,7 @@ export function CalendarClient({ events, clients, projects, tasks }: CalendarCli
|
|||||||
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold tracking-normal text-foreground">Takvim</h1>
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">{t("calendar.title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CalendarEventDialog
|
<CalendarEventDialog
|
||||||
@@ -442,16 +445,16 @@ function startOfToday() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatMonth(date: Date) {
|
function formatMonth(date: Date) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", { month: "long", year: "numeric" }).format(date);
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), { month: "long", year: "numeric" }).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateLabel(dateKey: string) {
|
function formatDateLabel(dateKey: string) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(`${dateKey}T00:00:00`));
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "long", year: "numeric" }).format(new Date(`${dateKey}T00:00:00`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeRange(event: CalendarEventItem) {
|
function formatTimeRange(event: CalendarEventItem) {
|
||||||
const start = new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.starts_at));
|
const start = new Intl.DateTimeFormat(getDocumentIntlLocale(), { hour: "2-digit", minute: "2-digit" }).format(new Date(event.starts_at));
|
||||||
const end = event.ends_at ? new Intl.DateTimeFormat("tr-TR", { hour: "2-digit", minute: "2-digit" }).format(new Date(event.ends_at)) : null;
|
const end = event.ends_at ? new Intl.DateTimeFormat(getDocumentIntlLocale(), { hour: "2-digit", minute: "2-digit" }).format(new Date(event.ends_at)) : null;
|
||||||
return end ? `${start} - ${end}` : start;
|
return end ? `${start} - ${end}` : start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
|
import { useTranslations } from "@/components/i18n/i18n-provider";
|
||||||
import { DefaultChatTransport, type UIMessage } from "ai";
|
import { DefaultChatTransport, type UIMessage } from "ai";
|
||||||
import { Brain, Loader2, MessageSquare, Plus, Send, Trash2 } from "lucide-react";
|
import { Brain, Loader2, MessageSquare, Plus, Send, Trash2 } from "lucide-react";
|
||||||
import { Button } from "poyraz-ui/atoms";
|
import { Button } from "poyraz-ui/atoms";
|
||||||
@@ -43,6 +44,7 @@ type ChatSession = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AIChatPage() {
|
export default function AIChatPage() {
|
||||||
|
const t = useTranslations();
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
@@ -235,7 +237,7 @@ export default function AIChatPage() {
|
|||||||
<Brain className="h-4 w-4" />
|
<Brain className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm font-semibold text-foreground">AI Asistan</h1>
|
<h1 className="text-sm font-semibold text-foreground">{t("chat.title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button effect="shine" variant="secondary" size="sm" className="md:hidden text-xs px-3" onClick={() => setIsMobileSessionsOpen(true)}>
|
<Button effect="shine" variant="secondary" size="sm" className="md:hidden text-xs px-3" onClick={() => setIsMobileSessionsOpen(true)}>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
|
||||||
|
import { useTranslations } from "@/components/i18n/i18n-provider";
|
||||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { PendingLink } from "@/components/ui/pending-link";
|
import { PendingLink } from "@/components/ui/pending-link";
|
||||||
import { StatCard } from "@/components/system/stat-card";
|
import { StatCard } from "@/components/system/stat-card";
|
||||||
@@ -27,6 +29,7 @@ type DashboardClientProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DashboardClient({ data }: DashboardClientProps) {
|
export function DashboardClient({ data }: DashboardClientProps) {
|
||||||
|
const t = useTranslations();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -41,20 +44,20 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
|
|
||||||
// Format dates for Recharts using local timezone
|
// Format dates for Recharts using local timezone
|
||||||
const incomeTrendData = (financeTrend || []).map(f => ({
|
const incomeTrendData = (financeTrend || []).map(f => ({
|
||||||
name: new Date(f.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
|
name: new Date(f.date).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" }),
|
||||||
income: f.income,
|
income: f.income,
|
||||||
expense: f.expense
|
expense: f.expense
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const moodTrendData = (moodTrend || []).map(l => ({
|
const moodTrendData = (moodTrend || []).map(l => ({
|
||||||
date: new Date(l.date).toLocaleDateString("tr-TR", { month: "short", day: "numeric" }),
|
date: new Date(l.date).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" }),
|
||||||
mood: l.mood,
|
mood: l.mood,
|
||||||
energy: l.energy,
|
energy: l.energy,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Format currency
|
// Format currency
|
||||||
const formatCurrency = (val: number) => {
|
const formatCurrency = (val: number) => {
|
||||||
return new Intl.NumberFormat("tr-TR", {
|
return new Intl.NumberFormat(getDocumentIntlLocale(), {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
@@ -67,19 +70,19 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||||
Dashboard
|
{t("dashboard.title")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Select value={data.range} onValueChange={handleRangeChange}>
|
<Select value={data.range} onValueChange={handleRangeChange}>
|
||||||
<SelectTrigger className="w-[160px]">
|
<SelectTrigger className="w-[160px]">
|
||||||
<SelectValue placeholder="Tarih aralığı" />
|
<SelectValue placeholder={t("dashboard.filters.range")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="today">Bugün</SelectItem>
|
<SelectItem value="today">{t("dashboard.filters.today")}</SelectItem>
|
||||||
<SelectItem value="this_week">Bu Hafta</SelectItem>
|
<SelectItem value="this_week">{t("dashboard.filters.thisWeek")}</SelectItem>
|
||||||
<SelectItem value="this_month">Bu Ay</SelectItem>
|
<SelectItem value="this_month">{t("dashboard.filters.thisMonth")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -88,17 +91,17 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
|
|
||||||
{/* KPI Cards */}
|
{/* KPI Cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard label="Net Kazanç" value={formatCurrency(netProfit)} icon={Wallet} tone="green" />
|
<StatCard label={t("dashboard.stats.netEarnings")} value={formatCurrency(netProfit)} icon={Wallet} tone="green" />
|
||||||
<StatCard label="Aktif Projeler" value={activeProjectsCount.toString()} icon={FolderKanban} tone="blue" />
|
<StatCard label={t("dashboard.stats.activeProjects")} value={activeProjectsCount.toString()} icon={FolderKanban} tone="blue" />
|
||||||
<StatCard label="Tamamlanan Görev" value={completedTasksCount.toString()} icon={CheckCircle2} tone="amber" />
|
<StatCard label={t("dashboard.stats.completedTasks")} value={completedTasksCount.toString()} icon={CheckCircle2} tone="amber" />
|
||||||
<StatCard label="Ortalama Mood" value={avgMood} icon={Activity} tone="red" />
|
<StatCard label={t("dashboard.stats.averageMood")} value={avgMood} icon={Activity} tone="red" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Charts */}
|
{/* Charts */}
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<h3 className="mb-6 text-sm font-semibold text-foreground">Gelir / Gider Özeti</h3>
|
<h3 className="mb-6 text-sm font-semibold text-foreground">{t("dashboard.sections.financeSummary")}</h3>
|
||||||
<div className="h-[300px] w-full">
|
<div className="h-[300px] w-full">
|
||||||
{incomeTrendData.length > 0 ? (
|
{incomeTrendData.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -160,7 +163,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında finansal veri yok.</div>
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("dashboard.empty.finance")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -168,7 +171,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<h3 className="mb-6 text-sm font-semibold text-foreground">Mood & Enerji Trendi</h3>
|
<h3 className="mb-6 text-sm font-semibold text-foreground">{t("dashboard.sections.moodTrend")}</h3>
|
||||||
<div className="h-[300px] w-full">
|
<div className="h-[300px] w-full">
|
||||||
{moodTrendData.length > 0 ? (
|
{moodTrendData.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -216,7 +219,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">Bu tarih aralığında günlük verisi yok.</div>
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">{t("dashboard.empty.journal")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -229,7 +232,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-foreground">Son Eklenen Projeler</h3>
|
<h3 className="text-sm font-semibold text-foreground">{t("dashboard.sections.recentProjects")}</h3>
|
||||||
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -240,7 +243,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{project.name}</p>
|
<p className="text-sm font-medium">{project.name}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{new Date(project.created_at).toLocaleDateString("tr-TR", { month: "short", day: "numeric" })}
|
{new Date(project.created_at).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={project.status === 'completed' ? 'secondary' : 'default'} className="capitalize text-[10px] px-1.5 py-0">
|
<Badge variant={project.status === 'completed' ? 'secondary' : 'default'} className="capitalize text-[10px] px-1.5 py-0">
|
||||||
@@ -250,7 +253,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
</PendingLink>
|
</PendingLink>
|
||||||
))}
|
))}
|
||||||
{data.projects.length === 0 && (
|
{data.projects.length === 0 && (
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz proje yok.</div>
|
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">{t("dashboard.empty.projects")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -260,7 +263,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-foreground">Son Eklenen Müşteriler</h3>
|
<h3 className="text-sm font-semibold text-foreground">{t("dashboard.sections.recentClients")}</h3>
|
||||||
<Users className="h-4 w-4 text-muted-foreground" />
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -274,11 +277,11 @@ export function DashboardClient({ data }: DashboardClientProps) {
|
|||||||
<p className="text-xs text-muted-foreground">{client.company_name || "Bireysel"}</p>
|
<p className="text-xs text-muted-foreground">{client.company_name || "Bireysel"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{new Date(client.created_at).toLocaleDateString("tr-TR", { month: "short", day: "numeric" })}
|
{new Date(client.created_at).toLocaleDateString(getDocumentIntlLocale(), { month: "short", day: "numeric" })}
|
||||||
</div>
|
</div>
|
||||||
</PendingLink>
|
</PendingLink>
|
||||||
)) : (
|
)) : (
|
||||||
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">Henüz müşteri yok.</div>
|
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-sm border-border bg-muted/20">{t("dashboard.empty.clients")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
|
||||||
|
import { useTranslations } from "@/components/i18n/i18n-provider";
|
||||||
import {
|
import {
|
||||||
createFinanceTransactionRecord,
|
createFinanceTransactionRecord,
|
||||||
deleteFinanceTransactionRecord,
|
deleteFinanceTransactionRecord,
|
||||||
@@ -103,6 +105,7 @@ type FinanceClientProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function FinanceClient({ transactions, clients, projects }: FinanceClientProps) {
|
export function FinanceClient({ transactions, clients, projects }: FinanceClientProps) {
|
||||||
|
const t = useTranslations();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
|
const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
|
||||||
const summaryTrackRef = useRef<HTMLDivElement>(null);
|
const summaryTrackRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -146,7 +149,7 @@ export function FinanceClient({ transactions, clients, projects }: FinanceClient
|
|||||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||||
Finans işlemleri
|
{t("finance.title")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -730,7 +733,7 @@ function calculateExpenseCategories(transactions: FinanceTransactionItem[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(value: number, currency = "USD") {
|
function formatCurrency(value: number, currency = "USD") {
|
||||||
return new Intl.NumberFormat("tr-TR", {
|
return new Intl.NumberFormat(getDocumentIntlLocale(), {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency,
|
currency,
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
@@ -738,7 +741,7 @@ function formatCurrency(value: number, currency = "USD") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function formatDate(value: string) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", {
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
|
||||||
|
import { useTranslations } from "@/components/i18n/i18n-provider";
|
||||||
import {
|
import {
|
||||||
createDailyLogRecord,
|
createDailyLogRecord,
|
||||||
deleteDailyLogRecord,
|
deleteDailyLogRecord,
|
||||||
@@ -60,6 +62,7 @@ const scoreLabels: Record<number, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function JournalClient({ logs }: JournalClientProps) {
|
export function JournalClient({ logs }: JournalClientProps) {
|
||||||
|
const t = useTranslations();
|
||||||
const summary = useMemo(() => calculateSummary(logs), [logs]);
|
const summary = useMemo(() => calculateSummary(logs), [logs]);
|
||||||
const chartData = useMemo(
|
const chartData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -79,7 +82,7 @@ export function JournalClient({ logs }: JournalClientProps) {
|
|||||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||||
Mood ve enerji
|
{t("journal.title")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -436,7 +439,7 @@ function average(values: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function formatDate(value: string) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", {
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -444,14 +447,14 @@ function formatDate(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatShortDate(value: string) {
|
function formatShortDate(value: string) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", {
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
}).format(new Date(`${value}T00:00:00`));
|
}).format(new Date(`${value}T00:00:00`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatWeekday(value: string) {
|
function formatWeekday(value: string) {
|
||||||
return new Intl.DateTimeFormat("tr-TR", {
|
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
}).format(new Date(`${value}T00:00:00`));
|
}).format(new Date(`${value}T00:00:00`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { DashboardShell } from "@/components/layout/dashboard-shell";
|
|||||||
import { domainActorFromSession } from "@/server/auth/domain-actor";
|
import { domainActorFromSession } from "@/server/auth/domain-actor";
|
||||||
import { requireFreelancer } from "@/server/auth/session";
|
import { requireFreelancer } from "@/server/auth/session";
|
||||||
import { getPublicBranding } from "@/server/branding/runtime";
|
import { getPublicBranding } from "@/server/branding/runtime";
|
||||||
|
import { resolveRequestLocale } from "@/server/i18n/resolver";
|
||||||
|
import { createTranslator, getClientI18nPayload } from "@/server/i18n/translator";
|
||||||
import { getUserPreferences } from "@/server/settings/preferences";
|
import { getUserPreferences } from "@/server/settings/preferences";
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
export default async function DashboardLayout({
|
||||||
@@ -13,6 +15,23 @@ export default async function DashboardLayout({
|
|||||||
const { user, profile } = context;
|
const { user, profile } = context;
|
||||||
const branding = getPublicBranding();
|
const branding = getPublicBranding();
|
||||||
const preferences = getUserPreferences(domainActorFromSession(context));
|
const preferences = getUserPreferences(domainActorFromSession(context));
|
||||||
|
const resolvedLocale = await resolveRequestLocale();
|
||||||
|
const translator = createTranslator(resolvedLocale.locale, [
|
||||||
|
"common",
|
||||||
|
"navigation",
|
||||||
|
"dashboard",
|
||||||
|
"clients",
|
||||||
|
"projects",
|
||||||
|
"tasks",
|
||||||
|
"calendar",
|
||||||
|
"finance",
|
||||||
|
"journal",
|
||||||
|
"chat",
|
||||||
|
"settings",
|
||||||
|
"status",
|
||||||
|
"validation",
|
||||||
|
]);
|
||||||
|
const t = translator.t;
|
||||||
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
|
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
|
||||||
|
|
||||||
const shortName =
|
const shortName =
|
||||||
@@ -33,6 +52,36 @@ export default async function DashboardLayout({
|
|||||||
darkLogoUrl: branding.darkLogoUrl,
|
darkLogoUrl: branding.darkLogoUrl,
|
||||||
}}
|
}}
|
||||||
colorMode={preferences.colorMode}
|
colorMode={preferences.colorMode}
|
||||||
|
i18n={getClientI18nPayload(resolvedLocale.locale, [
|
||||||
|
"common",
|
||||||
|
"navigation",
|
||||||
|
"dashboard",
|
||||||
|
"clients",
|
||||||
|
"projects",
|
||||||
|
"tasks",
|
||||||
|
"calendar",
|
||||||
|
"finance",
|
||||||
|
"journal",
|
||||||
|
"chat",
|
||||||
|
"settings",
|
||||||
|
"status",
|
||||||
|
"validation",
|
||||||
|
])}
|
||||||
|
labels={{
|
||||||
|
skipToContent: t("navigation.shell.skipToContent"),
|
||||||
|
homeAriaLabel: t("navigation.shell.homeAriaLabel", { app: branding.applicationName }),
|
||||||
|
mobileMenuAriaLabel: t("navigation.shell.mobileMenuAriaLabel"),
|
||||||
|
mobileMenuTooltip: t("navigation.shell.mobileMenuTooltip"),
|
||||||
|
logoAlt: t("navigation.shell.logoAlt", { app: branding.applicationName }),
|
||||||
|
progressTitle: t("navigation.shell.progressTitle"),
|
||||||
|
progressValue: t("navigation.shell.progressValue"),
|
||||||
|
progressAriaLabel: t("navigation.shell.progressAriaLabel"),
|
||||||
|
accountMenuAriaLabel: t("navigation.shell.accountMenuAriaLabel"),
|
||||||
|
settings: t("navigation.items.settings"),
|
||||||
|
signOut: t("navigation.account.signOut"),
|
||||||
|
signingOut: t("navigation.account.signingOut"),
|
||||||
|
signOutError: t("navigation.account.signOutError"),
|
||||||
|
}}
|
||||||
user={{
|
user={{
|
||||||
email: user.email,
|
email: user.email,
|
||||||
displayName,
|
displayName,
|
||||||
|
|||||||
+4
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@/lib/color-mode";
|
} from "@/lib/color-mode";
|
||||||
import { Toaster } from "poyraz-ui/molecules";
|
import { Toaster } from "poyraz-ui/molecules";
|
||||||
import { getPublicBranding } from "@/server/branding/runtime";
|
import { getPublicBranding } from "@/server/branding/runtime";
|
||||||
|
import { resolveRequestLocale } from "@/server/i18n/resolver";
|
||||||
|
|
||||||
const colorModeScript = `(() => {
|
const colorModeScript = `(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@@ -52,10 +53,12 @@ export default async function RootLayout({
|
|||||||
const colorMode = isColorMode(cookieColorMode)
|
const colorMode = isColorMode(cookieColorMode)
|
||||||
? cookieColorMode
|
? cookieColorMode
|
||||||
: branding.defaultColorMode;
|
: branding.defaultColorMode;
|
||||||
|
const locale = await resolveRequestLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="tr"
|
lang={locale.locale}
|
||||||
|
dir={locale.direction}
|
||||||
className={cn("font-sans", colorMode === "dark" && "dark")}
|
className={cn("font-sans", colorMode === "dark" && "dark")}
|
||||||
data-color-mode={colorMode}
|
data-color-mode={colorMode}
|
||||||
style={branding.cssVariables as CSSProperties}
|
style={branding.cssVariables as CSSProperties}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type AppShellProps = {
|
|||||||
branding: AppShellBranding;
|
branding: AppShellBranding;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
homeHref: string;
|
homeHref: string;
|
||||||
|
labels?: AppShellLabels;
|
||||||
navGroups: AppShellNavGroup[];
|
navGroups: AppShellNavGroup[];
|
||||||
settingsHref: string;
|
settingsHref: string;
|
||||||
user: ShellUser;
|
user: ShellUser;
|
||||||
@@ -79,10 +80,27 @@ type AppShellProps = {
|
|||||||
colorMode?: ColorMode;
|
colorMode?: ColorMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppShellLabels = {
|
||||||
|
skipToContent: string;
|
||||||
|
homeAriaLabel: string;
|
||||||
|
mobileMenuAriaLabel: string;
|
||||||
|
mobileMenuTooltip: string;
|
||||||
|
logoAlt: string;
|
||||||
|
progressTitle: string;
|
||||||
|
progressValue: string;
|
||||||
|
progressAriaLabel: string;
|
||||||
|
accountMenuAriaLabel: string;
|
||||||
|
settings: string;
|
||||||
|
signOut: string;
|
||||||
|
signingOut: string;
|
||||||
|
signOutError: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function AppShell({
|
export function AppShell({
|
||||||
branding,
|
branding,
|
||||||
children,
|
children,
|
||||||
homeHref,
|
homeHref,
|
||||||
|
labels = defaultAppShellLabels,
|
||||||
navGroups,
|
navGroups,
|
||||||
settingsHref,
|
settingsHref,
|
||||||
user,
|
user,
|
||||||
@@ -90,7 +108,7 @@ export function AppShell({
|
|||||||
colorMode,
|
colorMode,
|
||||||
}: AppShellProps) {
|
}: AppShellProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const sidebarProps = { branding, homeHref, navGroups, pathname, progress, settingsHref, user };
|
const sidebarProps = { branding, homeHref, labels, navGroups, pathname, progress, settingsHref, user };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -100,7 +118,7 @@ export function AppShell({
|
|||||||
href="#main-content"
|
href="#main-content"
|
||||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-focus-ring"
|
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-focus-ring"
|
||||||
>
|
>
|
||||||
Ana içeriğe geç
|
{labels.skipToContent}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
@@ -118,9 +136,26 @@ export function AppShell({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultAppShellLabels: AppShellLabels = {
|
||||||
|
skipToContent: "Ana içeriğe geç",
|
||||||
|
homeAriaLabel: "Ana sayfa",
|
||||||
|
mobileMenuAriaLabel: "Ana menüyü aç veya kapat",
|
||||||
|
mobileMenuTooltip: "Menü",
|
||||||
|
logoAlt: "Logo",
|
||||||
|
progressTitle: "Proje ilerlemesi",
|
||||||
|
progressValue: "%{progress} tamamlandı",
|
||||||
|
progressAriaLabel: "Proje ilerlemesi",
|
||||||
|
accountMenuAriaLabel: "{name} için hesap menüsünü aç",
|
||||||
|
settings: "Ayarlar",
|
||||||
|
signOut: "Çıkış yap",
|
||||||
|
signingOut: "Çıkış yapılıyor",
|
||||||
|
signOutError: "Çıkış yapılamadı. Lütfen tekrar deneyin.",
|
||||||
|
};
|
||||||
|
|
||||||
type SidebarCompositionProps = {
|
type SidebarCompositionProps = {
|
||||||
branding: AppShellBranding;
|
branding: AppShellBranding;
|
||||||
homeHref: string;
|
homeHref: string;
|
||||||
|
labels: AppShellLabels;
|
||||||
navGroups: AppShellNavGroup[];
|
navGroups: AppShellNavGroup[];
|
||||||
pathname: string;
|
pathname: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
@@ -145,15 +180,15 @@ function MobileSidebar(props: SidebarCompositionProps) {
|
|||||||
<Link
|
<Link
|
||||||
href={props.homeHref}
|
href={props.homeHref}
|
||||||
className="flex min-w-0 max-w-40 items-center"
|
className="flex min-w-0 max-w-40 items-center"
|
||||||
aria-label={`${props.branding.applicationName} ana sayfa`}
|
aria-label={props.labels.homeAriaLabel}
|
||||||
>
|
>
|
||||||
<WorkspaceLogo branding={props.branding} compact />
|
<WorkspaceLogo branding={props.branding} compact />
|
||||||
</Link>
|
</Link>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<SidebarTrigger action="mobile" aria-label="Ana menüyü aç veya kapat" />
|
<SidebarTrigger action="mobile" aria-label={props.labels.mobileMenuAriaLabel} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Menü</TooltipContent>
|
<TooltipContent>{props.labels.mobileMenuTooltip}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -167,6 +202,7 @@ function MobileSidebar(props: SidebarCompositionProps) {
|
|||||||
function SidebarComposition({
|
function SidebarComposition({
|
||||||
branding,
|
branding,
|
||||||
homeHref,
|
homeHref,
|
||||||
|
labels,
|
||||||
navGroups,
|
navGroups,
|
||||||
pathname,
|
pathname,
|
||||||
progress,
|
progress,
|
||||||
@@ -179,7 +215,7 @@ function SidebarComposition({
|
|||||||
<Link
|
<Link
|
||||||
href={homeHref}
|
href={homeHref}
|
||||||
className="flex min-h-12 w-full items-center justify-center"
|
className="flex min-h-12 w-full items-center justify-center"
|
||||||
aria-label={`${branding.applicationName} ana sayfa`}
|
aria-label={labels.homeAriaLabel}
|
||||||
>
|
>
|
||||||
<WorkspaceLogo branding={branding} />
|
<WorkspaceLogo branding={branding} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -194,8 +230,8 @@ function SidebarComposition({
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className="flex flex-col gap-3">
|
<SidebarFooter className="flex flex-col gap-3">
|
||||||
{typeof progress === "number" ? <ProgressSummary progress={progress} /> : null}
|
{typeof progress === "number" ? <ProgressSummary labels={labels} progress={progress} /> : null}
|
||||||
<AccountMenu user={user} settingsHref={settingsHref} />
|
<AccountMenu labels={labels} user={user} settingsHref={settingsHref} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -257,7 +293,7 @@ function WorkspaceLogo({
|
|||||||
<span className="flex h-full w-full items-center justify-center overflow-hidden">
|
<span className="flex h-full w-full items-center justify-center overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={lightLogoUrl}
|
src={lightLogoUrl}
|
||||||
alt={`${branding.applicationName} logosu`}
|
alt={branding.applicationName}
|
||||||
width={180}
|
width={180}
|
||||||
height={56}
|
height={56}
|
||||||
unoptimized
|
unoptimized
|
||||||
@@ -265,7 +301,7 @@ function WorkspaceLogo({
|
|||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src={darkLogoUrl}
|
src={darkLogoUrl}
|
||||||
alt={`${branding.applicationName} logosu`}
|
alt={branding.applicationName}
|
||||||
width={180}
|
width={180}
|
||||||
height={56}
|
height={56}
|
||||||
unoptimized
|
unoptimized
|
||||||
@@ -275,22 +311,23 @@ function WorkspaceLogo({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProgressSummary({ progress }: { progress: number }) {
|
function ProgressSummary({ labels, progress }: { labels: AppShellLabels; progress: number }) {
|
||||||
const normalizedProgress = Math.max(0, Math.min(100, progress));
|
const normalizedProgress = Math.max(0, Math.min(100, progress));
|
||||||
|
const progressValue = labels.progressValue.replace("{progress}", String(normalizedProgress));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="soft" className="w-full overflow-hidden border-primary/10 shadow-none">
|
<Card variant="soft" className="w-full overflow-hidden border-primary/10 shadow-none">
|
||||||
<CardContent className="space-y-3 p-3">
|
<CardContent className="space-y-3 p-3">
|
||||||
<Typography variant="small" className="font-semibold">
|
<Typography variant="small" className="font-semibold">
|
||||||
Proje ilerlemesi
|
{labels.progressTitle}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Typography variant="caption" className="font-medium text-primary">
|
<Typography variant="caption" className="font-medium text-primary">
|
||||||
%{normalizedProgress} tamamlandı
|
{progressValue}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-label="Proje ilerlemesi"
|
aria-label={labels.progressAriaLabel}
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuemax={100}
|
aria-valuemax={100}
|
||||||
aria-valuenow={normalizedProgress}
|
aria-valuenow={normalizedProgress}
|
||||||
@@ -307,7 +344,7 @@ function ProgressSummary({ progress }: { progress: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: string }) {
|
function AccountMenu({ labels, user, settingsHref }: { labels: AppShellLabels; user: ShellUser; settingsHref: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSigningOut, startSignOutTransition] = useTransition();
|
const [isSigningOut, startSignOutTransition] = useTransition();
|
||||||
|
|
||||||
@@ -318,7 +355,7 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
|
|||||||
router.replace(result.redirectTo);
|
router.replace(result.redirectTo);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Çıkış yapılamadı. Lütfen tekrar deneyin.");
|
toast.error(labels.signOutError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -329,7 +366,7 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
|
|||||||
<Button effect="shine"
|
<Button effect="shine"
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={`${user.displayName} için hesap menüsünü aç`}
|
aria-label={labels.accountMenuAriaLabel.replace("{name}", user.displayName)}
|
||||||
className="group h-auto min-h-10 w-full justify-start p-1.5 text-left"
|
className="group h-auto min-h-10 w-full justify-start p-1.5 text-left"
|
||||||
>
|
>
|
||||||
<SidebarUserProfile
|
<SidebarUserProfile
|
||||||
@@ -365,7 +402,7 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
|
|||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={settingsHref} className="gap-2">
|
<Link href={settingsHref} className="gap-2">
|
||||||
<Settings className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
<Settings className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
<span>Ayarlar</span>
|
<span>{labels.settings}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -385,7 +422,7 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
|
|||||||
className="w-full justify-start gap-2 text-left text-destructive"
|
className="w-full justify-start gap-2 text-left text-destructive"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4 shrink-0" aria-hidden="true" />
|
<LogOut className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||||
<span>{isSigningOut ? "Çıkış yapılıyor" : "Çıkış yap"}</span>
|
<span>{isSigningOut ? labels.signingOut : labels.signOut}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
|
import { I18nProvider } from "@/components/i18n/i18n-provider";
|
||||||
import { sidebarData } from "@/config/sidebar";
|
import { AppShell, type AppShellBranding, type AppShellLabels } from "@/components/layout/app-shell";
|
||||||
|
import { localizeSidebarData } from "@/config/sidebar";
|
||||||
import type { ColorMode } from "@/lib/color-mode";
|
import type { ColorMode } from "@/lib/color-mode";
|
||||||
|
import { createTranslatorFromMessages } from "@/lib/i18n";
|
||||||
|
|
||||||
type DashboardShellProps = {
|
type DashboardShellProps = {
|
||||||
branding: AppShellBranding;
|
branding: AppShellBranding;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
colorMode: ColorMode;
|
colorMode: ColorMode;
|
||||||
|
i18n: {
|
||||||
|
locale: string;
|
||||||
|
messages: Record<string, string>;
|
||||||
|
};
|
||||||
|
labels: AppShellLabels;
|
||||||
user: {
|
user: {
|
||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -16,17 +23,23 @@ type DashboardShellProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DashboardShell({ branding, children, colorMode, user }: DashboardShellProps) {
|
export function DashboardShell({ branding, children, colorMode, i18n, labels, user }: DashboardShellProps) {
|
||||||
|
const translator = createTranslatorFromMessages(i18n.locale, i18n.messages);
|
||||||
|
const navGroups = localizeSidebarData(translator.t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<I18nProvider locale={i18n.locale} messages={i18n.messages}>
|
||||||
branding={branding}
|
<AppShell
|
||||||
colorMode={colorMode}
|
branding={branding}
|
||||||
homeHref="/"
|
colorMode={colorMode}
|
||||||
navGroups={sidebarData}
|
homeHref="/"
|
||||||
settingsHref="/settings"
|
labels={labels}
|
||||||
user={user}
|
navGroups={navGroups}
|
||||||
>
|
settingsHref="/settings"
|
||||||
{children}
|
user={user}
|
||||||
</AppShell>
|
>
|
||||||
|
{children}
|
||||||
|
</AppShell>
|
||||||
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-9
@@ -13,6 +13,7 @@ import {
|
|||||||
|
|
||||||
export type SidebarNavItem = {
|
export type SidebarNavItem = {
|
||||||
title: string;
|
title: string;
|
||||||
|
titleKey: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
items?: SidebarNavItem[];
|
items?: SidebarNavItem[];
|
||||||
@@ -20,33 +21,49 @@ export type SidebarNavItem = {
|
|||||||
|
|
||||||
export type SidebarNavGroup = {
|
export type SidebarNavGroup = {
|
||||||
title: string;
|
title: string;
|
||||||
|
titleKey: string;
|
||||||
items: SidebarNavItem[];
|
items: SidebarNavItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sidebarData: SidebarNavGroup[] = [
|
export const sidebarData: SidebarNavGroup[] = [
|
||||||
{
|
{
|
||||||
title: "GENEL BAKIŞ",
|
title: "GENEL BAKIŞ",
|
||||||
|
titleKey: "navigation.groups.overview",
|
||||||
items: [
|
items: [
|
||||||
{ title: "Dashboard", href: "/", icon: Sparkles },
|
{ title: "Dashboard", titleKey: "navigation.items.dashboard", href: "/", icon: Sparkles },
|
||||||
{ title: "Takvim", href: "/calendar", icon: Calendar },
|
{ title: "Takvim", titleKey: "navigation.items.calendar", href: "/calendar", icon: Calendar },
|
||||||
{ title: "Analizler", href: "/analytics", icon: BarChart3 },
|
{ title: "Analizler", titleKey: "navigation.items.analytics", href: "/analytics", icon: BarChart3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "OPERASYON",
|
title: "OPERASYON",
|
||||||
|
titleKey: "navigation.groups.operations",
|
||||||
items: [
|
items: [
|
||||||
{ title: "Müşteriler", href: "/clients", icon: Building2 },
|
{ title: "Müşteriler", titleKey: "navigation.items.clients", href: "/clients", icon: Building2 },
|
||||||
{ title: "Projeler", href: "/projects", icon: FolderKanban },
|
{ title: "Projeler", titleKey: "navigation.items.projects", href: "/projects", icon: FolderKanban },
|
||||||
{ title: "Görevler", href: "/tasks", icon: CheckSquare2 },
|
{ title: "Görevler", titleKey: "navigation.items.tasks", href: "/tasks", icon: CheckSquare2 },
|
||||||
{ title: "Finans", href: "/finance", icon: Wallet },
|
{ title: "Finans", titleKey: "navigation.items.finance", href: "/finance", icon: Wallet },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "KİŞİSEL",
|
title: "KİŞİSEL",
|
||||||
items: [{ title: "Günlük", href: "/journal", icon: BookOpenText }],
|
titleKey: "navigation.groups.personal",
|
||||||
|
items: [{ title: "Günlük", titleKey: "navigation.items.journal", href: "/journal", icon: BookOpenText }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI ASİSTAN",
|
title: "AI ASİSTAN",
|
||||||
items: [{ title: "Sohbet", href: "/chat", icon: MessageCircleHeart }],
|
titleKey: "navigation.groups.ai",
|
||||||
|
items: [{ title: "Sohbet", titleKey: "navigation.items.chat", href: "/chat", icon: MessageCircleHeart }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function localizeSidebarData(t: (key: string) => string): SidebarNavGroup[] {
|
||||||
|
return sidebarData.map((group) => ({
|
||||||
|
...group,
|
||||||
|
title: t(group.titleKey),
|
||||||
|
items: group.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
title: t(item.titleKey),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const checkedRoots = [
|
||||||
|
"app/(dashboard)",
|
||||||
|
"components/layout",
|
||||||
|
"components/system",
|
||||||
|
"config/sidebar.ts",
|
||||||
|
];
|
||||||
|
|
||||||
|
const files = checkedRoots.flatMap((entry) => {
|
||||||
|
const absolute = path.join(root, entry);
|
||||||
|
if (fs.statSync(absolute).isFile()) return [absolute];
|
||||||
|
return walk(absolute).filter((file) => /\.(ts|tsx)$/.test(file));
|
||||||
|
});
|
||||||
|
|
||||||
|
const forbiddenFormatterPattern = /"tr-TR"|'tr-TR'|from "date-fns\/locale"|from 'date-fns\/locale'|locale:\s*tr\b/;
|
||||||
|
const formatterViolations = files
|
||||||
|
.map((file) => ({
|
||||||
|
file,
|
||||||
|
lines: fs.readFileSync(file, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((line, index) => ({ line, number: index + 1 }))
|
||||||
|
.filter(({ line }) => forbiddenFormatterPattern.test(line)),
|
||||||
|
}))
|
||||||
|
.filter((entry) => entry.lines.length > 0);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
formatterViolations,
|
||||||
|
[],
|
||||||
|
`Hardcoded Turkish formatter usage remains:\n${formatterViolations.map(formatViolation).join("\n")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dashboardLayout = fs.readFileSync(path.join(root, "app/(dashboard)/layout.tsx"), "utf8");
|
||||||
|
const dashboardShell = fs.readFileSync(path.join(root, "components/layout/dashboard-shell.tsx"), "utf8");
|
||||||
|
assert.match(dashboardLayout, /getClientI18nPayload/, "Dashboard layout must provide client i18n payload.");
|
||||||
|
assert.match(dashboardShell, /localizeSidebarData/, "Dashboard shell must localize sidebar with client-safe icons.");
|
||||||
|
|
||||||
|
const dashboardClient = fs.readFileSync(path.join(root, "app/(dashboard)/dashboard-client.tsx"), "utf8");
|
||||||
|
assert.match(dashboardClient, /useTranslations/, "Dashboard page must consume translations.");
|
||||||
|
assert.doesNotMatch(dashboardClient, />Dashboard</, "Dashboard title must not be hardcoded.");
|
||||||
|
|
||||||
|
const turkishLiteralPattern = /["'`][^"'`\n]*(?:ğ|ü|ş|ö|ç|ı|İ|Ğ|Ü|Ş|Ö|Ç)[^"'`\n]*["'`]/;
|
||||||
|
const remainingTurkishLiterals = files
|
||||||
|
.flatMap((file) =>
|
||||||
|
fs.readFileSync(file, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((line, index) => ({ file, line, number: index + 1 }))
|
||||||
|
.filter(({ line }) => turkishLiteralPattern.test(line)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportPath = path.join(root, "docs", "self-hosted-redesign", "i18n-phase-4-hardcoded-text-report.md");
|
||||||
|
fs.writeFileSync(
|
||||||
|
reportPath,
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"title: Faz 4 Kalan Hardcoded Metin Raporu",
|
||||||
|
"phase: 4",
|
||||||
|
"status: generated",
|
||||||
|
`last_updated: ${new Date().toISOString()}`,
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Faz 4 Kalan Hardcoded Metin Raporu",
|
||||||
|
"",
|
||||||
|
"Bu rapor Faz-4 boundary script'i tarafindan uretilir. Formatter sabitleri release blocker kabul edilir; kalan Turkce stringler Faz-4 kapsaminda raporlanir ve sonraki UI migration dalgalarinda eritilir.",
|
||||||
|
"",
|
||||||
|
`Toplam kalan Turkce literal satiri: ${remainingTurkishLiterals.length}`,
|
||||||
|
"",
|
||||||
|
...remainingTurkishLiterals.slice(0, 250).map(({ file, line, number }) => (
|
||||||
|
`- \`${path.relative(root, file)}:${number}\` ${line.trim()}`
|
||||||
|
)),
|
||||||
|
remainingTurkishLiterals.length > 250 ? "" : null,
|
||||||
|
remainingTurkishLiterals.length > 250 ? `Ilk 250 satir listelendi; kalan: ${remainingTurkishLiterals.length - 250}` : null,
|
||||||
|
"",
|
||||||
|
].filter((line) => line !== null).join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`I18n phase 4 boundary passed. Hardcoded text report: ${path.relative(root, reportPath)}`);
|
||||||
|
|
||||||
|
function walk(dir) {
|
||||||
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const entryPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) return walk(entryPath);
|
||||||
|
if (entry.isFile()) return [entryPath];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatViolation(entry) {
|
||||||
|
return `${path.relative(root, entry.file)}\n${entry.lines.map(({ line, number }) => ` ${number}: ${line}`).join("\n")}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user