Apply tenant theme on business login and polish login branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-07 18:37:13 +03:30
co-authored by Cursor
parent 56f6b23074
commit a48a24e6f2
12 changed files with 122 additions and 52 deletions
@@ -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
}
@@ -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 (
@@ -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;
+10 -2
View File
@@ -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 ? <img src={logoUrl} alt="" className={styles.logo} /> : null}
<div className={styles.brandText}>
<span className={styles.businessName}>{businessName || businessDomain}</span>
<span className={styles.appName}>{t('app.poweredBy')}</span>
<span className={styles.appName}>{businessNameEn || businessDomain}</span>
</div>
<div className={styles.langSelect}>
<LanguageSelect />
@@ -673,6 +673,14 @@ export function LoginPage() {
</>
)}
</div>
<a
className={styles.poweredBy}
href="https://meshkee.com"
target="_blank"
rel="noopener noreferrer"
>
Powered by Meshkee.app
</a>
</div>
)
}
+4 -2
View File
@@ -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<ResolvedTenant>(`/tenants/${encodedHost}`)
return apiRequest<ResolvedTenant>(`/tenants/${encodedHost}`, { signal })
}