import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, Image, Palette, RotateCcw, Upload, X, } from "lucide-react"; import { useState } from "react"; import { THEME_PRESETS, useColorTheme } from "../../contexts/ColorThemeContext"; import { settingsAPI } from "../../utils/api"; const BrandingTab = () => { // Logo management state const [logoUploadState, setLogoUploadState] = useState({ dark: { uploading: false, error: null }, light: { uploading: false, error: null }, favicon: { uploading: false, error: null }, }); const [showLogoUploadModal, setShowLogoUploadModal] = useState(false); const [selectedLogoType, setSelectedLogoType] = useState("dark"); const { colorTheme, setColorTheme } = useColorTheme(); const queryClient = useQueryClient(); // Fetch current settings const { data: settings, isLoading, error, } = useQuery({ queryKey: ["settings"], queryFn: () => settingsAPI.get().then((res) => res.data), }); // Logo upload mutation const uploadLogoMutation = useMutation({ mutationFn: ({ logoType, fileContent, fileName }) => fetch("/api/v1/settings/logos/upload", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token")}`, }, body: JSON.stringify({ logoType, fileContent, fileName }), }).then((res) => res.json()), onSuccess: (_data, variables) => { queryClient.invalidateQueries(["settings"]); setLogoUploadState((prev) => ({ ...prev, [variables.logoType]: { uploading: false, error: null }, })); setShowLogoUploadModal(false); }, onError: (error, variables) => { console.error("Upload logo error:", error); setLogoUploadState((prev) => ({ ...prev, [variables.logoType]: { uploading: false, error: error.message || "Failed to upload logo", }, })); }, }); // Logo reset mutation const resetLogoMutation = useMutation({ mutationFn: (logoType) => fetch("/api/v1/settings/logos/reset", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token")}`, }, body: JSON.stringify({ logoType }), }).then((res) => res.json()), onSuccess: () => { queryClient.invalidateQueries(["settings"]); }, onError: (error) => { console.error("Reset logo error:", error); }, }); // Theme update mutation const updateThemeMutation = useMutation({ mutationFn: (theme) => settingsAPI.update({ colorTheme: theme }), onSuccess: (_data, theme) => { queryClient.invalidateQueries(["settings"]); setColorTheme(theme); }, onError: (error) => { console.error("Update theme error:", error); }, }); const handleThemeChange = (theme) => { updateThemeMutation.mutate(theme); }; if (isLoading) { return (
); } if (error) { return (

Error loading settings

{error.response?.data?.error || "Failed to load settings"}

); } return (
{/* Header */}

Logo & Branding

Customize your PatchMon installation with custom logos, favicon, and color themes. These will be displayed throughout the application.

{/* Color Theme Selector */}

Color Theme

Choose a color theme that will be applied to the login page and background areas throughout the app.

{Object.entries(THEME_PRESETS).map(([themeKey, theme]) => { const isSelected = colorTheme === themeKey; const gradientColors = theme.login.xColors; return ( ); })}
{updateThemeMutation.isPending && (
Updating theme...
)} {updateThemeMutation.isError && (

Failed to update theme: {updateThemeMutation.error?.message}

)}
{/* Logo Section Header */}

Logos

{/* Dark Logo */}

Dark Logo

Dark Logo { e.target.src = "/assets/logo_dark.png"; }} />

{settings?.logo_dark ? settings.logo_dark.split("/").pop() : "logo_dark.png (Default)"}

{settings?.logo_dark && ( )}
{logoUploadState.dark.error && (

{logoUploadState.dark.error}

)}
{/* Light Logo */}

Light Logo

Light Logo { e.target.src = "/assets/logo_light.png"; }} />

{settings?.logo_light ? settings.logo_light.split("/").pop() : "logo_light.png (Default)"}

{settings?.logo_light && ( )}
{logoUploadState.light.error && (

{logoUploadState.light.error}

)}
{/* Favicon */}

Favicon

Favicon { e.target.src = "/assets/favicon.svg"; }} />

{settings?.favicon ? settings.favicon.split("/").pop() : "favicon.svg (Default)"}

{settings?.favicon && ( )}
{logoUploadState.favicon.error && (

{logoUploadState.favicon.error}

)}
{/* Usage Instructions */}

Logo Usage

These logos are used throughout the application:

  • Dark Logo: Used in dark mode and on light backgrounds
  • Light Logo: Used in light mode and on dark backgrounds
  • Favicon: Used as the browser tab icon (SVG recommended)

Supported formats: PNG, JPG, SVG |{" "} Max size: 5MB |{" "} Recommended sizes: 200x60px for logos, 32x32px for favicon.

{/* Logo Upload Modal */} {showLogoUploadModal && ( setShowLogoUploadModal(false)} onSubmit={uploadLogoMutation.mutate} isLoading={uploadLogoMutation.isPending} error={uploadLogoMutation.error} logoType={selectedLogoType} /> )}
); }; // Logo Upload Modal Component const LogoUploadModal = ({ isOpen, onClose, onSubmit, isLoading, error, logoType, }) => { const [selectedFile, setSelectedFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [uploadError, setUploadError] = useState(""); const handleFileSelect = (e) => { const file = e.target.files[0]; if (file) { // Validate file type const allowedTypes = [ "image/png", "image/jpeg", "image/jpg", "image/svg+xml", ]; if (!allowedTypes.includes(file.type)) { setUploadError("Please select a PNG, JPG, or SVG file"); return; } // Validate file size (5MB limit) if (file.size > 5 * 1024 * 1024) { setUploadError("File size must be less than 5MB"); return; } setSelectedFile(file); setUploadError(""); // Create preview URL const url = URL.createObjectURL(file); setPreviewUrl(url); } }; const handleSubmit = (e) => { e.preventDefault(); setUploadError(""); if (!selectedFile) { setUploadError("Please select a file"); return; } // Convert file to base64 const reader = new FileReader(); reader.onload = (event) => { const base64 = event.target.result; onSubmit({ logoType, fileContent: base64, fileName: selectedFile.name, }); }; reader.readAsDataURL(selectedFile); }; const handleClose = () => { setSelectedFile(null); setPreviewUrl(null); setUploadError(""); onClose(); }; if (!isOpen) return null; return (

Upload{" "} {logoType === "favicon" ? "Favicon" : `${logoType.charAt(0).toUpperCase() + logoType.slice(1)} Logo`}

Supported formats: PNG, JPG, SVG. Max size: 5MB. {logoType === "favicon" ? " Recommended: 32x32px SVG." : " Recommended: 200x60px."}

{previewUrl && (
Preview
Preview
)} {(uploadError || error) && (

{uploadError || error?.response?.data?.error || error?.message}

)}

Important:

  • This will replace the current {logoType} logo
  • A backup will be created automatically
  • The change will be applied immediately
); }; export default BrandingTab;