fix(ui): improve dashboard responsiveness and i18n error message

- Add responsive breakpoints (sm/lg) for flex and grid layouts
- Prevent text overflow with min-w-0 and break-words utilities
- Stack buttons and inputs vertically on small screens
- Replace hardcoded HTTP error with translated audit log message
This commit is contained in:
diegosouzapw
2026-02-26 16:49:03 -03:00
parent 952b0b22c7
commit c2a777580a
25 changed files with 240 additions and 139 deletions

View File

@@ -41,7 +41,7 @@ export default function AuditLogTab() {
params.set("offset", String(offset));
const res = await fetch(`/api/compliance/audit-log?${params.toString()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) throw new Error(t("failedFetchAuditLog"));
const data: AuditEntry[] = await res.json();
setHasMore(data.length > PAGE_SIZE);

View File

@@ -75,7 +75,7 @@ export default function NewProviderPage() {
const selectedProvider = (AI_PROVIDERS as any)[formData.provider];
return (
<div className="max-w-2xl mx-auto">
<div className="max-w-2xl mx-auto min-w-0">
{/* Header */}
<div className="mb-8">
<Link
@@ -85,7 +85,9 @@ export default function NewProviderPage() {
<span className="material-symbols-outlined text-lg">arrow_back</span>
{t("backToProviders")}
</Link>
<h1 className="text-3xl font-semibold tracking-tight">{t("addNewProvider")}</h1>
<h1 className="text-2xl sm:text-3xl font-semibold tracking-tight break-words">
{t("addNewProvider")}
</h1>
<p className="text-text-muted mt-2">{t("configureNewProvider")}</p>
</div>
@@ -126,13 +128,13 @@ export default function NewProviderPage() {
<label className="text-sm font-medium">
{t("authMethod")} <span className="text-red-500">*</span>
</label>
<div className="flex gap-3">
<div className="flex flex-col sm:flex-row gap-3">
{authMethodOptions.map((method) => (
<button
key={method.value}
type="button"
onClick={() => handleChange("authMethod", method.value)}
className={`flex-1 flex items-center justify-center gap-2 p-4 rounded-lg border transition-all ${
className={`flex-1 min-w-0 flex items-center justify-center gap-2 p-4 rounded-lg border transition-all ${
formData.authMethod === method.value
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/50"
@@ -141,7 +143,7 @@ export default function NewProviderPage() {
<span className="material-symbols-outlined">
{method.value === "api_key" ? "key" : "lock"}
</span>
<span className="font-medium">
<span className="font-medium text-center break-words">
{method.value === "api_key" ? t("apiKeyLabel") : t("oauth2Label")}
</span>
</button>
@@ -198,7 +200,7 @@ export default function NewProviderPage() {
)}
{/* Actions */}
<div className="flex gap-3 pt-4 border-t border-border">
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-border">
<Link href="/dashboard/providers" className="flex-1">
<Button type="button" variant="ghost" fullWidth>
{t("cancel")}

View File

@@ -88,7 +88,7 @@ export default function IPFilterSection() {
</div>
{/* Mode selector */}
<div className="grid grid-cols-4 gap-2 mb-5">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 mb-5">
{MODES.map((m) => (
<button
key={m.value}
@@ -119,7 +119,7 @@ export default function IPFilterSection() {
{config.enabled && (
<div className="flex flex-col gap-4">
{/* Add IP */}
<div className="flex gap-2 items-end">
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-end">
<div className="flex-1">
<Input
label={t("addIpAddress")}
@@ -215,12 +215,12 @@ export default function IPFilterSection() {
{config.tempBans.map((ban) => (
<div
key={ban.ip}
className="flex items-center justify-between px-3 py-2 rounded-lg
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg
bg-orange-500/5 border border-orange-500/20 text-sm"
>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-orange-400">{ban.ip}</span>
<span className="text-xs text-text-muted"> {ban.reason}</span>
<div className="flex min-w-0 items-center gap-2">
<span className="font-mono text-xs text-orange-400 shrink-0">{ban.ip}</span>
<span className="text-xs text-text-muted truncate"> {ban.reason}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted tabular-nums">

View File

@@ -4,13 +4,13 @@ import { useState, useEffect, useCallback, useMemo } from "react";
import { Card } from "@/shared/components";
import { useTranslations } from "next-intl";
const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"];
const FIELD_LABELS = {
input: "Input",
output: "Output",
cached: "Cached",
reasoning: "Reasoning",
cache_creation: "Cache Write",
const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"] as const;
const FIELD_LABEL_KEYS: Record<(typeof PRICING_FIELDS)[number], string> = {
input: "input",
output: "output",
cached: "cached",
reasoning: "reasoning",
cache_creation: "cacheCreation",
};
export default function PricingTab() {
@@ -121,7 +121,7 @@ export default function PricingTab() {
});
if (response.ok) {
setSaveStatus(`${providerAlias.toUpperCase()} saved`);
setSaveStatus(`${providerAlias.toUpperCase()} ${t("saved")}`);
setEditedProviders((prev) => {
const next = new Set(prev);
next.delete(providerAlias);
@@ -130,36 +130,41 @@ export default function PricingTab() {
setTimeout(() => setSaveStatus(""), 3000);
} else {
const err = await response.json();
setSaveStatus(`Error: ${err.error}`);
setSaveStatus(`${t("errorOccurred")}: ${err.error}`);
}
} catch (error) {
setSaveStatus(`Save failed: ${error.message}`);
setSaveStatus(`${t("saveFailed")}: ${error.message}`);
} finally {
setSaving(false);
}
},
[pricingData]
[pricingData, t]
);
const resetProvider = useCallback(async (providerAlias) => {
if (!confirm(t("resetPricingConfirm", { provider: providerAlias.toUpperCase() }))) return;
try {
const response = await fetch(`/api/pricing?provider=${providerAlias}`, { method: "DELETE" });
if (response.ok) {
const updated = await response.json();
setPricingData(updated);
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`);
setEditedProviders((prev) => {
const next = new Set(prev);
next.delete(providerAlias);
return next;
const resetProvider = useCallback(
async (providerAlias) => {
if (!confirm(t("resetPricingConfirm", { provider: providerAlias.toUpperCase() }))) return;
try {
const response = await fetch(`/api/pricing?provider=${providerAlias}`, {
method: "DELETE",
});
setTimeout(() => setSaveStatus(""), 3000);
if (response.ok) {
const updated = await response.json();
setPricingData(updated);
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} ${t("resetDefaults")}`);
setEditedProviders((prev) => {
const next = new Set(prev);
next.delete(providerAlias);
return next;
});
setTimeout(() => setSaveStatus(""), 3000);
}
} catch (error) {
setSaveStatus(`${t("resetFailed")}: ${error.message}`);
}
} catch (error) {
setSaveStatus(`❌ Reset failed: ${error.message}`);
}
}, []);
},
[t]
);
const selectProviderFilter = useCallback((alias) => {
setSelectedProvider((prev) => (prev === alias ? null : alias));
@@ -312,12 +317,13 @@ function ProviderSection({
saving,
}) {
const t = useTranslations("settings");
const tGlobal = useTranslations();
const pricedCount = Object.keys(pricingData).length;
const authBadge =
provider.authType === "oauth"
? "OAuth"
? tGlobal("providers.oauthLabel")
: provider.authType === "apikey"
? "API Key"
? tGlobal("providers.apiKeyLabel")
: provider.authType;
return (
@@ -355,7 +361,7 @@ function ProviderSection({
<div className="flex items-center gap-3">
{isEdited && <span className="text-yellow-500 text-xs font-medium">{t("unsaved")}</span>}
<span className="text-text-muted text-xs">
{pricedCount}/{provider.modelCount} priced
{pricedCount}/{provider.modelCount} {t("withPricing")}
</span>
<div className="w-16 h-1.5 bg-bg-subtle rounded-full overflow-hidden">
<div
@@ -378,7 +384,7 @@ function ProviderSection({
{/* Actions bar */}
<div className="flex items-center justify-between px-4 py-2 bg-bg-subtle/50">
<span className="text-xs text-text-muted">
{provider.modelCount} models {pricedCount} with pricing configured
{provider.modelCount} {t("models")} {pricedCount} {t("withPricing")}
</span>
<div className="flex items-center gap-2">
<button
@@ -411,7 +417,7 @@ function ProviderSection({
<th className="px-4 py-2 text-left font-semibold">{t("model")}</th>
{PRICING_FIELDS.map((field) => (
<th key={field} className="px-2 py-2 text-right font-semibold w-24">
{FIELD_LABELS[field]}
{t(FIELD_LABEL_KEYS[field])}
</th>
))}
</tr>
@@ -437,6 +443,7 @@ function ProviderSection({
// ── Model Row ────────────────────────────────────────────────────────────
function ModelRow({ model, pricing, onPricingChange }) {
const t = useTranslations("settings");
const hasPricing = pricing && Object.values(pricing).some((v: any) => v > 0);
return (
@@ -449,7 +456,7 @@ function ModelRow({ model, pricing, onPricingChange }) {
<span className="font-medium text-xs">{model.name}</span>
{model.custom && (
<span className="px-1 py-0.5 text-[8px] font-bold bg-blue-500/15 text-blue-400 border border-blue-500/20 rounded uppercase">
custom
{t("custom")}
</span>
)}
<span className="text-text-muted text-[10px] opacity-0 group-hover:opacity-100 transition-opacity">

View File

@@ -93,7 +93,7 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
<h2 className="text-lg font-bold">{t("providerProfiles")}</h2>
</div>
{editMode ? (
<div className="flex gap-2">
<div className="flex flex-wrap justify-end gap-2">
<Button size="sm" variant="secondary" onClick={() => setEditMode(false)}>
{tc("cancel")}
</Button>
@@ -217,7 +217,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
<h3 className="text-xs font-bold uppercase tracking-wider mb-3 text-text-muted">
{t("defaultSafetyNet")}
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ key: "requestsPerMinute", label: t("rpm") },
{ key: "minTimeBetweenRequests", label: t("minGap"), format: formatMs },

View File

@@ -99,7 +99,10 @@ export default function RoutingTab() {
<h3 className="text-lg font-semibold">{t("routingStrategy")}</h3>
</div>
<div className="grid grid-cols-3 gap-2 mb-4" style={{ gridAutoRows: "1fr" }}>
<div
className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2 mb-4"
style={{ gridAutoRows: "1fr" }}
>
{STRATEGIES.map((s) => (
<button
key={s.value}
@@ -172,18 +175,18 @@ export default function RoutingTab() {
{aliases.map((a, i) => (
<div
key={i}
className="flex items-center justify-between px-3 py-2 rounded-lg bg-surface/30 border border-border/20"
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg bg-surface/30 border border-border/20"
>
<div className="flex items-center gap-3 text-sm">
<span className="font-mono text-purple-400">{a.pattern}</span>
<div className="flex min-w-0 items-center gap-2 text-sm">
<span className="font-mono text-purple-400 break-all">{a.pattern}</span>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<span className="font-mono text-text-main">{a.target}</span>
<span className="font-mono text-text-main break-all">{a.target}</span>
</div>
<button
onClick={() => removeAlias(i)}
className="text-text-muted hover:text-red-400 transition-colors"
className="shrink-0 text-text-muted hover:text-red-400 transition-colors"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
@@ -192,7 +195,7 @@ export default function RoutingTab() {
</div>
)}
<div className="flex gap-2 items-end">
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-end">
<div className="flex-1">
<Input
label={t("pattern")}
@@ -209,7 +212,12 @@ export default function RoutingTab() {
onChange={(e) => setNewTarget(e.target.value)}
/>
</div>
<Button size="sm" variant="primary" onClick={addAlias} className="mb-[2px]">
<Button
size="sm"
variant="primary"
onClick={addAlias}
className="mb-[2px] sm:w-auto w-full"
>
{t("add")}
</Button>
</div>

View File

@@ -92,14 +92,12 @@ export default function SystemPromptTab() {
disabled={loading}
/>
<div className="absolute bottom-2 right-3 text-xs text-text-muted/60 tabular-nums">
{config.prompt.length} chars
{t("chars", { count: config.prompt.length })}
</div>
</div>
<p className="text-xs text-text-muted/70 flex items-center gap-1.5">
<span className="material-symbols-outlined text-[14px]">info</span>
{t("systemPromptHint")} Send{" "}
<code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a
request to bypass.
{t("systemPromptHint")}
</p>
</div>
)}

View File

@@ -259,7 +259,7 @@ export default function SystemStorageTab() {
</div>
{/* Storage info grid */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<div className="p-3 rounded-lg bg-bg border border-border">
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">
{t("databasePath")}
@@ -277,7 +277,7 @@ export default function SystemStorageTab() {
</div>
{/* Export / Import */}
<div className="flex items-center gap-2 mb-4">
<div className="flex flex-wrap items-center gap-2 mb-4">
<Button variant="outline" size="sm" onClick={handleExport} loading={exportLoading}>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
download

View File

@@ -167,7 +167,7 @@ export default function ThinkingBudgetTab() {
<div className="p-4 rounded-lg bg-surface/30 border border-border/30">
<p className="text-sm font-medium mb-3">{t("baseEffortLevel")}</p>
<p className="text-xs text-text-muted mb-3">{t("adaptiveHint")}</p>
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{EFFORTS.map((e) => (
<button
key={e.value}

View File

@@ -34,34 +34,36 @@ export default function SettingsPage() {
const activeTab = userSelectedTab || tabs.find((t) => t.id === tabParam)?.id || "general";
return (
<div className="max-w-2xl mx-auto">
<div className="max-w-2xl mx-auto min-w-0">
<div className="flex flex-col gap-6">
{/* Tab navigation */}
<div
role="tablist"
aria-label={t("settingsSectionsAria")}
className="inline-flex items-center p-1 rounded-lg bg-black/5 dark:bg-white/5 self-start"
>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={activeTab === tab.id}
tabIndex={activeTab === tab.id ? 0 : -1}
onClick={() => setUserSelectedTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all text-sm",
activeTab === tab.id
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
{tab.icon}
</span>
<span className="hidden sm:inline">{t(tab.labelKey)}</span>
</button>
))}
<div className="w-full overflow-x-auto pb-1">
<div
role="tablist"
aria-label={t("settingsSectionsAria")}
className="inline-flex items-center p-1 rounded-lg bg-black/5 dark:bg-white/5 min-w-max"
>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={activeTab === tab.id}
tabIndex={activeTab === tab.id ? 0 : -1}
onClick={() => setUserSelectedTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all text-sm",
activeTab === tab.id
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
{tab.icon}
</span>
<span className="hidden sm:inline whitespace-nowrap">{t(tab.labelKey)}</span>
</button>
))}
</div>
</div>
{/* Tab contents */}

View File

@@ -26,9 +26,9 @@ export default function TranslatorPageClient() {
};
return (
<div className="p-8 space-y-6">
<div className="p-4 sm:p-8 space-y-6 min-w-0">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 min-w-0">
<div>
<h1 className="text-2xl font-bold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[28px]">translate</span>
@@ -38,7 +38,15 @@ export default function TranslatorPageClient() {
{modeDescriptions[mode] || t("modeDescriptionFallback")}
</p>
</div>
<SegmentedControl options={modes} value={mode} onChange={setMode} size="md" />
<div className="w-full sm:w-auto overflow-x-auto">
<SegmentedControl
options={modes}
value={mode}
onChange={setMode}
size="md"
className="min-w-max"
/>
</div>
</div>
{/* Mode Content */}

View File

@@ -248,7 +248,7 @@ export default function ChatTesterMode() {
};
return (
<div className="space-y-4">
<div className="space-y-4 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
@@ -264,7 +264,7 @@ export default function ChatTesterMode() {
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 min-w-0">
{/* Left: Chat Interface */}
<div className="space-y-4">
{/* Controls */}
@@ -349,7 +349,7 @@ export default function ChatTesterMode() {
? t("youWithFormat", { format: FORMAT_META[clientFormat]?.label })
: t("assistant")}
</p>
<p className="whitespace-pre-wrap">{msg.content}</p>
<p className="whitespace-pre-wrap break-words">{msg.content}</p>
</div>
</div>
))}

View File

@@ -54,7 +54,7 @@ export default function LiveMonitorMode() {
: 0;
return (
<div className="space-y-5">
<div className="space-y-5 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span
@@ -212,7 +212,7 @@ export default function LiveMonitorMode() {
{tgtMeta.label}
</Badge>
</td>
<td className="py-2 pr-4 text-xs font-mono text-text-muted">
<td className="py-2 pr-4 text-xs font-mono text-text-muted break-all">
{event.model || notAvailable}
</td>
<td className="py-2 pr-4">

View File

@@ -186,7 +186,7 @@ export default function PlaygroundMode() {
onClick={handleTranslate}
loading={translating}
disabled={!inputContent.trim() || translating}
className="whitespace-nowrap"
className="w-full sm:w-auto"
>
{t("translateAction")}
</Button>

View File

@@ -107,7 +107,7 @@ export default function TestBenchMode() {
...prev,
[scenario.id]: {
status: "error",
error: errData.error || `HTTP ${sendRes.status}`,
error: errData.error || t("errorMessage", { message: `HTTP ${sendRes.status}` }),
latency,
httpStatus: sendRes.status,
},
@@ -152,7 +152,7 @@ export default function TestBenchMode() {
const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai;
return (
<div className="space-y-5">
<div className="space-y-5 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
@@ -167,7 +167,7 @@ export default function TestBenchMode() {
{/* Controls */}
<Card>
<div className="p-4 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row items-end gap-4">
<div className="flex flex-col sm:flex-row items-end gap-4 min-w-0">
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("source")}
@@ -206,6 +206,7 @@ export default function TestBenchMode() {
onClick={handleRunAll}
loading={runningAll}
disabled={runningAll}
className="w-full sm:w-auto"
>
{t("runAllTests")}
</Button>

View File

@@ -125,11 +125,13 @@ export default function Features() {
</span>
</div>
<h3
className={`text-lg font-bold mb-2 ${feature.colors.titleHover} transition-colors`}
className={`text-lg font-bold mb-2 break-words ${feature.colors.titleHover} transition-colors`}
>
{t(feature.titleKey)}
</h3>
<p className="text-sm text-gray-400 leading-relaxed">{t(feature.descKey)}</p>
<p className="text-sm text-gray-400 leading-relaxed break-words">
{t(feature.descKey)}
</p>
</div>
))}
</div>

View File

@@ -49,8 +49,8 @@ export default function FlowAnimation() {
}, [providers.length]);
return (
<div className="mt-16 w-full max-w-4xl">
<div className="relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite]">
<div className="mt-16 w-full max-w-4xl overflow-hidden">
<div className="relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite] overflow-hidden">
{/* OmniRoute Hub - Center */}
<div className="relative z-20 w-32 h-32 rounded-full bg-[#111520] border-2 border-[#E54D5E] shadow-[0_0_40px_rgba(229,77,94,0.3)] flex flex-col items-center justify-center gap-1 group cursor-pointer hover:scale-105 transition-transform duration-500">
<span className="material-symbols-outlined text-4xl text-[#E54D5E]" aria-hidden="true">
@@ -158,7 +158,7 @@ export default function FlowAnimation() {
{providers.map((provider, idx) => (
<div
key={provider.id}
className={`px-4 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-xs shadow-lg hover:scale-110 transition-all cursor-help min-w-[140px] ${
className={`px-3 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-[11px] leading-tight text-center shadow-lg hover:scale-110 transition-all cursor-help min-w-[110px] max-w-[140px] ${
activeFlow === idx ? "ring-4 ring-[#E54D5E]/50 scale-110" : ""
}`}
title={provider.name}

View File

@@ -18,7 +18,7 @@ export default function Footer() {
</div>
<h3 className="text-white text-lg font-bold">{t("brandName")}</h3>
</div>
<p className="text-gray-500 text-sm max-w-xs mb-6">{t("footerTagline")}</p>
<p className="text-gray-500 text-sm max-w-xs mb-6 break-words">{t("footerTagline")}</p>
<div className="flex gap-4">
<a
className="text-gray-400 hover:text-white transition-colors"
@@ -101,7 +101,9 @@ export default function Footer() {
{/* Bottom */}
<div className="border-t border-[#2D333B] pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-gray-600 text-sm">{t("copyright", { year })}</p>
<p className="text-gray-600 text-sm break-words text-center md:text-left">
{t("copyright", { year })}
</p>
<div className="flex gap-6">
<a
className="text-gray-600 hover:text-white text-sm transition-colors"

View File

@@ -26,7 +26,7 @@ export default function GetStarted() {
<p className="text-gray-400 text-lg mb-8">{t("getStartedDescription")}</p>
<div className="flex flex-col gap-6">
<div className="flex gap-4">
<div className="flex gap-4 min-w-0">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
1
</div>
@@ -36,7 +36,7 @@ export default function GetStarted() {
</div>
</div>
<div className="flex gap-4">
<div className="flex gap-4 min-w-0">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
2
</div>
@@ -46,7 +46,7 @@ export default function GetStarted() {
</div>
</div>
<div className="flex gap-4">
<div className="flex gap-4 min-w-0">
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
3
</div>
@@ -72,13 +72,13 @@ export default function GetStarted() {
</div>
{/* Terminal content */}
<div className="p-6 font-mono text-sm leading-relaxed overflow-x-auto">
<div className="p-6 font-mono text-sm leading-relaxed overflow-x-hidden">
<div
className="flex items-center gap-2 mb-4 group cursor-pointer"
className="flex items-center gap-2 mb-4 group cursor-pointer min-w-0"
onClick={() => handleCopy(command)}
>
<span className="text-green-400">$</span>
<span className="text-white">{command}</span>
<span className="text-white break-all">{command}</span>
<span className="ml-auto text-gray-500 text-xs opacity-0 group-hover:opacity-100">
{copied ? t("copied") : t("copy")}
</span>
@@ -88,10 +88,10 @@ export default function GetStarted() {
<span className="text-[#E54D5E]">&gt;</span> {t("startingOmniRoute")}
<br />
<span className="text-[#E54D5E]">&gt;</span> {t("serverRunningOnLabel")}{" "}
<span className="text-blue-400">{endpoint}</span>
<span className="text-blue-400 break-all">{endpoint}</span>
<br />
<span className="text-[#E54D5E]">&gt;</span> {t("dashboardLabel")}:{" "}
<span className="text-blue-400">{dashboardUrl}</span>
<span className="text-blue-400 break-all">{dashboardUrl}</span>
<br />
<span className="text-green-400">&gt;</span> {t("readyToRoute")}
</div>
@@ -104,10 +104,10 @@ export default function GetStarted() {
<span className="text-purple-400">{t("dataLocation")}</span>
<br />
<span className="text-gray-500">{t("dataLocationMacLinux")}</span>{" "}
~/.omniroute/db.json
<span className="break-all">~/.omniroute/db.json</span>
<br />
<span className="text-gray-500">{t("dataLocationWindows")}</span>{" "}
%APPDATA%/omniroute/db.json
<span className="break-all">%APPDATA%/omniroute/db.json</span>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@ export default function HeroSection() {
const router = useRouter();
return (
<section className="relative pt-32 pb-20 px-6 min-h-[90vh] flex flex-col items-center justify-center overflow-hidden">
<section className="relative pt-32 pb-20 px-4 sm:px-6 min-h-[90vh] flex flex-col items-center justify-center overflow-hidden">
{/* Glow effect */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] bg-[#E54D5E]/10 rounded-full blur-[120px] pointer-events-none"></div>
@@ -19,13 +19,13 @@ export default function HeroSection() {
</div>
{/* Main heading */}
<h1 className="text-5xl md:text-7xl font-black leading-[1.1] tracking-tight">
<h1 className="text-4xl sm:text-5xl md:text-7xl font-black leading-[1.1] tracking-tight break-words">
{t("oneEndpoint")} <br />
<span className="text-[#E54D5E]">{t("allProviders")}</span>
</h1>
{/* Description */}
<p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto font-light">
<p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto font-light break-words">
{t("heroDescription")}
</p>
@@ -33,7 +33,7 @@ export default function HeroSection() {
<div className="flex flex-wrap items-center justify-center gap-4 w-full">
<button
onClick={() => router.push("/dashboard")}
className="h-12 px-8 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-base font-bold transition-all shadow-[0_0_15px_rgba(229,77,94,0.4)] flex items-center gap-2"
className="w-full sm:w-auto h-12 px-8 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-base font-bold transition-all shadow-[0_0_15px_rgba(229,77,94,0.4)] flex items-center justify-center gap-2"
>
<span className="material-symbols-outlined" aria-hidden="true">
rocket_launch
@@ -44,7 +44,7 @@ export default function HeroSection() {
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
className="h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center gap-2"
className="w-full sm:w-auto h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center justify-center gap-2"
>
<span className="material-symbols-outlined" aria-hidden="true">
code

View File

@@ -24,8 +24,8 @@ export default function HowItWorks() {
</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2">{t("howItWorksStep1Title")}</h3>
<p className="text-sm text-gray-400">{t("howItWorksStep1Description")}</p>
<h3 className="text-xl font-bold mb-2 break-words">{t("howItWorksStep1Title")}</h3>
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep1Description")}</p>
</div>
</div>
@@ -40,8 +40,10 @@ export default function HowItWorks() {
</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-[#E54D5E]">{t("howItWorksStep2Title")}</h3>
<p className="text-sm text-gray-400">{t("howItWorksStep2Description")}</p>
<h3 className="text-xl font-bold mb-2 text-[#E54D5E] break-words">
{t("howItWorksStep2Title")}
</h3>
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep2Description")}</p>
</div>
</div>
@@ -56,8 +58,8 @@ export default function HowItWorks() {
</div>
</div>
<div>
<h3 className="text-xl font-bold mb-2">{t("howItWorksStep3Title")}</h3>
<p className="text-sm text-gray-400">{t("howItWorksStep3Description")}</p>
<h3 className="text-xl font-bold mb-2 break-words">{t("howItWorksStep3Title")}</h3>
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep3Description")}</p>
</div>
</div>
</div>

View File

@@ -11,18 +11,20 @@ export default function Navigation() {
return (
<nav className="fixed top-0 z-50 w-full bg-[#0B0E14]/80 backdrop-blur-md border-b border-[#2D333B]">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-3">
{/* Logo */}
<button
type="button"
className="flex items-center gap-3 cursor-pointer bg-transparent border-none p-0"
className="flex items-center gap-3 cursor-pointer bg-transparent border-none p-0 min-w-0"
onClick={() => router.push("/")}
aria-label={t("navigateHome")}
>
<div className="size-8 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] flex items-center justify-center text-white">
<OmniRouteLogo size={20} className="text-white" />
</div>
<h2 className="text-white text-xl font-bold tracking-tight">{t("brandName")}</h2>
<h2 className="text-white text-lg sm:text-xl font-bold tracking-tight truncate sm:max-w-none max-w-[160px]">
{t("brandName")}
</h2>
</button>
{/* Desktop menu */}
@@ -59,7 +61,7 @@ export default function Navigation() {
</div>
{/* CTA + Mobile menu */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 sm:gap-4 shrink-0">
<button
onClick={() => router.push("/dashboard")}
className="hidden sm:flex h-9 items-center justify-center rounded-lg px-4 bg-[#E54D5E] hover:bg-[#C93D4E] transition-all text-white text-sm font-bold shadow-[0_0_15px_rgba(229,77,94,0.4)] hover:shadow-[0_0_20px_rgba(229,77,94,0.6)]"

View File

@@ -63,11 +63,15 @@ export default function LandingPage() {
<Features />
{/* CTA Section */}
<section className="py-32 px-6 relative overflow-hidden">
<section className="py-24 sm:py-32 px-4 sm:px-6 relative overflow-hidden">
<div className="absolute inset-0 bg-linear-to-t from-[#E54D5E]/5 to-transparent pointer-events-none"></div>
<div className="max-w-4xl mx-auto text-center relative z-10">
<h2 className="text-4xl md:text-5xl font-black mb-6">{t("ctaTitle")}</h2>
<p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto">{t("ctaDescription")}</p>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black mb-6 break-words">
{t("ctaTitle")}
</h2>
<p className="text-lg sm:text-xl text-gray-400 mb-10 max-w-2xl mx-auto break-words">
{t("ctaDescription")}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button
onClick={() => router.push("/dashboard")}

View File

@@ -4,6 +4,7 @@ import { ThemeProvider } from "@/shared/components/ThemeProvider";
import "@/lib/initCloudSync"; // Auto-initialize cloud sync
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getLocale } from "next-intl/server";
import { RTL_LOCALES } from "@/i18n/config";
const inter = Inter({
subsets: ["latin"],
@@ -23,9 +24,10 @@ export const metadata = {
export default async function RootLayout({ children }) {
const locale = await getLocale();
const messages = await getMessages();
const isRtl = RTL_LOCALES.includes(locale as (typeof RTL_LOCALES)[number]);
return (
<html lang={locale} suppressHydrationWarning>
<html lang={locale} dir={isRtl ? "rtl" : "ltr"} suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />

View File

@@ -1,4 +1,35 @@
export const LOCALES = ["en", "pt-BR"] as const;
export const LOCALES = [
"en",
"pt-BR",
"es",
"fr",
"it",
"ru",
"zh-CN",
"de",
"in",
"th",
"uk-UA",
"ar",
"ja",
"vi",
"bg",
"da",
"fi",
"he",
"hu",
"id",
"ko",
"ms",
"nl",
"no",
"pt",
"ro",
"pl",
"sk",
"sv",
"phi",
] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = "en";
@@ -10,6 +41,36 @@ export const LANGUAGES: readonly {
}[] = [
{ code: "en", label: "EN", name: "English", flag: "🇺🇸" },
{ code: "pt-BR", label: "PT-BR", name: "Português (Brasil)", flag: "🇧🇷" },
{ code: "es", label: "ES", name: "Español", flag: "🇪🇸" },
{ code: "fr", label: "FR", name: "Français", flag: "🇫🇷" },
{ code: "it", label: "IT", name: "Italiano", flag: "🇮🇹" },
{ code: "ru", label: "RU", name: "Русский", flag: "🇷🇺" },
{ code: "zh-CN", label: "ZH-CN", name: "中文 (简体)", flag: "🇨🇳" },
{ code: "de", label: "DE", name: "Deutsch", flag: "🇩🇪" },
{ code: "in", label: "IN", name: "हिन्दी", flag: "🇮🇳" },
{ code: "th", label: "TH", name: "ไทย", flag: "🇹🇭" },
{ code: "uk-UA", label: "UK-UA", name: "Українська", flag: "🇺🇦" },
{ code: "ar", label: "AR", name: "العربية", flag: "🇸🇦" },
{ code: "ja", label: "JA", name: "日本語", flag: "🇯🇵" },
{ code: "vi", label: "VI", name: "Tiếng Việt", flag: "🇻🇳" },
{ code: "bg", label: "BG", name: "Български", flag: "🇧🇬" },
{ code: "da", label: "DA", name: "Dansk", flag: "🇩🇰" },
{ code: "fi", label: "FI", name: "Suomi", flag: "🇫🇮" },
{ code: "he", label: "HE", name: "עברית", flag: "🇮🇱" },
{ code: "hu", label: "HU", name: "Magyar", flag: "🇭🇺" },
{ code: "id", label: "ID", name: "Bahasa Indonesia", flag: "🇮🇩" },
{ code: "ko", label: "KO", name: "한국어", flag: "🇰🇷" },
{ code: "ms", label: "MS", name: "Bahasa Melayu", flag: "🇲🇾" },
{ code: "nl", label: "NL", name: "Nederlands", flag: "🇳🇱" },
{ code: "no", label: "NO", name: "Norsk", flag: "🇳🇴" },
{ code: "pt", label: "PT", name: "Português (Portugal)", flag: "🇵🇹" },
{ code: "ro", label: "RO", name: "Română", flag: "🇷🇴" },
{ code: "pl", label: "PL", name: "Polski", flag: "🇵🇱" },
{ code: "sk", label: "SK", name: "Slovenčina", flag: "🇸🇰" },
{ code: "sv", label: "SV", name: "Svenska", flag: "🇸🇪" },
{ code: "phi", label: "PHI", name: "Filipino", flag: "🇵🇭" },
] as const;
export const RTL_LOCALES = ["ar", "he"] as const;
export const LOCALE_COOKIE = "NEXT_LOCALE";