diff --git a/app/portal/page.tsx b/app/portal/page.tsx index bc0650d..3ca51e0 100644 --- a/app/portal/page.tsx +++ b/app/portal/page.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, Badge } from "poyraz-ui/atoms"; import { FolderKanban, CheckCircle2, Clock, BarChart } from "lucide-react"; import Link from "next/link"; import { StatCard } from "@/components/system/stat-card"; +import { getPublicBranding } from "@/server/branding/runtime"; import { getSqliteConnection } from "@/server/db/client"; import { ContentTranslationService, getContentFallbackLocale } from "@/server/i18n/content"; import { formatDate } from "@/lib/i18n/format"; @@ -16,6 +17,12 @@ export default async function PortalDashboardPage() { const content = new ContentTranslationService(getSqliteConnection().db); const localization = content.getPublicLocalizationContext(); const fallbackLocale = getContentFallbackLocale(locale.locale, localization); + const branding = content.resolveEntity("branding", { id: "default", ...getPublicBranding() }, { + locale: locale.locale, + fallbackLocale, + defaultLocale: locale.defaultLocale, + translations: content.listEntityTranslations("branding", "default"), + }); const projectRows = service.listProjects(actor); const projectTranslations = content.listBatch("project", projectRows.map((project) => project.id)); const projects = projectRows.map((project) => content.resolveEntity("project", project, { @@ -29,19 +36,23 @@ export default async function PortalDashboardPage() { const avgProgress = projects.length ? Math.round(projects.reduce((sum, project) => sum + project.progress, 0) / projects.length) : 0; + const number = new Intl.NumberFormat(locale.locale); return (

{t("portal.dashboard.title")}

+ {branding.portalWelcomeText ? ( +

{branding.portalWelcomeText}

+ ) : null}
- - - + + +
@@ -62,6 +73,9 @@ export default async function PortalDashboardPage() {

{project.name}

+ {project.description ? ( +

{project.description}

+ ) : null}
{project.status === "completed" ? t("portal.status.project.completed") : project.status === "active" ? t("portal.status.project.active") : t("portal.status.project.waiting")} @@ -77,7 +91,7 @@ export default async function PortalDashboardPage() {
{t("portal.labels.progress")} - %{project.progress} + {t("portal.labels.percent", { value: number.format(project.progress) })}
@@ -89,6 +103,9 @@ export default async function PortalDashboardPage() { ))}
+ {branding.portalFooterText ? ( +

{branding.portalFooterText}

+ ) : null}
); } diff --git a/app/portal/projects/[id]/actions.ts b/app/portal/projects/[id]/actions.ts index 3b3a141..c5b619b 100644 --- a/app/portal/projects/[id]/actions.ts +++ b/app/portal/projects/[id]/actions.ts @@ -2,21 +2,28 @@ import { revalidatePath } from "next/cache"; import { cleanText } from "@/server/web/form-data"; +import { resolvePortalLocale } from "@/server/i18n/resolver"; import { requirePortalBackend } from "@/server/web/portal"; export async function createRevisionRequest(projectId: string, formData: FormData) { try { - const { actor, service } = await requirePortalBackend(); + const { actor, context, service } = await requirePortalBackend(); + const locale = await resolvePortalLocale(context); const description = cleanText(formData.get("description")); - if (!description) return { error: "Revizyon açıklaması boş olamaz." }; + if (!description) return { errorKey: "portal.revision.errors.descriptionRequired" }; - service.requestRevision(actor, { projectId, description }); + service.requestRevision(actor, { + projectId, + description, + sourceLocale: locale.locale, + }); revalidatePath(`/portal/projects/${projectId}`); revalidatePath("/portal/revisions"); return { success: true }; } catch (error) { + console.error("Portal revision request failed", error); return { - error: error instanceof Error ? error.message : "Revizyon talebi oluşturulamadı.", + errorKey: "portal.revision.error", }; } } diff --git a/app/portal/projects/[id]/page.tsx b/app/portal/projects/[id]/page.tsx index 89f6142..fe7e55e 100644 --- a/app/portal/projects/[id]/page.tsx +++ b/app/portal/projects/[id]/page.tsx @@ -83,6 +83,7 @@ export default async function PortalProjectPage({ params }: { params: Promise<{ id: revision.id, description: revision.description, status: revision.status, + source_locale: revision.sourceLocale, created_at: revision.createdAt.toISOString(), })), }; diff --git a/app/portal/projects/[id]/portal-project-client.tsx b/app/portal/projects/[id]/portal-project-client.tsx index 0b76a72..1638926 100644 --- a/app/portal/projects/[id]/portal-project-client.tsx +++ b/app/portal/projects/[id]/portal-project-client.tsx @@ -48,6 +48,7 @@ export type PortalTask = { export type PortalRevision = { id: string; description: string; + source_locale: string | null; status: "pending" | "in_progress" | "completed" | "rejected"; created_at: string; }; @@ -78,11 +79,11 @@ export function PortalProjectClient({ try { const response = await createRevisionRequest(project.id, formData); - if (response.error) throw new Error(response.error); + if (response.errorKey) throw new Error(response.errorKey); toast.success(t("portal.revision.success")); setOpenRevision(false); } catch (error: unknown) { - toast.error(error instanceof Error ? error.message : t("portal.revision.error")); + toast.error(error instanceof Error ? t(error.message) : t("portal.revision.error")); } finally { setIsSubmitting(false); } @@ -92,12 +93,16 @@ export function PortalProjectClient({ (revision) => revision.status === "pending" || revision.status === "in_progress", ).length; const hasRevisionQuota = project.can_request_revision; + const number = new Intl.NumberFormat(locale); return (

{project.name}

+ {project.description ? ( +

{project.description}

+ ) : null}
@@ -189,7 +194,7 @@ export function PortalProjectClient({

{t("portal.sections.progress")}

- %{project.progress} + {t("portal.labels.percent", { value: number.format(project.progress) })}
@@ -248,7 +253,7 @@ export function PortalProjectClient({

{section.title}

- {section.type} + {t(`portal.planTypes.${section.type}`)}

{section.content}

@@ -264,7 +269,7 @@ export function PortalProjectClient({
{t("portal.labels.remainingQuota")}: - {project.revision_quota !== null ? project.revision_quota : t("portal.labels.unlimited")} + {project.revision_quota !== null ? number.format(project.revision_quota) : t("portal.labels.unlimited")}
diff --git a/app/portal/projects/page.tsx b/app/portal/projects/page.tsx index 1fbe4e7..e877601 100644 --- a/app/portal/projects/page.tsx +++ b/app/portal/projects/page.tsx @@ -23,6 +23,7 @@ export default async function PortalProjectsPage() { defaultLocale: locale.defaultLocale, translations: projectTranslations.get(project.id) ?? [], })); + const number = new Intl.NumberFormat(locale.locale); return (
@@ -47,6 +48,9 @@ export default async function PortalProjectsPage() { {project.status === "completed" ? t("portal.status.project.completed") : project.status === "active" ? t("portal.status.project.active") : t("portal.status.project.waiting")}
+ {project.description ? ( +

{project.description}

+ ) : null} {project.dueDate && (
@@ -57,7 +61,7 @@ export default async function PortalProjectsPage() {
{t("portal.labels.progress")} - %{project.progress} + {t("portal.labels.percent", { value: number.format(project.progress) })}
diff --git a/app/portal/revisions/page.tsx b/app/portal/revisions/page.tsx index 0fddef1..0a84da2 100644 --- a/app/portal/revisions/page.tsx +++ b/app/portal/revisions/page.tsx @@ -65,7 +65,7 @@ export default async function PortalRevisionsPage() {
- {t("portal.labels.project")} → + {t("portal.labels.openProjectCta")}
diff --git a/app/portal/tasks/page.tsx b/app/portal/tasks/page.tsx index 6a710e5..910a9ad 100644 --- a/app/portal/tasks/page.tsx +++ b/app/portal/tasks/page.tsx @@ -64,11 +64,14 @@ export default async function PortalTasksPage() {
{projectNames.get(task.projectId!)}
+ {task.description ? ( +

{task.description}

+ ) : null}
- {date ? formatDate(date, locale.locale) : "-"} + {date ? formatDate(date, locale.locale) : t("portal.labels.noDate")}
{isDone ? : }
diff --git a/components/system/destructive-confirmation.tsx b/components/system/destructive-confirmation.tsx index 6d664dd..10d587f 100644 --- a/components/system/destructive-confirmation.tsx +++ b/components/system/destructive-confirmation.tsx @@ -12,7 +12,7 @@ import { } from "poyraz-ui/molecules"; type DestructiveConfirmationProps = { - cancelLabel?: string; + cancelLabel: string; confirmLabel: string; description: string; loading?: boolean; @@ -23,7 +23,7 @@ type DestructiveConfirmationProps = { }; export function DestructiveConfirmation({ - cancelLabel = "Vazgeç", + cancelLabel, confirmLabel, description, loading = false, diff --git a/components/system/feedback-state.tsx b/components/system/feedback-state.tsx index 274c34b..265b3c0 100644 --- a/components/system/feedback-state.tsx +++ b/components/system/feedback-state.tsx @@ -45,7 +45,7 @@ export function FeedbackState({ action, description, title, variant }: FeedbackS ); } -export function LoadingState({ label = "İçerik yükleniyor" }: { label?: string }) { +export function LoadingState({ label }: { label: string }) { return (
{label} @@ -60,10 +60,10 @@ export function LoadingState({ label = "İçerik yükleniyor" }: { label?: strin ); } -export function RetryAction({ onClick }: { onClick: () => void }) { +export function RetryAction({ label, onClick }: { label: string; onClick: () => void }) { return ( ); } diff --git a/components/system/status-badge.tsx b/components/system/status-badge.tsx index 5d8036f..f71da78 100644 --- a/components/system/status-badge.tsx +++ b/components/system/status-badge.tsx @@ -2,38 +2,39 @@ import { Badge } from "poyraz-ui/atoms"; import type { ComponentProps } from "react"; const statusPresentation = { - accepted: { label: "Kabul edildi", variant: "success" }, - active: { label: "Aktif", variant: "success" }, - archived: { label: "Arşivlendi", variant: "outline" }, - cancelled: { label: "İptal edildi", variant: "outline" }, - completed: { label: "Tamamlandı", variant: "success" }, - done: { label: "Tamamlandı", variant: "success" }, - draft: { label: "Taslak", variant: "secondary" }, - expired: { label: "Süresi doldu", variant: "destructive" }, - in_progress: { label: "Devam ediyor", variant: "info" }, - overdue: { label: "Gecikmiş", variant: "destructive" }, - paid: { label: "Ödendi", variant: "success" }, - paused: { label: "Duraklatıldı", variant: "warning" }, - pending: { label: "Bekliyor", variant: "warning" }, - planned: { label: "Planlandı", variant: "secondary" }, - planning: { label: "Planlanıyor", variant: "secondary" }, - rejected: { label: "Reddedildi", variant: "destructive" }, - revoked: { label: "İptal edildi", variant: "destructive" }, - sent: { label: "Gönderildi", variant: "info" }, - todo: { label: "Yapılacak", variant: "secondary" }, -} as const satisfies Record["variant"]> }>; + accepted: { labelKey: "status.common.accepted", variant: "success" }, + active: { labelKey: "status.common.active", variant: "success" }, + archived: { labelKey: "status.common.archived", variant: "outline" }, + cancelled: { labelKey: "status.common.cancelled", variant: "outline" }, + completed: { labelKey: "status.common.completed", variant: "success" }, + done: { labelKey: "status.common.done", variant: "success" }, + draft: { labelKey: "status.common.draft", variant: "secondary" }, + expired: { labelKey: "status.common.expired", variant: "destructive" }, + in_progress: { labelKey: "status.common.inProgress", variant: "info" }, + overdue: { labelKey: "status.common.overdue", variant: "destructive" }, + paid: { labelKey: "status.common.paid", variant: "success" }, + paused: { labelKey: "status.common.paused", variant: "warning" }, + pending: { labelKey: "status.common.pending", variant: "warning" }, + planned: { labelKey: "status.common.planned", variant: "secondary" }, + planning: { labelKey: "status.common.planning", variant: "secondary" }, + rejected: { labelKey: "status.common.rejected", variant: "destructive" }, + revoked: { labelKey: "status.common.revoked", variant: "destructive" }, + sent: { labelKey: "status.common.sent", variant: "info" }, + todo: { labelKey: "status.common.todo", variant: "secondary" }, +} as const satisfies Record["variant"]> }>; export type NetaStatus = keyof typeof statusPresentation; type StatusBadgeProps = Omit, "children" | "variant"> & { + label: string; status: NetaStatus; }; -export function StatusBadge({ status, ...props }: StatusBadgeProps) { +export function StatusBadge({ label, status, ...props }: StatusBadgeProps) { const presentation = statusPresentation[status]; return ( - {presentation.label} + {label} ); } diff --git a/locales/en/index.ts b/locales/en/index.ts index 98c70e3..09dc5d5 100644 --- a/locales/en/index.ts +++ b/locales/en/index.ts @@ -51,6 +51,11 @@ export const enCatalog = { "invite.expired": "This invitation has expired. Ask the freelancer for a new link.", "invite.accepted": "This invitation was already used. You can sign in with your account.", "invite.revoked": "This invitation was revoked. Ask the freelancer for a new link.", + "invite.unavailable": "This invitation is no longer available. Ask the freelancer for a new link.", + "invite.notFound": "The invitation link is invalid or could not be found.", + "invite.invalidInput": "The invitation details are invalid. Check your name and password.", + "invite.emailRegistered": "An account already exists with this email address.", + "invite.clientLinked": "A portal account already exists for this client.", "invite.success": "Your portal account has been created. You can sign in now.", "language": "Language", "marketing.headline": "Manage freelance work, clients and finance in one place.", @@ -859,9 +864,9 @@ export const enCatalog = { "portal.appearance.brandingNotice": "Logo, color and brand settings are managed by the freelancer; only your personal theme preference changes here.", "portal.appearance.autoSave": "Saved automatically on selection", "portal.profile.title": "Profile", - "portal.profile.description": "Profile editing will be completed in the next phase. This page prepares the route and authorization boundary now.", + "portal.profile.description": "Manage the name and profile photo displayed in your portal account.", "portal.security.title": "Security", - "portal.security.description": "Password and session security controls will be completed in the next phase. This page is isolated from owner-only security settings.", + "portal.security.description": "Change your portal account password and securely sign out other sessions.", "languages.title": "Languages", "languages.description": "Manage instance languages, the default language, completion and usage impact.", "languages.actions.add": "Add language", @@ -1117,8 +1122,12 @@ export const enCatalog = { "actions.newRequest": "Create new request", "labels.delivery": "Delivery", "labels.deadline": "Due", + "labels.noDate": "No date", + "labels.openProject": "Open project", + "labels.openProjectCta": "Open project →", "labels.progress": "Progress", "labels.project": "Project", + "labels.percent": "{value}%", "labels.remainingQuota": "Remaining quota", "labels.unlimited": "Unlimited", "tabs.overview": "Overview", @@ -1135,6 +1144,17 @@ export const enCatalog = { "revision.descriptionPlaceholder": "Could this section be blue? Also, let's update the copy...", "revision.success": "Your revision request has been sent.", "revision.error": "Could not create revision request.", + "revision.errors.descriptionRequired": "Revision description is required.", + "planTypes.assets": "Visual assets", + "planTypes.audience": "Target audience", + "planTypes.color_palette": "Color palette", + "planTypes.design_system": "Design system", + "planTypes.goal": "Goal", + "planTypes.notes": "Notes", + "planTypes.overview": "Overview", + "planTypes.problem": "Problem solved", + "planTypes.scope": "Scope", + "planTypes.typography": "Typography", "status.project.active": "Active", "status.project.completed": "Completed", "status.project.waiting": "Waiting", @@ -1147,6 +1167,25 @@ export const enCatalog = { "status.revision.rejected": "Rejected", }, status: { + "common.accepted": "Accepted", + "common.active": "Active", + "common.archived": "Archived", + "common.cancelled": "Cancelled", + "common.completed": "Completed", + "common.done": "Done", + "common.draft": "Draft", + "common.expired": "Expired", + "common.inProgress": "In progress", + "common.overdue": "Overdue", + "common.paid": "Paid", + "common.paused": "Paused", + "common.pending": "Pending", + "common.planned": "Planned", + "common.planning": "Planning", + "common.rejected": "Rejected", + "common.revoked": "Revoked", + "common.sent": "Sent", + "common.todo": "To do", "project.active": "Active", "project.completed": "Completed", "task.todo": "To do", diff --git a/locales/tr/index.ts b/locales/tr/index.ts index 8085dec..f7278e8 100644 --- a/locales/tr/index.ts +++ b/locales/tr/index.ts @@ -51,6 +51,11 @@ export const trCatalog = { "invite.expired": "Bu davetin süresi dolmuş. Freelancer'dan yeni bir bağlantı istemelisin.", "invite.accepted": "Bu davet daha önce kullanılmış. Hesabınla giriş yapabilirsin.", "invite.revoked": "Bu davet iptal edilmiş. Freelancer'dan yeni bir bağlantı istemelisin.", + "invite.unavailable": "Bu davet artık kullanılamıyor. Freelancer'dan yeni bir bağlantı istemelisin.", + "invite.notFound": "Davet bağlantısı geçersiz veya bulunamadı.", + "invite.invalidInput": "Davet bilgileri geçersiz. Lütfen adını ve şifreni kontrol et.", + "invite.emailRegistered": "Bu e-posta adresiyle kayıtlı bir hesap zaten var.", + "invite.clientLinked": "Bu müşteri için portal hesabı zaten mevcut.", "invite.success": "Portal hesabın oluşturuldu. Şimdi giriş yapabilirsin.", "language": "Dil", "marketing.headline": "Freelancer işlerini, müşterilerini ve finansını tek yerde yönet.", @@ -859,9 +864,9 @@ export const trCatalog = { "portal.appearance.brandingNotice": "Logo, renk ve marka ayarları freelancer tarafından yönetilir; burada yalnız kişisel tema tercihiniz değişir.", "portal.appearance.autoSave": "Seçince otomatik kaydedilir", "portal.profile.title": "Profil", - "portal.profile.description": "Profil düzenleme bir sonraki fazda tamamlanacak. Bu sayfa route ve yetki sınırını şimdiden hazırlar.", + "portal.profile.description": "Portal hesabınızda görünen adınızı ve profil fotoğrafınızı yönetin.", "portal.security.title": "Güvenlik", - "portal.security.description": "Şifre ve oturum güvenliği kontrolleri bir sonraki fazda tamamlanacak. Bu sayfa owner-only güvenlik ayarlarından izoledir.", + "portal.security.description": "Portal hesabınızın şifresini değiştirin ve diğer oturumları güvenli şekilde kapatın.", "languages.title": "Diller", "languages.description": "Instance dillerini, varsayılan dili, tamamlanma durumunu ve kullanım etkilerini yönetin.", "languages.actions.add": "Dil ekle", @@ -1117,8 +1122,12 @@ export const trCatalog = { "actions.newRequest": "Yeni Talep Oluştur", "labels.delivery": "Teslim", "labels.deadline": "Son Teslim", + "labels.noDate": "Tarih yok", + "labels.openProject": "Projeyi aç", + "labels.openProjectCta": "Projeyi aç →", "labels.progress": "İlerleme", "labels.project": "Proje", + "labels.percent": "%{value}", "labels.remainingQuota": "Kalan Hak", "labels.unlimited": "Sınırsız", "tabs.overview": "Genel Bakış", @@ -1135,6 +1144,17 @@ export const trCatalog = { "revision.descriptionPlaceholder": "Şu kısmın rengi mavi olabilir mi? Ayrıca metinleri güncelleyelim...", "revision.success": "Revizyon talebiniz başarıyla iletildi.", "revision.error": "Revizyon talebi oluşturulamadı.", + "revision.errors.descriptionRequired": "Revizyon açıklaması zorunludur.", + "planTypes.assets": "Görsel varlıklar", + "planTypes.audience": "Hedef kitle", + "planTypes.color_palette": "Renk paleti", + "planTypes.design_system": "Tasarım sistemi", + "planTypes.goal": "Amaç", + "planTypes.notes": "Notlar", + "planTypes.overview": "Genel bakış", + "planTypes.problem": "Çözdüğü problem", + "planTypes.scope": "Kapsam", + "planTypes.typography": "Tipografi", "status.project.active": "Aktif", "status.project.completed": "Tamamlandı", "status.project.waiting": "Beklemede", @@ -1147,6 +1167,25 @@ export const trCatalog = { "status.revision.rejected": "Reddedildi", }, status: { + "common.accepted": "Kabul edildi", + "common.active": "Aktif", + "common.archived": "Arşivlendi", + "common.cancelled": "İptal edildi", + "common.completed": "Tamamlandı", + "common.done": "Tamamlandı", + "common.draft": "Taslak", + "common.expired": "Süresi doldu", + "common.inProgress": "Devam ediyor", + "common.overdue": "Gecikmiş", + "common.paid": "Ödendi", + "common.paused": "Duraklatıldı", + "common.pending": "Bekliyor", + "common.planned": "Planlandı", + "common.planning": "Planlanıyor", + "common.rejected": "Reddedildi", + "common.revoked": "İptal edildi", + "common.sent": "Gönderildi", + "common.todo": "Yapılacak", "project.active": "Aktif", "project.completed": "Tamamlandı", "task.todo": "Yapılacak",