import type { AppRecord, ReleaseRecord, SourceRecord } from "@/packages/core/src/index"; export type DashboardSource = { source: SourceRecord; apps: AppRecord[]; releases: ReleaseRecord[]; }; export type DashboardData = { workspace: { name: string; slug: string; }; sources: DashboardSource[]; usage: { storageBytes: number; sourceCount: number; }; limits: { maxSources: number; maxAppsPerSource: number; maxStorageBytes: number; maxUploadBytes: number; }; }; export type Notice = { tone: "success" | "error" | "info"; text: string; }; export type Visibility = SourceRecord["visibility"]; export const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:4000"; export const AUTH_MODE = process.env.NEXT_PUBLIC_AUTH_MODE || "demo"; export const DEMO_HEADERS = { "x-demo-user": "demo@altdock.local" }; export async function api(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); headers.set("accept", "application/json"); if (init.body) headers.set("content-type", "application/json"); Object.entries(DEMO_HEADERS).forEach(([key, value]) => headers.set(key, value)); const response = await fetch(`${API_BASE}${path}`, { ...init, headers, credentials: "include" }); const body = (await response.json().catch(() => ({}))) as T & { error?: string; detail?: string }; if (!response.ok) throw new Error(body.detail || body.error || `Request failed (${response.status})`); return body; } export function formatBytes(value: number) { if (!value) return "0 B"; const units = ["B", "KB", "MB", "GB"]; const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1); return `${(value / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`; } export function formatDate(value: string) { return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric" }).format(new Date(value)); } export function formatDateTime(value: string) { return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value)); } export function statusLabel(status: ReleaseRecord["status"]) { return { uploaded: "アップロード済み", processing: "処理中", ready: "公開準備完了", rejected: "要修正", published: "公開中", archived: "アーカイブ", }[status]; } export function statusTone(status: ReleaseRecord["status"]) { return { uploaded: "neutral", processing: "info", ready: "warning", rejected: "danger", published: "success", archived: "neutral", }[status] as "neutral" | "info" | "warning" | "danger" | "success"; } export function visibilityLabel(visibility: Visibility) { return { draft: "下書き", unlisted: "限定公開", public: "公開" }[visibility]; } export function visibilityTone(visibility: Visibility) { return { draft: "neutral", unlisted: "warning", public: "success" }[visibility] as "neutral" | "warning" | "success"; } export function flattenSources(dashboard: DashboardData) { return dashboard.sources; } export function flattenApps(dashboard: DashboardData) { return dashboard.sources.flatMap((entry) => entry.apps.map((app) => ({ app, source: entry.source, releases: entry.releases.filter((release) => release.appId === app.id) }))); } export function flattenReleases(dashboard: DashboardData) { return dashboard.sources.flatMap((entry) => entry.releases.map((release) => ({ release, source: entry.source, app: entry.apps.find((app) => app.id === release.appId) }))); }