Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).

Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
Codex Agent
2025-11-01 19:50:17 +01:00
parent 2c14493604
commit 79b209de9a
55 changed files with 3348 additions and 462 deletions

View File

@@ -1,5 +1,6 @@
import { authorizedFetch } from './auth/tokens';
import { ApiError } from './lib/apiError';
import type { EventLimitSummary } from './lib/limitWarnings';
import i18n from './i18n';
type JsonValue = Record<string, unknown>;
@@ -62,6 +63,7 @@ export type TenantEvent = {
purchased_at: string | null;
expires_at: string | null;
} | null;
limits?: EventLimitSummary | null;
[key: string]: unknown;
};
@@ -128,6 +130,13 @@ export type TenantPackageSummary = {
package_limits: Record<string, unknown> | null;
};
export type NotificationPreferences = Record<string, boolean>;
export type NotificationPreferencesMeta = {
credit_warning_sent_at?: string | null;
credit_warning_threshold?: number | null;
};
export type CreditBalance = {
balance: number;
free_event_granted_at?: string | null;
@@ -490,6 +499,7 @@ function normalizeEvent(event: JsonValue): TenantEvent {
engagement_mode: engagementMode,
settings,
package: event.package ?? null,
limits: (event.limits ?? null) as EventLimitSummary | null,
};
return normalized;
@@ -779,10 +789,17 @@ export async function getEventTypes(): Promise<TenantEventType[]> {
.filter((row): row is TenantEventType => Boolean(row));
}
export async function getEventPhotos(slug: string): Promise<TenantPhoto[]> {
export async function getEventPhotos(slug: string): Promise<{ photos: TenantPhoto[]; limits: EventLimitSummary | null }> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos`);
const data = await jsonOrThrow<{ data?: TenantPhoto[] }>(response, 'Failed to load photos');
return (data.data ?? []).map(normalizePhoto);
const data = await jsonOrThrow<{ data?: TenantPhoto[]; limits?: EventLimitSummary | null }>(
response,
'Failed to load photos'
);
return {
photos: (data.data ?? []).map(normalizePhoto),
limits: (data.limits ?? null) as EventLimitSummary | null,
};
}
export async function featurePhoto(slug: string, id: number): Promise<TenantPhoto> {
@@ -1049,6 +1066,56 @@ export async function getTenantPackagesOverview(): Promise<{
return { packages, activePackage };
}
export type NotificationPreferenceResponse = {
defaults: NotificationPreferences;
preferences: NotificationPreferences;
overrides: NotificationPreferences | null;
meta: NotificationPreferencesMeta | null;
};
export async function getNotificationPreferences(): Promise<NotificationPreferenceResponse> {
const response = await authorizedFetch('/api/v1/tenant/settings/notifications');
const payload = await jsonOrThrow<{ data?: { defaults?: NotificationPreferences; preferences?: NotificationPreferences; overrides?: NotificationPreferences; meta?: NotificationPreferencesMeta } }>(
response,
'Failed to load notification preferences'
);
const data = payload.data ?? {};
return {
defaults: data.defaults ?? {},
preferences: data.preferences ?? {},
overrides: data.overrides ?? null,
meta: data.meta ?? null,
};
}
export async function updateNotificationPreferences(
preferences: NotificationPreferences
): Promise<NotificationPreferenceResponse> {
const response = await authorizedFetch('/api/v1/tenant/settings/notifications', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ preferences }),
});
const payload = await jsonOrThrow<{ data?: { preferences?: NotificationPreferences; overrides?: NotificationPreferences; meta?: NotificationPreferencesMeta } }>(
response,
'Failed to update notification preferences'
);
const data = payload.data ?? {};
return {
defaults: {},
preferences: data.preferences ?? preferences,
overrides: data.overrides ?? null,
meta: data.meta ?? null,
};
}
export async function getTenantPaddleTransactions(cursor?: string): Promise<{
data: PaddleTransactionSummary[];
nextCursor: string | null;