mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
feat(settings): add appearance tab and whitelabeling features
- Add dedicated 'appearance' tab to settings navigation - Move AppearanceTab from general to dedicated tab - Add whitelabeling fields to Zod schema (customLogoUrl, customLogoBase64) - Add whitelabeling UI section with: - App name customization - Custom logo URL input - Logo file upload with preview - Reset to default functionality - Add i18n keys for whitelabeling features This allows users to customize the application name and logo for white-labeling purposes.
This commit is contained in:
@@ -282,6 +282,109 @@ export default function AppearanceTab() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
badge
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{t("whitelabeling")}</h4>
|
||||
<p className="text-sm text-text-muted">{t("whitelabelingDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">{t("appName")}</p>
|
||||
<p className="text-sm text-text-muted">{t("appNameDesc")}</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.instanceName || "OmniRoute"}
|
||||
onChange={(e) => updateSetting("instanceName", e.target.value)}
|
||||
placeholder="OmniRoute"
|
||||
maxLength={100}
|
||||
className="h-10 px-3 rounded-lg bg-surface border border-border text-sm text-text-main focus:outline-none focus:border-primary w-48"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<p className="font-medium">{t("customLogo")}</p>
|
||||
<p className="text-sm text-text-muted">{t("customLogoDesc")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.customLogoUrl || ""}
|
||||
onChange={(e) => updateSetting("customLogoUrl", e.target.value)}
|
||||
className="flex-1 h-10 px-3 rounded-lg bg-surface border border-border text-sm text-text-main focus:outline-none focus:border-primary"
|
||||
placeholder="https://example.com/logo.png"
|
||||
maxLength={2000}
|
||||
/>
|
||||
{(settings.customLogoUrl || settings.customLogoBase64) && (
|
||||
<img
|
||||
src={settings.customLogoBase64 || settings.customLogoUrl}
|
||||
alt="Logo preview"
|
||||
className="h-10 w-10 rounded border border-border object-contain bg-surface"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-medium">{t("uploadLogo")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 500 * 1024) {
|
||||
alert("Logo file must be less than 500KB");
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64 = event.target?.result as string;
|
||||
updateSetting("customLogoBase64", base64);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}}
|
||||
className="text-sm text-text-muted"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
updateSetting("customLogoUrl", "");
|
||||
updateSetting("customLogoBase64", "");
|
||||
}}
|
||||
>
|
||||
{t("resetLogo")}
|
||||
</Button>
|
||||
</div>
|
||||
{(settings.customLogoBase64 || settings.customLogoUrl) && (
|
||||
<div className="mt-2 p-3 bg-black/5 dark:bg-white/5 rounded-lg">
|
||||
<p className="text-xs text-text-muted mb-2">{t("logoPreview")}</p>
|
||||
<img
|
||||
src={settings.customLogoBase64 || settings.customLogoUrl}
|
||||
alt="Logo preview"
|
||||
className="h-12 w-auto max-w-full rounded"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import ResilienceTab from "./components/ResilienceTab";
|
||||
|
||||
const tabs = [
|
||||
{ id: "general", labelKey: "general", icon: "settings" },
|
||||
{ id: "appearance", labelKey: "appearance", icon: "palette" },
|
||||
{ id: "ai", labelKey: "ai", icon: "smart_toy" },
|
||||
{ id: "security", labelKey: "security", icon: "shield" },
|
||||
{ id: "routing", labelKey: "routing", icon: "route" },
|
||||
@@ -75,14 +76,9 @@ export default function SettingsPage() {
|
||||
role="tabpanel"
|
||||
aria-label={t(tabs.find((t2) => t2.id === activeTab)?.labelKey || "general")}
|
||||
>
|
||||
{activeTab === "general" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-6">
|
||||
<SystemStorageTab />
|
||||
<AppearanceTab />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === "general" && <SystemStorageTab />}
|
||||
|
||||
{activeTab === "appearance" && <AppearanceTab />}
|
||||
|
||||
{activeTab === "ai" && (
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
@@ -1789,6 +1789,15 @@
|
||||
"themeViolet": "Violet",
|
||||
"themeOrange": "Orange",
|
||||
"themeCyan": "Cyan",
|
||||
"whitelabeling": "Branding",
|
||||
"whitelabelingDesc": "Customize the application name and logo",
|
||||
"appName": "Application Name",
|
||||
"appNameDesc": "Display name shown in sidebar and browser tab",
|
||||
"customLogo": "Custom Logo URL",
|
||||
"customLogoDesc": "URL to your custom logo image",
|
||||
"uploadLogo": "Upload Logo",
|
||||
"resetLogo": "Reset to Default",
|
||||
"logoPreview": "Preview",
|
||||
"promptCache": "Prompt Cache",
|
||||
"flushCache": "Flush Cache",
|
||||
"flushing": "Flushing…",
|
||||
|
||||
@@ -16,6 +16,8 @@ export const updateSettingsSchema = z.object({
|
||||
requireLogin: z.boolean().optional(),
|
||||
enableSocks5Proxy: z.boolean().optional(),
|
||||
instanceName: z.string().max(100).optional(),
|
||||
customLogoUrl: z.string().max(2000).optional(),
|
||||
customLogoBase64: z.string().max(100000).optional(),
|
||||
corsOrigins: z.string().max(500).optional(),
|
||||
cloudUrl: z.string().max(500).optional(),
|
||||
baseUrl: z.string().max(500).optional(),
|
||||
|
||||
Reference in New Issue
Block a user