mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: enhance tenant management with photo handling and UI improvements
- Added a new endpoint in SuperAdminTenantController to fetch tenant photos. - Introduced TenantDetailModal for displaying tenant details and associated photos. - Created TenantUsageCell component to visualize tenant usage metrics. - Updated localization files to include new keys for tenant photo management. - Enhanced SuperAdminTenantManager to support modal interactions for tenant details. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { Body, Controller, Get, Param, Patch } from '@afilmory/framework'
|
||||
import { photoAssets } from '@afilmory/db'
|
||||
import { Body, Controller, Get, Param, Patch, Query } from '@afilmory/framework'
|
||||
import { DbAccessor } from 'core/database/database.provider'
|
||||
import { Roles } from 'core/guards/roles.decorator'
|
||||
import { BypassResponseTransform } from 'core/interceptors/response-transform.decorator'
|
||||
import { BillingPlanService } from 'core/modules/platform/billing/billing-plan.service'
|
||||
import { BillingUsageService } from 'core/modules/platform/billing/billing-usage.service'
|
||||
import { TenantService } from 'core/modules/platform/tenant/tenant.service'
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
|
||||
import type { BillingPlanId } from '../billing/billing-plan.types'
|
||||
import { UpdateTenantBanDto, UpdateTenantPlanDto } from './super-admin.dto'
|
||||
@@ -16,8 +19,27 @@ export class SuperAdminTenantController {
|
||||
private readonly tenantService: TenantService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly billingUsageService: BillingUsageService,
|
||||
private readonly db: DbAccessor,
|
||||
) {}
|
||||
|
||||
@Get('/:tenantId/photos')
|
||||
async getTenantPhotos(@Param('tenantId') tenantId: string, @Query('limit') limit = '20') {
|
||||
const photos = await this.db
|
||||
.get()
|
||||
.select()
|
||||
.from(photoAssets)
|
||||
.where(eq(photoAssets.tenantId, tenantId))
|
||||
.limit(Number(limit))
|
||||
.orderBy(desc(photoAssets.createdAt))
|
||||
|
||||
return {
|
||||
photos: photos.map((p) => ({
|
||||
...p,
|
||||
publicUrl: p.manifest.data.thumbnailUrl,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async listTenants() {
|
||||
const [tenantAggregates, plans] = await Promise.all([
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@afilmory/framework'
|
||||
import { DatabaseModule } from 'core/database/database.module'
|
||||
import { SystemSettingModule } from 'core/modules/configuration/system-setting/system-setting.module'
|
||||
import { PhotoBuilderService } from 'core/modules/content/photo/builder/photo-builder.service'
|
||||
import { BillingModule } from 'core/modules/platform/billing/billing.module'
|
||||
@@ -9,7 +10,7 @@ import { SuperAdminSettingController } from './super-admin-settings.controller'
|
||||
import { SuperAdminTenantController } from './super-admin-tenants.controller'
|
||||
|
||||
@Module({
|
||||
imports: [SystemSettingModule, BillingModule, TenantModule],
|
||||
imports: [SystemSettingModule, BillingModule, TenantModule, DatabaseModule],
|
||||
controllers: [SuperAdminSettingController, SuperAdminBuilderDebugController, SuperAdminTenantController],
|
||||
providers: [PhotoBuilderService],
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"hooks": "~/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@animate-ui": "https://animate-ui.com/r/{name}.json"
|
||||
"@animate-ui": "https://animate-ui.com/r/{name}.json",
|
||||
"@coss": "https://coss.com/ui/r/{name}.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@afilmory/hooks": "workspace:*",
|
||||
"@afilmory/ui": "workspace:*",
|
||||
"@afilmory/utils": "workspace:*",
|
||||
"@base-ui-components/react": "1.0.0-beta.6",
|
||||
"@creem_io/better-auth": "0.0.8",
|
||||
"@essentials/request-timeout": "1.3.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
|
||||
73
be/apps/dashboard/src/components/ui/tabs.tsx
Normal file
73
be/apps/dashboard/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import { clsxm as cn } from '@afilmory/utils'
|
||||
import { Tabs as TabsPrimitive } from '@base-ui-components/react/tabs'
|
||||
|
||||
type TabsVariant = 'default' | 'underline'
|
||||
|
||||
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
className={cn('flex flex-col gap-2 data-[orientation=vertical]:flex-row', className)}
|
||||
data-slot="tabs"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
variant = 'default',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: TabsPrimitive.List.Props & {
|
||||
variant?: TabsVariant
|
||||
}) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
className={cn(
|
||||
'relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground',
|
||||
'data-[orientation=vertical]:flex-col',
|
||||
variant === 'default'
|
||||
? 'rounded-lg bg-muted p-0.5 text-muted-foreground/64'
|
||||
: 'data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-trigger]:hover:bg-accent',
|
||||
className,
|
||||
)}
|
||||
data-slot="tabs-list"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TabsPrimitive.Indicator
|
||||
className={cn(
|
||||
'-translate-y-(--active-tab-bottom) absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) transition-[width,translate] duration-200 ease-in-out',
|
||||
variant === 'underline'
|
||||
? 'data-[orientation=vertical]:-translate-x-px z-10 bg-primary text-white data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=horizontal]:translate-y-px'
|
||||
: '-z-1 rounded-md bg-background shadow-sm dark:bg-accent text-white',
|
||||
)}
|
||||
data-slot="tab-indicator"
|
||||
/>
|
||||
</TabsPrimitive.List>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Tab
|
||||
className={cn(
|
||||
"flex flex-1 shrink-0 cursor-pointer items-center justify-center whitespace-nowrap rounded-md border border-transparent font-medium text-sm outline-none transition-[color,background-color,box-shadow] focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:opacity-64 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
'hover:text-muted-foreground data-selected:text-white data-active:text-white',
|
||||
'gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1.5)-1px)]',
|
||||
'data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start',
|
||||
className,
|
||||
)}
|
||||
data-slot="tabs-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
|
||||
return <TabsPrimitive.Panel className={cn('flex-1 outline-none', className)} data-slot="tabs-content" {...props} />
|
||||
}
|
||||
|
||||
export { Tabs, TabsPanel as TabsContent, TabsList, TabsPanel, TabsTab, TabsTab as TabsTrigger }
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
BuilderDebugResult,
|
||||
SuperAdminSettingsResponse,
|
||||
SuperAdminTenantListResponse,
|
||||
SuperAdminTenantPhotosResponse,
|
||||
UpdateSuperAdminSettingsPayload,
|
||||
UpdateTenantBanPayload,
|
||||
UpdateTenantPlanPayload,
|
||||
@@ -177,3 +178,10 @@ export async function runBuilderDebugTest(file: File, options?: RunBuilderDebugO
|
||||
|
||||
return camelCaseKeys<BuilderDebugResult>(finalResult)
|
||||
}
|
||||
|
||||
export async function fetchSuperAdminTenantPhotos(tenantId: string): Promise<SuperAdminTenantPhotosResponse> {
|
||||
const response = await coreApi<SuperAdminTenantPhotosResponse>(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${tenantId}/photos`, {
|
||||
method: 'GET',
|
||||
})
|
||||
return camelCaseKeys<SuperAdminTenantPhotosResponse>(response)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@afilmory/ui'
|
||||
import { Button, Modal, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@afilmory/ui'
|
||||
import { Spring } from '@afilmory/utils'
|
||||
import { RefreshCcwIcon } from 'lucide-react'
|
||||
import { m } from 'motion/react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { LinearBorderPanel } from '~/components/common/LinearBorderPanel'
|
||||
import { BILLING_USAGE_EVENT_CONFIG } from '~/modules/photos/constants'
|
||||
import type { BillingUsageEventType } from '~/modules/photos/types'
|
||||
|
||||
import { useSuperAdminTenantsQuery, useUpdateTenantBanMutation, useUpdateTenantPlanMutation } from '../hooks'
|
||||
import type { BillingPlanDefinition, SuperAdminTenantSummary } from '../types'
|
||||
import { TenantDetailModal } from './TenantDetailModal'
|
||||
import { TenantUsageCell } from './TenantUsageCell'
|
||||
|
||||
const DATE_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
|
||||
dateStyle: 'medium',
|
||||
@@ -93,25 +92,23 @@ export function SuperAdminTenantManager() {
|
||||
|
||||
return (
|
||||
<m.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={Spring.presets.smooth}>
|
||||
<LinearBorderPanel className="p-6 bg-background-secondary">
|
||||
<header className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
onClick={() => tenantsQuery.refetch()}
|
||||
disabled={tenantsQuery.isFetching}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
<span>
|
||||
{tenantsQuery.isFetching
|
||||
? t('superadmin.tenants.refresh.loading')
|
||||
: t('superadmin.tenants.refresh.button')}
|
||||
</span>
|
||||
</Button>
|
||||
</header>
|
||||
<header className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
onClick={() => tenantsQuery.refetch()}
|
||||
disabled={tenantsQuery.isFetching}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
<span>
|
||||
{tenantsQuery.isFetching ? t('superadmin.tenants.refresh.loading') : t('superadmin.tenants.refresh.button')}
|
||||
</span>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<LinearBorderPanel className="p-6 bg-background-secondary mt-4">
|
||||
{tenants.length === 0 ? (
|
||||
<p className="text-text-secondary text-sm">{t('superadmin.tenants.empty')}</p>
|
||||
) : (
|
||||
@@ -130,11 +127,11 @@ export function SuperAdminTenantManager() {
|
||||
<tbody className="divide-y divide-border/20">
|
||||
{tenants.map((tenant) => (
|
||||
<tr key={tenant.id}>
|
||||
<td className="px-3 py-3">
|
||||
<td className="px-3 py-3 align-top">
|
||||
<div className="font-medium text-text">{tenant.name}</div>
|
||||
<div className="text-text-secondary text-xs">{tenant.slug}</div>
|
||||
</td>
|
||||
<td className="px-3 py-3">
|
||||
<td className="px-3 py-3 align-top">
|
||||
<PlanSelector
|
||||
value={tenant.planId}
|
||||
plans={plans}
|
||||
@@ -143,12 +140,24 @@ export function SuperAdminTenantManager() {
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-3 align-top">
|
||||
<TenantUsageCell usageTotals={tenant.usageTotals} />
|
||||
<div
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => Modal.present(TenantDetailModal, { tenant })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
Modal.present(TenantDetailModal, { tenant })
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<TenantUsageCell usageTotals={tenant.usageTotals} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-center align-middle">
|
||||
<td className="px-3 py-3 text-center align-top">
|
||||
<StatusBadge status={tenant.status} banned={tenant.banned} />
|
||||
</td>
|
||||
<td className="px-3 py-3 text-center align-middle">
|
||||
<td className="px-3 py-1 text-center align-top">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
@@ -164,7 +173,9 @@ export function SuperAdminTenantManager() {
|
||||
: t('superadmin.tenants.button.ban')}
|
||||
</Button>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-text-secondary text-xs">{formatDateLabel(tenant.createdAt)}</td>
|
||||
<td className="px-3 py-3 align-top text-text-secondary text-xs">
|
||||
{formatDateLabel(tenant.createdAt)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -207,62 +218,6 @@ function PlanSelector({
|
||||
)
|
||||
}
|
||||
|
||||
function TenantUsageCell({ usageTotals }: { usageTotals: SuperAdminTenantSummary['usageTotals'] }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const locale = i18n.language ?? i18n.resolvedLanguage ?? 'en'
|
||||
const numberFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
}),
|
||||
[locale],
|
||||
)
|
||||
|
||||
const entries = useMemo(() => {
|
||||
const totalsMap = new Map<BillingUsageEventType, { total: number; unit: 'count' | 'byte' }>()
|
||||
usageTotals?.forEach((entry) => {
|
||||
totalsMap.set(entry.eventType as BillingUsageEventType, {
|
||||
total: entry.totalQuantity ?? 0,
|
||||
unit: entry.unit,
|
||||
})
|
||||
})
|
||||
|
||||
return (Object.keys(BILLING_USAGE_EVENT_CONFIG) as BillingUsageEventType[]).map((eventType) => {
|
||||
const config = BILLING_USAGE_EVENT_CONFIG[eventType]
|
||||
const usage = totalsMap.get(eventType)
|
||||
return {
|
||||
eventType,
|
||||
label: t(config.labelKey),
|
||||
tone: config.tone,
|
||||
total: usage?.total ?? 0,
|
||||
unit: usage?.unit ?? 'count',
|
||||
}
|
||||
})
|
||||
}, [usageTotals, t])
|
||||
|
||||
const activeEntries = entries.filter((entry) => entry.total > 0)
|
||||
|
||||
if (activeEntries.length === 0) {
|
||||
return <p className="text-text-tertiary text-xs">{t('superadmin.tenants.usage.empty')}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{activeEntries.map((entry) => (
|
||||
<UsageBadge
|
||||
key={`${entry.eventType}`}
|
||||
label={entry.label}
|
||||
tone={entry.tone}
|
||||
value={entry.total}
|
||||
unit={entry.unit}
|
||||
formatter={numberFormatter}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PlanDescription({ plan }: { plan: BillingPlanDefinition | undefined }) {
|
||||
if (!plan) {
|
||||
return null
|
||||
@@ -270,56 +225,6 @@ function PlanDescription({ plan }: { plan: BillingPlanDefinition | undefined })
|
||||
return <p className="text-text-tertiary text-xs">{plan.description}</p>
|
||||
}
|
||||
|
||||
type UsageBadgeProps = {
|
||||
label: string
|
||||
tone: (typeof BILLING_USAGE_EVENT_CONFIG)[BillingUsageEventType]['tone']
|
||||
value: number
|
||||
unit: 'count' | 'byte'
|
||||
formatter: Intl.NumberFormat
|
||||
}
|
||||
|
||||
function UsageBadge({ label, tone, value, unit, formatter }: UsageBadgeProps) {
|
||||
const toneClass =
|
||||
tone === 'accent'
|
||||
? 'bg-emerald-500/10 text-emerald-200 border-emerald-500/30'
|
||||
: tone === 'warning'
|
||||
? 'bg-rose-500/10 text-rose-200 border-rose-500/30'
|
||||
: 'bg-fill/30 text-text-secondary border-border/40'
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium ${toneClass}`}
|
||||
>
|
||||
<span className="uppercase tracking-wide text-[10px] text-text-tertiary/80">{label}</span>
|
||||
<span className="text-xs font-semibold text-text">{formatUsageValue(value, unit, formatter)}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function formatUsageValue(value: number, unit: 'count' | 'byte', formatter: Intl.NumberFormat): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '0'
|
||||
}
|
||||
if (unit === 'byte') {
|
||||
return formatBytes(value)
|
||||
}
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let value = bytes
|
||||
let unitIndex = 0
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
function StatusBadge({ status, banned }: { status: SuperAdminTenantSummary['status']; banned: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
if (banned) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { ModalComponent } from '@afilmory/ui'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@afilmory/ui'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs'
|
||||
|
||||
import { useSuperAdminTenantPhotosQuery } from '../hooks'
|
||||
import type { SuperAdminTenantSummary } from '../types'
|
||||
import { TenantUsageCell } from './TenantUsageCell'
|
||||
|
||||
interface TenantDetailModalProps {
|
||||
tenant: SuperAdminTenantSummary
|
||||
}
|
||||
|
||||
export const TenantDetailModal: ModalComponent<TenantDetailModalProps> = ({ tenant }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const photoCount = tenant.usageTotals?.find((u) => u.eventType === 'photo.asset.created')?.totalQuantity ?? 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 h-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{tenant.name}</DialogTitle>
|
||||
<DialogDescription>{tenant.slug}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="overview" className="w-full flex-1 flex flex-col min-h-0">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">{t('superadmin.tenants.modal.tab.overview')}</TabsTrigger>
|
||||
<TabsTrigger value="photos">{t('superadmin.tenants.modal.tab.photos')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6 px-2 mt-4 flex-1 overflow-y-auto">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-3">{t('superadmin.tenants.table.usage')}</h3>
|
||||
<TenantUsageCell usageTotals={tenant.usageTotals} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-3">
|
||||
{t('superadmin.tenants.modal.overview.details')}
|
||||
</h3>
|
||||
<dl className="divide-y border-t border-b">
|
||||
<div className="py-3 flex justify-between text-sm">
|
||||
<dt className="text-muted-foreground">{t('superadmin.tenants.table.status')}</dt>
|
||||
<dd className="font-medium">{tenant.status}</dd>
|
||||
</div>
|
||||
<div className="py-3 flex justify-between text-sm">
|
||||
<dt className="text-muted-foreground">{t('superadmin.tenants.table.plan')}</dt>
|
||||
<dd className="font-medium">{tenant.planId}</dd>
|
||||
</div>
|
||||
<div className="py-3 flex justify-between text-sm">
|
||||
<dt className="text-muted-foreground">{t('superadmin.tenants.modal.overview.photos')}</dt>
|
||||
<dd className="font-medium">{photoCount}</dd>
|
||||
</div>
|
||||
<div className="py-3 flex justify-between text-sm">
|
||||
<dt className="text-muted-foreground">{t('superadmin.tenants.table.created')}</dt>
|
||||
<dd className="font-medium">{new Date(tenant.createdAt).toLocaleDateString()}</dd>
|
||||
</div>
|
||||
<div className="py-3 flex justify-between text-sm">
|
||||
<dt className="text-muted-foreground">{t('superadmin.tenants.table.ban')}</dt>
|
||||
<dd className="font-medium">{tenant.banned ? 'Yes' : 'No'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="photos" className="mt-4 flex-1 overflow-y-auto min-h-0">
|
||||
<TenantPhotosTab tenantId={tenant.id} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TenantDetailModal.contentClassName = 'max-w-4xl h-[80vh]'
|
||||
|
||||
function TenantPhotosTab({ tenantId }: { tenantId: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading, isError } = useSuperAdminTenantPhotosQuery(tenantId)
|
||||
|
||||
if (isLoading) {
|
||||
return <p className="text-sm text-muted-foreground">{t('superadmin.tenants.modal.photos.loading')}</p>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <p className="text-sm text-red-500">{t('superadmin.tenants.modal.photos.error')}</p>
|
||||
}
|
||||
|
||||
const photos = data?.photos ?? []
|
||||
|
||||
if (photos.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">{t('superadmin.tenants.modal.photos.empty')}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-border/40 text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground w-16">
|
||||
{t('superadmin.tenants.modal.photos.table.preview')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||
{t('superadmin.tenants.modal.photos.table.name')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground w-24">
|
||||
{t('superadmin.tenants.modal.photos.table.size')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground w-40">
|
||||
{t('superadmin.tenants.modal.photos.table.created')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/20">
|
||||
{photos.map((photo) => (
|
||||
<tr key={photo.id}>
|
||||
<td className="px-4 py-2">
|
||||
<div className="w-10 h-10 bg-muted rounded overflow-hidden">
|
||||
{/* We might not have a public URL if it's not synced or private, but let's try */}
|
||||
{photo.publicUrl && (
|
||||
<img src={photo.publicUrl} alt="" className="w-full h-full object-cover" loading="lazy" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 font-medium truncate max-w-[200px]" title={photo.storageKey}>
|
||||
{photo.storageKey.split('/').pop()}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{formatBytes(photo.size ?? 0)}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{new Date(photo.createdAt).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let value = bytes
|
||||
let unitIndex = 0
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BILLING_USAGE_EVENT_CONFIG } from '~/modules/photos/constants'
|
||||
import type { BillingUsageEventType } from '~/modules/photos/types'
|
||||
|
||||
import type { SuperAdminTenantSummary } from '../types'
|
||||
|
||||
export function TenantUsageCell({ usageTotals }: { usageTotals: SuperAdminTenantSummary['usageTotals'] }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const locale = i18n.language ?? i18n.resolvedLanguage ?? 'en'
|
||||
const numberFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
}),
|
||||
[locale],
|
||||
)
|
||||
|
||||
const entries = useMemo(() => {
|
||||
const totalsMap = new Map<BillingUsageEventType, { total: number; unit: 'count' | 'byte' }>()
|
||||
usageTotals?.forEach((entry) => {
|
||||
totalsMap.set(entry.eventType as BillingUsageEventType, {
|
||||
total: entry.totalQuantity ?? 0,
|
||||
unit: entry.unit,
|
||||
})
|
||||
})
|
||||
|
||||
return (Object.keys(BILLING_USAGE_EVENT_CONFIG) as BillingUsageEventType[]).map((eventType) => {
|
||||
const config = BILLING_USAGE_EVENT_CONFIG[eventType]
|
||||
const usage = totalsMap.get(eventType)
|
||||
return {
|
||||
eventType,
|
||||
label: t(config.labelKey),
|
||||
tone: config.tone,
|
||||
total: usage?.total ?? 0,
|
||||
unit: usage?.unit ?? 'count',
|
||||
}
|
||||
})
|
||||
}, [usageTotals, t])
|
||||
|
||||
const activeEntries = entries.filter((entry) => entry.total > 0)
|
||||
|
||||
if (activeEntries.length === 0) {
|
||||
return <p className="text-text-tertiary text-xs">{t('superadmin.tenants.usage.empty')}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{activeEntries.map((entry) => (
|
||||
<UsageBadge
|
||||
key={`${entry.eventType}`}
|
||||
label={entry.label}
|
||||
tone={entry.tone}
|
||||
value={entry.total}
|
||||
unit={entry.unit}
|
||||
formatter={numberFormatter}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type UsageBadgeProps = {
|
||||
label: string
|
||||
tone: (typeof BILLING_USAGE_EVENT_CONFIG)[BillingUsageEventType]['tone']
|
||||
value: number
|
||||
unit: 'count' | 'byte'
|
||||
formatter: Intl.NumberFormat
|
||||
}
|
||||
|
||||
function UsageBadge({ label, tone, value, unit, formatter }: UsageBadgeProps) {
|
||||
const toneClass =
|
||||
tone === 'accent'
|
||||
? 'bg-emerald-500/10 text-emerald-200 border-emerald-500/30'
|
||||
: tone === 'warning'
|
||||
? 'bg-rose-500/10 text-rose-200 border-rose-500/30'
|
||||
: 'bg-fill/30 text-text-secondary border-border/40'
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium ${toneClass}`}
|
||||
>
|
||||
<span className="uppercase tracking-wide text-[10px] text-text-tertiary/80">{label}</span>
|
||||
<span className="text-xs font-semibold text-text">{formatUsageValue(value, unit, formatter)}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function formatUsageValue(value: number, unit: 'count' | 'byte', formatter: Intl.NumberFormat): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '0'
|
||||
}
|
||||
if (unit === 'byte') {
|
||||
return formatBytes(value)
|
||||
}
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let value = bytes
|
||||
let unitIndex = 0
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
import {
|
||||
fetchSuperAdminSettings,
|
||||
fetchSuperAdminTenantPhotos,
|
||||
fetchSuperAdminTenants,
|
||||
updateSuperAdminSettings,
|
||||
updateSuperAdminTenantBan,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
import type {
|
||||
SuperAdminSettingsResponse,
|
||||
SuperAdminTenantListResponse,
|
||||
SuperAdminTenantPhotosResponse,
|
||||
UpdateSuperAdminSettingsPayload,
|
||||
UpdateTenantBanPayload,
|
||||
UpdateTenantPlanPayload,
|
||||
@@ -74,3 +76,11 @@ export function useUpdateTenantBanMutation() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSuperAdminTenantPhotosQuery(tenantId: string | undefined) {
|
||||
return useQuery<SuperAdminTenantPhotosResponse>({
|
||||
queryKey: [...SUPER_ADMIN_TENANTS_QUERY_KEY, tenantId, 'photos'],
|
||||
queryFn: () => fetchSuperAdminTenantPhotos(tenantId!),
|
||||
enabled: !!tenantId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PhotoManifestItem } from '@afilmory/builder'
|
||||
|
||||
import type { BillingUsageTotalsEntry, PhotoSyncLogLevel } from '../photos/types'
|
||||
import type { BillingUsageTotalsEntry, PhotoAssetListItem, PhotoSyncLogLevel } from '../photos/types'
|
||||
import type { SchemaFormValue, UiSchema } from '../schema-form/types'
|
||||
|
||||
export type SuperAdminSettingField = string
|
||||
@@ -109,3 +109,7 @@ export interface UpdateTenantBanPayload {
|
||||
tenantId: string
|
||||
banned: boolean
|
||||
}
|
||||
|
||||
export interface SuperAdminTenantPhotosResponse {
|
||||
photos: PhotoAssetListItem[]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--destructive-foreground: var(--color-red-700);
|
||||
--info: var(--color-blue-500);
|
||||
--info-foreground: var(--color-blue-700);
|
||||
--success: var(--color-emerald-500);
|
||||
--success-foreground: var(--color-emerald-700);
|
||||
--warning: var(--color-amber-500);
|
||||
--warning-foreground: var(--color-amber-700);
|
||||
}
|
||||
|
||||
:root,
|
||||
@@ -218,6 +225,19 @@ body {
|
||||
--color-ring: var(--color-accent);
|
||||
--color-foreground: var(--color-text);
|
||||
--color-muted-foreground: var(--color-text-secondary);
|
||||
--animate-skeleton: skeleton 2s -1s infinite linear;
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-warning: var(--warning);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-info-foreground: var(--info-foreground);
|
||||
--color-info: var(--info);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
@keyframes skeleton {
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
@@ -257,3 +277,13 @@ body {
|
||||
corner-shape: squircle;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--destructive-foreground: var(--color-red-400);
|
||||
--info: var(--color-blue-500);
|
||||
--info-foreground: var(--color-blue-400);
|
||||
--success: var(--color-emerald-500);
|
||||
--success-foreground: var(--color-emerald-400);
|
||||
--warning: var(--color-amber-500);
|
||||
--warning-foreground: var(--color-amber-400);
|
||||
}
|
||||
@@ -737,6 +737,17 @@
|
||||
"superadmin.tenants.description": "Switch plans for specific tenants or suspend those that violate policies.",
|
||||
"superadmin.tenants.empty": "There are no tenants to manage right now.",
|
||||
"superadmin.tenants.error.loading": "Unable to load tenant data: {{reason}}",
|
||||
"superadmin.tenants.modal.overview.details": "Details",
|
||||
"superadmin.tenants.modal.overview.photos": "Total Photos",
|
||||
"superadmin.tenants.modal.photos.empty": "No photos found for this tenant.",
|
||||
"superadmin.tenants.modal.photos.error": "Failed to load photos.",
|
||||
"superadmin.tenants.modal.photos.loading": "Loading photos...",
|
||||
"superadmin.tenants.modal.photos.table.created": "Created",
|
||||
"superadmin.tenants.modal.photos.table.name": "Name",
|
||||
"superadmin.tenants.modal.photos.table.preview": "Preview",
|
||||
"superadmin.tenants.modal.photos.table.size": "Size",
|
||||
"superadmin.tenants.modal.tab.overview": "Overview",
|
||||
"superadmin.tenants.modal.tab.photos": "Photos",
|
||||
"superadmin.tenants.plan.placeholder": "Select a plan",
|
||||
"superadmin.tenants.refresh.button": "Refresh list",
|
||||
"superadmin.tenants.refresh.loading": "Refreshing…",
|
||||
|
||||
@@ -419,7 +419,6 @@
|
||||
"photos.sync.toasts.conflict-error-desc": "当前无法解决冲突,请稍后再试。",
|
||||
"photos.sync.toasts.conflict-resolved": "冲突已解决",
|
||||
"photos.sync.toasts.conflict-select": "请先选择至少一个冲突项。",
|
||||
"photos.sync.toasts.conflict-storage": "已将存储版本写入数据库。",
|
||||
"photos.tabs.library": "图库管理",
|
||||
"photos.tabs.storage": "素材存储",
|
||||
"photos.tabs.sync": "存储同步",
|
||||
@@ -737,6 +736,10 @@
|
||||
"superadmin.tenants.description": "为特定租户切换订阅计划或封禁违规租户。",
|
||||
"superadmin.tenants.empty": "当前没有可管理的租户。",
|
||||
"superadmin.tenants.error.loading": "无法加载租户数据:{{reason}}",
|
||||
"superadmin.tenants.modal.overview.details": "详细信息",
|
||||
"superadmin.tenants.modal.overview.photos": "照片总数",
|
||||
"superadmin.tenants.modal.tab.overview": "概览",
|
||||
"superadmin.tenants.modal.tab.photos": "照片",
|
||||
"superadmin.tenants.plan.placeholder": "选择订阅计划",
|
||||
"superadmin.tenants.refresh.button": "刷新列表",
|
||||
"superadmin.tenants.refresh.loading": "正在刷新…",
|
||||
|
||||
133
pnpm-lock.yaml
generated
133
pnpm-lock.yaml
generated
@@ -103,7 +103,7 @@ importers:
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
eslint-config-hyoban:
|
||||
specifier: 4.0.10
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
fast-glob:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
@@ -939,7 +939,7 @@ importers:
|
||||
version: 3.929.0
|
||||
'@creem_io/better-auth':
|
||||
specifier: 0.0.8
|
||||
version: 0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(creem@0.4.0)(zod@4.1.12)
|
||||
version: 0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)
|
||||
'@hono/node-server':
|
||||
specifier: ^1.19.6
|
||||
version: 1.19.6(hono@4.10.5)
|
||||
@@ -951,7 +951,7 @@ importers:
|
||||
version: 1.5.4
|
||||
better-auth:
|
||||
specifier: 1.3.34
|
||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
busboy:
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
@@ -1028,9 +1028,12 @@ importers:
|
||||
'@afilmory/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../../packages/utils
|
||||
'@base-ui-components/react':
|
||||
specifier: 1.0.0-beta.6
|
||||
version: 1.0.0-beta.6(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@creem_io/better-auth':
|
||||
specifier: 0.0.8
|
||||
version: 0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(creem@0.4.0)(zod@4.1.12)
|
||||
version: 0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)
|
||||
'@essentials/request-timeout':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
@@ -1084,7 +1087,7 @@ importers:
|
||||
version: 5.90.8(react@19.2.0)
|
||||
better-auth:
|
||||
specifier: 1.3.34
|
||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -1108,7 +1111,7 @@ importers:
|
||||
version: 10.2.0
|
||||
jotai:
|
||||
specifier: 2.15.1
|
||||
version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
||||
version: 2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
||||
lucide-react:
|
||||
specifier: 0.553.0
|
||||
version: 0.553.0(react@19.2.0)
|
||||
@@ -1138,7 +1141,7 @@ importers:
|
||||
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-scan:
|
||||
specifier: 0.4.3
|
||||
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2)
|
||||
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2)
|
||||
sonner:
|
||||
specifier: 2.0.7
|
||||
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -1199,7 +1202,7 @@ importers:
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
eslint-config-hyoban:
|
||||
specifier: 4.0.10
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
lint-staged:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6
|
||||
@@ -2360,6 +2363,27 @@ packages:
|
||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@base-ui-components/react@1.0.0-beta.6':
|
||||
resolution: {integrity: sha512-jE07DkS7JcGKGH4/hNPTabyGfEzrfDhGPM0h9exGDWwRhEJXZK9xhJ4m3fr+yPf8gpddOCMMoKyV4xITJKrSOA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17 || ^18 || ^19
|
||||
react: ^17 || ^18 || ^19
|
||||
react-dom: ^17 || ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@base-ui-components/utils@0.2.0':
|
||||
resolution: {integrity: sha512-uLLsLpYxPVq/QNsrfHwXuI+uiCr8+P/SLtTgrA4hL5b4UmWo3dnhwo38edfu+gCgfblrJveDj1D4RxEvDc7RXw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17 || ^18 || ^19
|
||||
react: ^17 || ^18 || ^19
|
||||
react-dom: ^17 || ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10852,6 +10876,9 @@ packages:
|
||||
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
||||
engines: {node: '>=0.10.5'}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
@@ -11377,6 +11404,9 @@ packages:
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
tabbable@6.3.0:
|
||||
resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
|
||||
|
||||
tailwind-api-utils@1.0.3:
|
||||
resolution: {integrity: sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ==}
|
||||
peerDependencies:
|
||||
@@ -13654,6 +13684,31 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@base-ui-components/react@1.0.0-beta.6(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@base-ui-components/utils': 0.2.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@floating-ui/utils': 0.2.10
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
reselect: 5.1.1
|
||||
tabbable: 6.3.0
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.3
|
||||
|
||||
'@base-ui-components/utils@0.2.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@floating-ui/utils': 0.2.10
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
reselect: 5.1.1
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.3
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)':
|
||||
@@ -13774,9 +13829,9 @@ snapshots:
|
||||
conventional-commits-filter: 5.0.0
|
||||
conventional-commits-parser: 6.2.1
|
||||
|
||||
'@creem_io/better-auth@0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(creem@0.4.0)(zod@4.1.12)':
|
||||
'@creem_io/better-auth@0.0.8(patch_hash=9eddcbca5430bb73593d5b26bcc1e85830a89b543ed577340323b869bdce0154)(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)':
|
||||
dependencies:
|
||||
better-auth: 1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
better-auth: 1.3.34(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
creem: 0.4.0
|
||||
zod: 4.1.12
|
||||
|
||||
@@ -18373,7 +18428,7 @@ snapshots:
|
||||
|
||||
batch-cluster@15.0.1: {}
|
||||
|
||||
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||
'@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||
@@ -18390,7 +18445,7 @@ snapshots:
|
||||
nanostores: 1.0.1
|
||||
zod: 4.1.12
|
||||
optionalDependencies:
|
||||
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
@@ -20949,6 +21004,13 @@ snapshots:
|
||||
|
||||
jose@6.1.0: {}
|
||||
|
||||
jotai@2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/template': 7.27.2
|
||||
'@types/react': 19.2.3
|
||||
react: 19.2.0
|
||||
|
||||
jotai@2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -22017,6 +22079,31 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.1
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001752
|
||||
postcss: 8.4.31
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.1
|
||||
'@next/swc-darwin-x64': 16.0.1
|
||||
'@next/swc-linux-arm64-gnu': 16.0.1
|
||||
'@next/swc-linux-arm64-musl': 16.0.1
|
||||
'@next/swc-linux-x64-gnu': 16.0.1
|
||||
'@next/swc-linux-x64-musl': 16.0.1
|
||||
'@next/swc-win32-arm64-msvc': 16.0.1
|
||||
'@next/swc-win32-x64-msvc': 16.0.1
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
optional: true
|
||||
|
||||
next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.1
|
||||
@@ -23158,7 +23245,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/generator': 7.28.3
|
||||
@@ -23167,7 +23254,7 @@ snapshots:
|
||||
'@clack/prompts': 0.8.2
|
||||
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@preact/signals': 1.3.2(preact@10.27.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.2)
|
||||
'@types/node': 20.19.25
|
||||
bippy: 0.3.27(@types/react@19.2.3)(react@19.2.0)
|
||||
esbuild: 0.25.11
|
||||
@@ -23180,7 +23267,7 @@ snapshots:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
tsx: 4.20.6
|
||||
optionalDependencies:
|
||||
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
unplugin: 2.1.0
|
||||
@@ -23189,7 +23276,7 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2):
|
||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/generator': 7.28.3
|
||||
@@ -23198,7 +23285,7 @@ snapshots:
|
||||
'@clack/prompts': 0.8.2
|
||||
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@preact/signals': 1.3.2(preact@10.27.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||
'@types/node': 20.19.25
|
||||
bippy: 0.3.27(@types/react@19.2.3)(react@19.2.0)
|
||||
esbuild: 0.25.11
|
||||
@@ -23515,6 +23602,8 @@ snapshots:
|
||||
|
||||
requireindex@1.2.0: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -24082,6 +24171,14 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
optional: true
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
@@ -24132,6 +24229,8 @@ snapshots:
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tabbable@6.3.0: {}
|
||||
|
||||
tailwind-api-utils@1.0.3(tailwindcss@4.1.17):
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.3
|
||||
|
||||
Reference in New Issue
Block a user