mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-24 23:22:31 +08:00
feat(admin): refine pro-gated settings UI
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ProBadgeProps {
|
||||
className?: string
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export function ProBadge({ className }: ProBadgeProps) {
|
||||
function ProBadgePill({ className }: { className?: string }) {
|
||||
return (
|
||||
<span
|
||||
data-slot="pro-badge"
|
||||
@@ -15,3 +17,20 @@ export function ProBadge({ className }: ProBadgeProps) {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProBadge({ className, tooltip }: ProBadgeProps) {
|
||||
if (!tooltip) return <ProBadgePill className={className} />
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button" className="inline-flex cursor-help appearance-none bg-transparent p-0">
|
||||
<ProBadgePill className={className} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-72">
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { BrandingConfig, BrandingField } from '@shared/types'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Eye, ImageUp, Palette, Trash2, Upload } from 'lucide-react'
|
||||
import { ImageUp, Palette, Trash2, Upload } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { brandingQueryKey } from '@/components/branding/BrandingProvider'
|
||||
import { ProBadge } from '@/components/ProBadge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useSiteOptions } from '@/hooks/use-site-options'
|
||||
import { useEntitlement } from '@/hooks/useEntitlement'
|
||||
import { getBranding, resetBrandingField, saveBranding } from '@/lib/api'
|
||||
|
||||
@@ -18,7 +17,6 @@ import { getBranding, resetBrandingField, saveBranding } from '@/lib/api'
|
||||
interface BrandingFormState {
|
||||
logoFile: File | null
|
||||
faviconFile: File | null
|
||||
hidePoweredBy: boolean
|
||||
previewLogoUrl: string | null
|
||||
faviconPreviewUrl: string | null
|
||||
}
|
||||
@@ -27,11 +25,9 @@ function useBrandingFormState(initial: BrandingConfig): {
|
||||
state: BrandingFormState
|
||||
setLogoFile: (f: File | null) => void
|
||||
setFaviconFile: (f: File | null) => void
|
||||
setHidePoweredBy: (v: boolean) => void
|
||||
} {
|
||||
const [logoFile, setLogoFileRaw] = useState<File | null>(null)
|
||||
const [faviconFile, setFaviconFileRaw] = useState<File | null>(null)
|
||||
const [hidePoweredBy, setHidePoweredBy] = useState(initial.hide_powered_by)
|
||||
const [previewLogoUrl, setPreviewLogoUrl] = useState<string | null>(initial.logo_url)
|
||||
const [faviconPreviewUrl, setFaviconPreviewUrl] = useState<string | null>(initial.favicon_url)
|
||||
|
||||
@@ -62,10 +58,9 @@ function useBrandingFormState(initial: BrandingConfig): {
|
||||
}, [])
|
||||
|
||||
return {
|
||||
state: { logoFile, faviconFile, hidePoweredBy, previewLogoUrl, faviconPreviewUrl },
|
||||
state: { logoFile, faviconFile, previewLogoUrl, faviconPreviewUrl },
|
||||
setLogoFile,
|
||||
setFaviconFile,
|
||||
setHidePoweredBy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,44 +148,6 @@ function FileUploadField({
|
||||
)
|
||||
}
|
||||
|
||||
function LivePreview({ branding }: { branding: BrandingConfig }) {
|
||||
const { t } = useTranslation()
|
||||
const { siteName } = useSiteOptions()
|
||||
const logoSrc = branding.logo_url ?? '/logo.svg'
|
||||
|
||||
return (
|
||||
<div className="rounded-[24px] border border-border/60 bg-gradient-to-br from-sidebar via-background to-muted/40 p-5 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
||||
<Eye className="h-4 w-4" />
|
||||
<span>{t('admin.settings.branding.preview')}</span>
|
||||
</div>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="rounded-2xl border border-border/60 bg-background/90 p-4">
|
||||
<div className="flex items-center gap-3 border-b border-border/60 pb-3">
|
||||
<img src={logoSrc} alt={siteName} className="size-9 rounded-lg object-contain" />
|
||||
<span className="text-lg font-semibold">{siteName}</span>
|
||||
</div>
|
||||
<div className="pt-3">
|
||||
{!branding.hide_powered_by && (
|
||||
<p className="text-xs text-center text-muted-foreground/70">{t('admin.settings.branding.poweredBy')}</p>
|
||||
)}
|
||||
{branding.hide_powered_by && (
|
||||
<p className="text-xs text-center text-muted-foreground/70">
|
||||
{t('admin.settings.branding.poweredHidden')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-dashed border-border/70 bg-background/70 p-4">
|
||||
<p className="text-sm font-medium">{t('admin.settings.branding.previewHintTitle')}</p>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">{t('admin.settings.branding.previewHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main Section ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function BrandingSection() {
|
||||
@@ -217,14 +174,13 @@ export function BrandingSection() {
|
||||
function BrandingForm({ initial, disabled }: { initial: BrandingConfig; disabled: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const { state, setLogoFile, setFaviconFile, setHidePoweredBy } = useBrandingFormState(initial)
|
||||
const { state, setLogoFile, setFaviconFile } = useBrandingFormState(initial)
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
saveBranding({
|
||||
logo: state.logoFile,
|
||||
favicon: state.faviconFile,
|
||||
hide_powered_by: state.hidePoweredBy,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: brandingQueryKey })
|
||||
@@ -239,125 +195,93 @@ function BrandingForm({ initial, disabled }: { initial: BrandingConfig; disabled
|
||||
queryClient.invalidateQueries({ queryKey: brandingQueryKey })
|
||||
if (field === 'logo') setLogoFile(null)
|
||||
if (field === 'favicon') setFaviconFile(null)
|
||||
if (field === 'hide_powered_by') setHidePoweredBy(false)
|
||||
toast.success(t('admin.settings.branding.resetSuccess', { field: t(`admin.settings.branding.fields.${field}`) }))
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const previewBranding: BrandingConfig = {
|
||||
logo_url: state.previewLogoUrl,
|
||||
favicon_url: state.faviconPreviewUrl,
|
||||
wordmark_text: null,
|
||||
hide_powered_by: state.hidePoweredBy,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1.2fr)_340px]">
|
||||
<div className="space-y-6">
|
||||
<Card className="border-border/60">
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-2xl border border-border/60 bg-primary/10 p-2 text-primary">
|
||||
<Palette className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<CardTitle>{t('admin.settings.branding.assetsTitle')}</CardTitle>
|
||||
<CardDescription>{t('admin.settings.branding.assetsDescription')}</CardDescription>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<Card className="border-border/60">
|
||||
<CardHeader className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-2xl border border-border/60 bg-primary/10 p-2 text-primary">
|
||||
<Palette className="h-5 w-5" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!disabled && (
|
||||
<>
|
||||
<FileUploadField
|
||||
id="logo-upload"
|
||||
label={t('admin.settings.branding.logo')}
|
||||
hint={t('admin.settings.branding.logoHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
previewUrl={state.previewLogoUrl}
|
||||
onFileChange={setLogoFile}
|
||||
onReset={() => resetMutation.mutate('logo')}
|
||||
/>
|
||||
<FileUploadField
|
||||
id="favicon-upload"
|
||||
label={t('admin.settings.branding.favicon')}
|
||||
hint={t('admin.settings.branding.faviconHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
accept="image/png,image/x-icon,image/vnd.microsoft.icon,image/svg+xml"
|
||||
previewUrl={state.faviconPreviewUrl}
|
||||
onFileChange={setFaviconFile}
|
||||
onReset={() => resetMutation.mutate('favicon')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{disabled && (
|
||||
<div className="space-y-4 rounded-2xl border border-dashed border-border/70 bg-muted/25 p-4 text-sm text-muted-foreground">
|
||||
<p>{t('admin.settings.branding.lockedMessage')}</p>
|
||||
<FileUploadField
|
||||
id="logo-upload-disabled"
|
||||
label={t('admin.settings.branding.logo')}
|
||||
hint={t('admin.settings.branding.logoHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
disabled
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
previewUrl={state.previewLogoUrl}
|
||||
onFileChange={() => {}}
|
||||
onReset={() => {}}
|
||||
/>
|
||||
<FileUploadField
|
||||
id="favicon-upload-disabled"
|
||||
label={t('admin.settings.branding.favicon')}
|
||||
hint={t('admin.settings.branding.faviconHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
disabled
|
||||
accept="image/png,image/x-icon,image/vnd.microsoft.icon,image/svg+xml"
|
||||
previewUrl={state.faviconPreviewUrl}
|
||||
onFileChange={() => {}}
|
||||
onReset={() => {}}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle>{t('admin.settings.branding.assetsTitle')}</CardTitle>
|
||||
<ProBadge tooltip={t('admin.settings.proLockedWhiteLabel')} />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border/60">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle>{t('admin.settings.branding.visibilityTitle')}</CardTitle>
|
||||
<CardDescription>{t('admin.settings.branding.visibilityDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between gap-4 rounded-2xl border border-border/60 bg-background p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="hide-powered-by">{t('admin.settings.branding.hidePoweredBy')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.settings.branding.hidePoweredByHint')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="hide-powered-by"
|
||||
checked={state.hidePoweredBy}
|
||||
disabled={disabled}
|
||||
onCheckedChange={setHidePoweredBy}
|
||||
<CardDescription>{t('admin.settings.branding.assetsDescription')}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!disabled && (
|
||||
<>
|
||||
<FileUploadField
|
||||
id="logo-upload"
|
||||
label={t('admin.settings.branding.logo')}
|
||||
hint={t('admin.settings.branding.logoHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
previewUrl={state.previewLogoUrl}
|
||||
onFileChange={setLogoFile}
|
||||
onReset={() => resetMutation.mutate('logo')}
|
||||
/>
|
||||
<FileUploadField
|
||||
id="favicon-upload"
|
||||
label={t('admin.settings.branding.favicon')}
|
||||
hint={t('admin.settings.branding.faviconHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
accept="image/png,image/x-icon,image/vnd.microsoft.icon,image/svg+xml"
|
||||
previewUrl={state.faviconPreviewUrl}
|
||||
onFileChange={setFaviconFile}
|
||||
onReset={() => resetMutation.mutate('favicon')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{disabled && (
|
||||
<div className="space-y-4 rounded-2xl border border-dashed border-border/70 bg-muted/25 p-4 text-sm text-muted-foreground">
|
||||
<FileUploadField
|
||||
id="logo-upload-disabled"
|
||||
label={t('admin.settings.branding.logo')}
|
||||
hint={t('admin.settings.branding.logoHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
disabled
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
previewUrl={state.previewLogoUrl}
|
||||
onFileChange={() => {}}
|
||||
onReset={() => {}}
|
||||
/>
|
||||
<FileUploadField
|
||||
id="favicon-upload-disabled"
|
||||
label={t('admin.settings.branding.favicon')}
|
||||
hint={t('admin.settings.branding.faviconHint')}
|
||||
emptyLabel={t('admin.settings.branding.upload')}
|
||||
replaceLabel={t('admin.settings.branding.replace')}
|
||||
resetLabel={t('admin.settings.branding.reset')}
|
||||
disabled
|
||||
accept="image/png,image/x-icon,image/vnd.microsoft.icon,image/svg+xml"
|
||||
previewUrl={state.faviconPreviewUrl}
|
||||
onFileChange={() => {}}
|
||||
onReset={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button onClick={() => saveMutation.mutate()} disabled={disabled || saveMutation.isPending}>
|
||||
{saveMutation.isPending ? t('admin.settings.branding.saving') : t('admin.settings.branding.save')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2 xl:sticky xl:top-4 xl:self-start">
|
||||
<LivePreview branding={previewBranding} />
|
||||
</div>
|
||||
<Button onClick={() => saveMutation.mutate()} disabled={disabled || saveMutation.isPending}>
|
||||
{saveMutation.isPending ? t('admin.settings.branding.saving') : t('admin.settings.branding.save')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,12 +154,14 @@
|
||||
"admin.settings.metricExperienceHint": "Use custom assets and copy to make the instance feel like your own product.",
|
||||
"admin.settings.identityTitle": "Site identity",
|
||||
"admin.settings.identityDescription": "Set the primary name and short description users see across the product.",
|
||||
"admin.settings.identityProTooltip": "Custom site name and description are available in ZPan Pro.",
|
||||
"admin.settings.proLockedWhiteLabel": "This section is a ZPan Pro feature. Without Pro, site name, description, branding, and footer copyright controls stay visible but locked.",
|
||||
"admin.settings.registrationTitle": "New user registration",
|
||||
"admin.settings.registrationDescription": "Control whether new users are allowed to create accounts on this instance.",
|
||||
"admin.settings.registrationLabel": "Allow new user sign-ups",
|
||||
"admin.settings.registrationHintOpen": "New users can register directly from the sign-up page.",
|
||||
"admin.settings.registrationHintClosed": "New account creation is disabled for this instance.",
|
||||
"admin.settings.registrationUpgradeHint": "Open registration requires ZPan Pro.",
|
||||
"admin.settings.registrationUpgradeHint": "Open registration is disabled without ZPan Pro.",
|
||||
"admin.settings.siteNamePlaceholder": "ZPan Workspace",
|
||||
"admin.settings.siteNameHint": "Keep it short and recognizable. It will be reused in navigation and browser chrome.",
|
||||
"admin.settings.siteDescriptionPlaceholder": "Private file hosting for your team, customers, or community.",
|
||||
@@ -181,7 +183,7 @@
|
||||
"admin.settings.branding.poweredHidden": "Footer credit hidden",
|
||||
"admin.settings.branding.previewHintTitle": "What changes here",
|
||||
"admin.settings.branding.previewHint": "Logo and favicon updates affect shared surfaces immediately after saving.",
|
||||
"admin.settings.branding.assetsTitle": "Logo and favicon",
|
||||
"admin.settings.branding.assetsTitle": "Branding",
|
||||
"admin.settings.branding.assetsDescription": "Upload the assets users will see in the sidebar, browser tab, and public entry points.",
|
||||
"admin.settings.branding.logo": "Logo",
|
||||
"admin.settings.branding.logoHint": "Best for square or compact marks. PNG, JPG, WebP, or SVG.",
|
||||
|
||||
@@ -154,12 +154,14 @@
|
||||
"admin.settings.metricExperienceHint": "通过品牌素材和文案,让实例更像你自己的产品,而不是默认安装页。",
|
||||
"admin.settings.identityTitle": "站点标识",
|
||||
"admin.settings.identityDescription": "设置用户在整个产品中都会看到的站点名称和简短介绍。",
|
||||
"admin.settings.identityProTooltip": "自定义站点名称和描述仅在 ZPan Pro 中可用。",
|
||||
"admin.settings.proLockedWhiteLabel": "这个区块属于 ZPan Pro。未激活 Pro 时,站点名称、描述、Branding 和底部署名控制会继续显示,但会保持禁用。",
|
||||
"admin.settings.registrationTitle": "新用户注册",
|
||||
"admin.settings.registrationDescription": "控制这个实例是否允许新用户自行创建账号。",
|
||||
"admin.settings.registrationLabel": "允许新用户注册",
|
||||
"admin.settings.registrationHintOpen": "新用户可以直接从注册页创建账号。",
|
||||
"admin.settings.registrationHintClosed": "当前实例已关闭新账号注册。",
|
||||
"admin.settings.registrationUpgradeHint": "开放注册需要 ZPan Pro。",
|
||||
"admin.settings.registrationUpgradeHint": "未激活 ZPan Pro 时,不允许开放注册。",
|
||||
"admin.settings.siteNamePlaceholder": "ZPan Workspace",
|
||||
"admin.settings.siteNameHint": "保持简短且易识别,它会被复用到导航和浏览器标题等位置。",
|
||||
"admin.settings.siteDescriptionPlaceholder": "为你的团队、客户或社区提供私有文件托管服务。",
|
||||
@@ -181,7 +183,7 @@
|
||||
"admin.settings.branding.poweredHidden": "底部署名已隐藏",
|
||||
"admin.settings.branding.previewHintTitle": "这里会变化什么",
|
||||
"admin.settings.branding.previewHint": "保存后,Logo 和 favicon 会立即影响到共享界面和应用入口。",
|
||||
"admin.settings.branding.assetsTitle": "Logo 与 Favicon",
|
||||
"admin.settings.branding.assetsTitle": "Branding",
|
||||
"admin.settings.branding.assetsDescription": "上传会出现在侧边栏、浏览器标签页和公共入口中的品牌素材。",
|
||||
"admin.settings.branding.logo": "Logo",
|
||||
"admin.settings.branding.logoHint": "适合方形或紧凑型标识。支持 PNG、JPG、WebP、SVG。",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
import { BrandingSection } from '@/components/admin/branding-section'
|
||||
import { ProBadge } from '@/components/ProBadge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -43,11 +44,24 @@ const settingsSchema = z.object({
|
||||
|
||||
type SettingsFormValues = z.infer<typeof settingsSchema>
|
||||
|
||||
function ProFeatureHeader({ title, description, tooltip }: { title: string; description: string; tooltip: string }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<ProBadge tooltip={tooltip} />
|
||||
</div>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const { siteName, siteDescription, defaultOrgQuota: quotaBytes, authSignupMode, isLoading } = useSiteOptions()
|
||||
const { hasFeature } = useEntitlement()
|
||||
const hasWhiteLabel = hasFeature('white_label')
|
||||
const hasOpenRegistration = hasFeature('open_registration')
|
||||
|
||||
const form = useForm<SettingsFormValues>({
|
||||
@@ -112,17 +126,21 @@ function SettingsPage() {
|
||||
<div className="rounded-2xl border border-border/60 bg-primary/10 p-2 text-primary">
|
||||
<Globe2 className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<CardTitle>{t('admin.settings.identityTitle')}</CardTitle>
|
||||
<CardDescription>{t('admin.settings.identityDescription')}</CardDescription>
|
||||
</div>
|
||||
<ProFeatureHeader
|
||||
title={t('admin.settings.identityTitle')}
|
||||
description={t('admin.settings.identityDescription')}
|
||||
tooltip={t('admin.settings.identityProTooltip')}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className={`space-y-2 ${!hasWhiteLabel ? 'opacity-60' : ''}`}>
|
||||
<Label htmlFor="siteName">{t('admin.settings.siteName')}</Label>
|
||||
<Input
|
||||
id="siteName"
|
||||
readOnly={!hasWhiteLabel}
|
||||
aria-disabled={!hasWhiteLabel}
|
||||
tabIndex={!hasWhiteLabel ? -1 : undefined}
|
||||
placeholder={t('admin.settings.siteNamePlaceholder')}
|
||||
{...form.register('siteName')}
|
||||
/>
|
||||
@@ -132,11 +150,14 @@ function SettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={`space-y-2 ${!hasWhiteLabel ? 'opacity-60' : ''}`}>
|
||||
<Label htmlFor="siteDescription">{t('admin.settings.siteDescription')}</Label>
|
||||
<Textarea
|
||||
id="siteDescription"
|
||||
rows={4}
|
||||
readOnly={!hasWhiteLabel}
|
||||
aria-disabled={!hasWhiteLabel}
|
||||
tabIndex={!hasWhiteLabel ? -1 : undefined}
|
||||
placeholder={t('admin.settings.siteDescriptionPlaceholder')}
|
||||
{...form.register('siteDescription')}
|
||||
/>
|
||||
@@ -154,10 +175,11 @@ function SettingsPage() {
|
||||
<div className="rounded-2xl border border-border/60 bg-amber-500/10 p-2 text-amber-600">
|
||||
<Globe2 className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<CardTitle>{t('admin.settings.registrationTitle')}</CardTitle>
|
||||
<CardDescription>{t('admin.settings.registrationDescription')}</CardDescription>
|
||||
</div>
|
||||
<ProFeatureHeader
|
||||
title={t('admin.settings.registrationTitle')}
|
||||
description={t('admin.settings.registrationDescription')}
|
||||
tooltip={t('admin.settings.registrationUpgradeHint')}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -169,11 +191,6 @@ function SettingsPage() {
|
||||
? t('admin.settings.registrationHintOpen')
|
||||
: t('admin.settings.registrationHintClosed')}
|
||||
</p>
|
||||
{!hasOpenRegistration && (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{t('admin.settings.registrationUpgradeHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
id="registrationsEnabled"
|
||||
@@ -241,13 +258,7 @@ function SettingsPage() {
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold">{t('admin.settings.brandingTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('admin.settings.brandingDescription')}</p>
|
||||
</div>
|
||||
<BrandingSection />
|
||||
</div>
|
||||
<BrandingSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user