From a48a24e6f2cd78988873b0490e134ffa23bf2728 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Fri, 7 Aug 2026 18:37:13 +0330 Subject: [PATCH] Apply tenant theme on business login and polish login branding. Co-authored-by: Cursor --- .../src/context/BusinessThemeContext.tsx | 25 ++++++--------- .../src/context/TenantBrandingContext.tsx | 10 ++++-- apps/business/src/pages/LoginPage.module.css | 14 +++++++++ apps/business/src/pages/LoginPage.tsx | 12 +++++-- apps/business/src/services/tenantService.ts | 6 ++-- .../src/context/TenantBrandingContext.tsx | 9 ++++-- apps/customer/src/pages/LoginPage.module.css | 14 +++++++++ apps/customer/src/pages/LoginPage.tsx | 12 +++++-- .../BusinessActionsOverflowMenu.module.css | 16 +++++++--- .../components/DomainLinksOverflowMenu.tsx | 31 ++++++++++++++++--- .../PrimaryColorSwatchControl.module.css | 10 +++--- apps/super-admin/src/pages/WebsitesPage.tsx | 15 +++------ 12 files changed, 122 insertions(+), 52 deletions(-) diff --git a/apps/business/src/context/BusinessThemeContext.tsx b/apps/business/src/context/BusinessThemeContext.tsx index f1d79a9..2a83766 100644 --- a/apps/business/src/context/BusinessThemeContext.tsx +++ b/apps/business/src/context/BusinessThemeContext.tsx @@ -1,32 +1,24 @@ import { useEffect, type ReactNode } from 'react' -import { useAuth } from './AuthContext' -import { getActiveBusinessId } from '../lib/businessContext' import { isAbortError } from '../lib/api' -import { getSettings } from '../services/settingsService' +import { getBusinessDomain } from '../lib/config' +import { resolveTenantByDomain } from '../services/tenantService' import { applyBusinessPrimaryColor, resetBusinessPrimaryColor } from '../utils/applyBusinessTheme' import { normalizeBusinessPrimaryColorId } from '../utils/businessPrimaryColors' export function BusinessThemeProvider({ children }: { children: ReactNode }) { - const { user, isLoading } = useAuth() - useEffect(() => { - if (isLoading) return - - if (!user || !getActiveBusinessId()) { - resetBusinessPrimaryColor() - return - } - const controller = new AbortController() + const domain = getBusinessDomain() async function loadTheme() { try { - const data = await getSettings(controller.signal) + const tenant = await resolveTenantByDomain(domain, controller.signal) + if (controller.signal.aborted) return applyBusinessPrimaryColor( - normalizeBusinessPrimaryColorId(data.settings.branding?.primaryColor), + normalizeBusinessPrimaryColorId(tenant.primaryColor), ) } catch (err) { - if (isAbortError(err)) return + if (isAbortError(err) || controller.signal.aborted) return applyBusinessPrimaryColor(undefined) } } @@ -35,8 +27,9 @@ export function BusinessThemeProvider({ children }: { children: ReactNode }) { return () => { controller.abort() + resetBusinessPrimaryColor() } - }, [user, isLoading]) + }, []) return children } diff --git a/apps/business/src/context/TenantBrandingContext.tsx b/apps/business/src/context/TenantBrandingContext.tsx index c6d3371..ac4ddd5 100644 --- a/apps/business/src/context/TenantBrandingContext.tsx +++ b/apps/business/src/context/TenantBrandingContext.tsx @@ -18,6 +18,7 @@ import { resolveTenantByDomain } from '../services/tenantService' interface TenantBrandingContextValue { businessName: string + businessNameEn: string logoUrl: string | null faviconUrl: string | null refreshBranding: () => void @@ -135,9 +136,14 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { return nameEn.trim() || nameFa.trim() || getBusinessDomain() }, [locale, nameEn, nameFa]) + const businessNameEn = useMemo( + () => nameEn.trim() || nameFa.trim() || getBusinessDomain(), + [nameEn, nameFa], + ) + const value = useMemo( - () => ({ businessName, logoUrl, faviconUrl, refreshBranding }), - [businessName, logoUrl, faviconUrl, refreshBranding], + () => ({ businessName, businessNameEn, logoUrl, faviconUrl, refreshBranding }), + [businessName, businessNameEn, logoUrl, faviconUrl, refreshBranding], ) return ( diff --git a/apps/business/src/pages/LoginPage.module.css b/apps/business/src/pages/LoginPage.module.css index b1e0c98..969236f 100644 --- a/apps/business/src/pages/LoginPage.module.css +++ b/apps/business/src/pages/LoginPage.module.css @@ -1,10 +1,12 @@ .page { min-height: 100vh; display: flex; + flex-direction: column; align-items: center; justify-content: center; padding: 24px; position: relative; + gap: 20px; } .card { @@ -60,6 +62,18 @@ margin-top: 2px; } +.poweredBy { + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + text-align: center; + transition: color 0.2s; +} + +.poweredBy:hover { + color: var(--primary); +} + .title { font-size: 24px; font-weight: 700; diff --git a/apps/business/src/pages/LoginPage.tsx b/apps/business/src/pages/LoginPage.tsx index cefe433..62d351e 100644 --- a/apps/business/src/pages/LoginPage.tsx +++ b/apps/business/src/pages/LoginPage.tsx @@ -23,7 +23,7 @@ type SmsStep = 'phone' | 'code' export function LoginPage() { const navigate = useNavigate() const { login, loginWithOtp } = useAuth() - const { businessName, logoUrl } = useTenantBranding() + const { businessName, businessNameEn, logoUrl } = useTenantBranding() const businessDomain = getBusinessDomain() const t = useT() @@ -217,7 +217,7 @@ export function LoginPage() { {logoUrl ? : null}
{businessName || businessDomain} - {t('app.poweredBy')} + {businessNameEn || businessDomain}
@@ -673,6 +673,14 @@ export function LoginPage() { )}
+ + Powered by Meshkee.app + ) } diff --git a/apps/business/src/services/tenantService.ts b/apps/business/src/services/tenantService.ts index 3ca5fe3..b54e997 100644 --- a/apps/business/src/services/tenantService.ts +++ b/apps/business/src/services/tenantService.ts @@ -1,5 +1,6 @@ import { apiRequest } from '../lib/api' import type { DashboardLocale } from '@meshkee/dashboard-core' +import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors' export interface ResolvedTenant { id: string @@ -7,12 +8,13 @@ export interface ResolvedTenant { nameFa: string | null slug: string domain: string + primaryColor: BusinessPrimaryColorId defaultLocale?: DashboardLocale logoUrl?: string | null faviconUrl?: string | null } -export async function resolveTenantByDomain(host: string) { +export async function resolveTenantByDomain(host: string, signal?: AbortSignal) { const encodedHost = encodeURIComponent(host) - return apiRequest(`/tenants/${encodedHost}`) + return apiRequest(`/tenants/${encodedHost}`, { signal }) } diff --git a/apps/customer/src/context/TenantBrandingContext.tsx b/apps/customer/src/context/TenantBrandingContext.tsx index 9a90078..8c0082d 100644 --- a/apps/customer/src/context/TenantBrandingContext.tsx +++ b/apps/customer/src/context/TenantBrandingContext.tsx @@ -16,6 +16,7 @@ import { resolveTenantByDomain } from '../services/tenantService' interface TenantBrandingContextValue { businessName: string + businessNameEn: string logoUrl: string | null faviconUrl: string | null } @@ -35,6 +36,7 @@ function pickBusinessName( export function TenantBrandingProvider({ children }: { children: ReactNode }) { const { setLocale } = useLocale() const [businessName, setBusinessName] = useState('') + const [businessNameEn, setBusinessNameEn] = useState('') const [logoUrl, setLogoUrl] = useState(null) const [faviconUrl, setFaviconUrl] = useState(null) const defaultLocaleAppliedRef = useRef(false) @@ -59,6 +61,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { tenant.name, domain, ) + const nameEn = pickBusinessName(info?.name, tenant.name, domain) const nextLogo = info?.logoUrl?.trim() || tenant.logoUrl?.trim() || null @@ -69,6 +72,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { null setBusinessName(name || domain) + setBusinessNameEn(nameEn || domain) setLogoUrl(nextLogo) setFaviconUrl(nextFavicon) applyDocumentFavicon(nextFavicon) @@ -84,6 +88,7 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { } catch (err) { if (isAbortError(err) || controller.signal.aborted) return setBusinessName(domain) + setBusinessNameEn(domain) setLogoUrl(null) setFaviconUrl(null) applyDocumentFavicon(null) @@ -98,8 +103,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) { }, [domain, setLocale]) const value = useMemo( - () => ({ businessName, logoUrl, faviconUrl }), - [businessName, logoUrl, faviconUrl], + () => ({ businessName, businessNameEn, logoUrl, faviconUrl }), + [businessName, businessNameEn, logoUrl, faviconUrl], ) return ( diff --git a/apps/customer/src/pages/LoginPage.module.css b/apps/customer/src/pages/LoginPage.module.css index babdbf0..6b88696 100644 --- a/apps/customer/src/pages/LoginPage.module.css +++ b/apps/customer/src/pages/LoginPage.module.css @@ -1,10 +1,12 @@ .page { min-height: 100vh; display: flex; + flex-direction: column; align-items: center; justify-content: center; padding: 24px; position: relative; + gap: 20px; } .card { @@ -60,6 +62,18 @@ margin-top: 2px; } +.poweredBy { + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + text-align: center; + transition: color 0.2s; +} + +.poweredBy:hover { + color: var(--primary); +} + .title { font-size: 24px; font-weight: 700; diff --git a/apps/customer/src/pages/LoginPage.tsx b/apps/customer/src/pages/LoginPage.tsx index 1a4c9d0..ef801f9 100644 --- a/apps/customer/src/pages/LoginPage.tsx +++ b/apps/customer/src/pages/LoginPage.tsx @@ -35,7 +35,7 @@ export function LoginPage() { const [searchParams] = useSearchParams() const redirectTo = safeRedirectPath(searchParams.get('redirect')) const { login, loginWithOtp } = useAuth() - const { businessName, logoUrl } = useTenantBranding() + const { businessName, businessNameEn, logoUrl } = useTenantBranding() const tenantDomain = getTenantDomain() const t = useT() @@ -247,7 +247,7 @@ export function LoginPage() {
{businessName || tenantDomain} - {t('app.poweredBy')} + {businessNameEn || tenantDomain}
@@ -704,6 +704,14 @@ export function LoginPage() { )}
+ + Powered by Meshkee.app + ) } diff --git a/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css b/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css index 93cb4ac..f3d284d 100644 --- a/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css +++ b/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css @@ -31,11 +31,11 @@ min-width: 220px; padding: 6px; border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.55); - background: rgba(255, 255, 255, 0.28); - backdrop-filter: blur(48px) saturate(1.2); - -webkit-backdrop-filter: blur(48px) saturate(1.2); - box-shadow: var(--glass-shadow); + border: 1px solid rgba(255, 255, 255, 0.85); + background: rgba(255, 255, 255, 0.78); + backdrop-filter: blur(72px) saturate(1.35); + -webkit-backdrop-filter: blur(72px) saturate(1.35); + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.16); animation: menuIn 0.14s ease; } @@ -86,6 +86,12 @@ color: #ef4444; } +.menuDivider { + height: 1px; + margin: 4px 6px; + background: rgba(148, 163, 184, 0.28); +} + @keyframes menuIn { from { opacity: 0; diff --git a/apps/super-admin/src/components/DomainLinksOverflowMenu.tsx b/apps/super-admin/src/components/DomainLinksOverflowMenu.tsx index 4656d0b..86e72c2 100644 --- a/apps/super-admin/src/components/DomainLinksOverflowMenu.tsx +++ b/apps/super-admin/src/components/DomainLinksOverflowMenu.tsx @@ -1,6 +1,13 @@ import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import { Building2, ExternalLink, Globe, MoreHorizontal, UserRound } from 'lucide-react' +import { + Building2, + ExternalLink, + Globe, + MoreHorizontal, + Trash2, + UserRound, +} from 'lucide-react' import styles from './BusinessActionsOverflowMenu.module.css' type PopoverPosition = { @@ -10,6 +17,7 @@ type PopoverPosition = { interface DomainLinksOverflowMenuProps { host: string + onRemove: () => void } function apexHost(host: string) { @@ -28,7 +36,7 @@ function openHost(host: string) { window.open(siteUrl(host), '_blank', 'noopener,noreferrer') } -export function DomainLinksOverflowMenu({ host }: DomainLinksOverflowMenuProps) { +export function DomainLinksOverflowMenu({ host, onRemove }: DomainLinksOverflowMenuProps) { const [open, setOpen] = useState(false) const [popoverPos, setPopoverPos] = useState(null) const wrapRef = useRef(null) @@ -126,8 +134,8 @@ export function DomainLinksOverflowMenu({ host }: DomainLinksOverflowMenuProps) e.stopPropagation() toggleOpen() }} - title="Open sites" - aria-label={`Open sites for ${apex}`} + title="More actions" + aria-label={`More actions for ${apex}`} aria-haspopup="menu" aria-expanded={open} > @@ -141,7 +149,7 @@ export function DomainLinksOverflowMenu({ host }: DomainLinksOverflowMenuProps) ref={popoverRef} className={styles.menu} role="menu" - aria-label={`Open sites for ${apex}`} + aria-label={`More actions for ${apex}`} style={{ top: popoverPos.top, left: popoverPos.left, @@ -168,6 +176,19 @@ export function DomainLinksOverflowMenu({ host }: DomainLinksOverflowMenuProps) ))} +
+
, document.body, )} diff --git a/apps/super-admin/src/components/PrimaryColorSwatchControl.module.css b/apps/super-admin/src/components/PrimaryColorSwatchControl.module.css index 244a583..3d18fed 100644 --- a/apps/super-admin/src/components/PrimaryColorSwatchControl.module.css +++ b/apps/super-admin/src/components/PrimaryColorSwatchControl.module.css @@ -50,11 +50,11 @@ max-width: 280px; padding: 8px 10px; border-radius: var(--radius-sm); - border: 1px solid rgba(255, 255, 255, 0.55); - background: rgba(255, 255, 255, 0.28); - backdrop-filter: blur(48px) saturate(1.2); - -webkit-backdrop-filter: blur(48px) saturate(1.2); - box-shadow: var(--glass-shadow); + border: 1px solid rgba(255, 255, 255, 0.85); + background: rgba(255, 255, 255, 0.78); + backdrop-filter: blur(72px) saturate(1.35); + -webkit-backdrop-filter: blur(72px) saturate(1.35); + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.16); } .option { diff --git a/apps/super-admin/src/pages/WebsitesPage.tsx b/apps/super-admin/src/pages/WebsitesPage.tsx index 2d9cf63..4f8a7c1 100644 --- a/apps/super-admin/src/pages/WebsitesPage.tsx +++ b/apps/super-admin/src/pages/WebsitesPage.tsx @@ -9,7 +9,6 @@ import { Rocket, RotateCcw, Search, - Trash2, Unlock, } from 'lucide-react' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' @@ -590,16 +589,10 @@ export function WebsitesPage() { > - - + setRemoveTarget(domain)} + />