improve system
This commit is contained in:
parent
abbd6ec9be
commit
214ac28926
|
|
@ -19,6 +19,7 @@ import {
|
|||
useAdminSystem,
|
||||
useAdminUploads,
|
||||
} from "~/shared/services/admin";
|
||||
import type { AdminUploadsContent, AdminUploadsContentFlags } from "~/shared/services/admin";
|
||||
import { clearAdminAuth, getAdminAuthSnapshot } from "~/shared/libs/admin-auth";
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat("ru-RU");
|
||||
|
|
@ -151,6 +152,20 @@ type SyncLimitsFormValues = {
|
|||
disk_low_watermark_pct: number;
|
||||
};
|
||||
|
||||
type UploadCategory = "issues" | "processing" | "ready" | "unindexed";
|
||||
type UploadFilter = "all" | UploadCategory;
|
||||
|
||||
type UploadDecoration = {
|
||||
item: AdminUploadsContent;
|
||||
categories: Set<UploadCategory>;
|
||||
searchText: string;
|
||||
hasIssue: boolean;
|
||||
isReady: boolean;
|
||||
isProcessing: boolean;
|
||||
isUnindexed: boolean;
|
||||
flags: AdminUploadsContentFlags | null;
|
||||
};
|
||||
|
||||
const initialAuthState = getAdminAuthSnapshot().token ? "checking" : "unauthorized";
|
||||
|
||||
export const AdminPage = () => {
|
||||
|
|
@ -159,6 +174,8 @@ export const AdminPage = () => {
|
|||
const [activeSection, setActiveSection] = useState<(typeof ADMIN_SECTIONS)[number]["id"]>("overview");
|
||||
const [token, setToken] = useState("");
|
||||
const [flash, setFlash] = useState<FlashMessage | null>(null);
|
||||
const [uploadsFilter, setUploadsFilter] = useState<UploadFilter>("all");
|
||||
const [uploadsSearch, setUploadsSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!flash) {
|
||||
|
|
@ -194,15 +211,28 @@ export const AdminPage = () => {
|
|||
});
|
||||
|
||||
const isDataEnabled = authState === "authorized";
|
||||
const normalizedUploadsSearch = uploadsSearch.trim();
|
||||
const isUploadsFilterActive = uploadsFilter !== "all";
|
||||
const hasUploadsSearch = normalizedUploadsSearch.length > 0;
|
||||
const uploadsScanLimit = isUploadsFilterActive || hasUploadsSearch ? 200 : 100;
|
||||
|
||||
const storageQuery = useAdminStorage({
|
||||
enabled: isDataEnabled,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
const uploadsQuery = useAdminUploads({
|
||||
enabled: isDataEnabled,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
const uploadsQuery = useAdminUploads(
|
||||
{
|
||||
filter: isUploadsFilterActive ? uploadsFilter : undefined,
|
||||
search: hasUploadsSearch ? normalizedUploadsSearch : undefined,
|
||||
limit: 40,
|
||||
scan: uploadsScanLimit,
|
||||
},
|
||||
{
|
||||
enabled: isDataEnabled,
|
||||
refetchInterval: 30_000,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
const systemQuery = useAdminSystem({
|
||||
enabled: isDataEnabled,
|
||||
refetchInterval: 60_000,
|
||||
|
|
@ -509,8 +539,8 @@ export const AdminPage = () => {
|
|||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-slate-100">IPFS</h3>
|
||||
<div className="grid gap-3 rounded-xl bg-slate-950/60 p-4 ring-1 ring-slate-800">
|
||||
<InfoRow label="ID">{(ipfs.identity as Record<string, unknown>)?.ID ?? "—"}</InfoRow>
|
||||
<InfoRow label="Agent">{(ipfs.identity as Record<string, unknown>)?.AgentVersion ?? "—"}</InfoRow>
|
||||
<InfoRow label="ID">{formatUnknown((ipfs.identity as Record<string, unknown>)?.ID)}</InfoRow>
|
||||
<InfoRow label="Agent">{formatUnknown((ipfs.identity as Record<string, unknown>)?.AgentVersion)}</InfoRow>
|
||||
<InfoRow label="Peers">{numberFormatter.format(Number((ipfs.bitswap as Record<string, unknown>)?.Peers ?? 0))}</InfoRow>
|
||||
<InfoRow label="Repo size">{formatBytes(Number((ipfs.repo as Record<string, unknown>)?.RepoSize ?? 0))}</InfoRow>
|
||||
<InfoRow label="Storage max">{formatBytes(Number((ipfs.repo as Record<string, unknown>)?.StorageMax ?? 0))}</InfoRow>
|
||||
|
|
@ -615,17 +645,437 @@ export const AdminPage = () => {
|
|||
};
|
||||
|
||||
const renderUploads = () => {
|
||||
if (!uploadsQuery.data) {
|
||||
return uploadsQuery.isLoading ? (
|
||||
<Section id="uploads" title="Загрузки" description="Статистика сессий">Загрузка…</Section>
|
||||
) : null;
|
||||
if (uploadsQuery.isLoading && !uploadsQuery.data) {
|
||||
return <Section id="uploads" title="Загрузки" description="Мониторинг загрузок и конвертации">Загрузка…</Section>;
|
||||
}
|
||||
const { states, total, recent } = uploadsQuery.data;
|
||||
if (!uploadsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { states, total, recent, contents = [], matching_total: matchingTotalRaw, category_totals: categoryTotalsRaw } = uploadsQuery.data;
|
||||
|
||||
const categoryTotals = categoryTotalsRaw ?? {};
|
||||
const categoryCounts: Record<UploadCategory, number> = {
|
||||
issues: categoryTotals.issues ?? 0,
|
||||
processing: categoryTotals.processing ?? 0,
|
||||
ready: categoryTotals.ready ?? 0,
|
||||
unindexed: categoryTotals.unindexed ?? 0,
|
||||
};
|
||||
|
||||
const computeFlags = (item: AdminUploadsContent): AdminUploadsContentFlags => {
|
||||
const normalize = (value: string | null | undefined) => (value ?? '').toLowerCase();
|
||||
const uploadState = normalize(item.status.upload_state);
|
||||
const conversionState = normalize(item.status.conversion_state);
|
||||
const ipfsState = normalize(item.status.ipfs_state);
|
||||
const pinState = normalize(item.ipfs?.pin_state);
|
||||
const derivativeStates = item.derivatives.map((derivative) => normalize(derivative.status));
|
||||
const statusValues = [uploadState, conversionState, ipfsState, pinState, ...derivativeStates];
|
||||
|
||||
const hasIssue =
|
||||
statusValues.some((value) => value.includes('fail') || value.includes('error') || value.includes('timeout')) ||
|
||||
item.upload_history.some((event) => Boolean(event.error)) ||
|
||||
item.derivatives.some((derivative) => Boolean(derivative.error)) ||
|
||||
Boolean(item.ipfs?.pin_error);
|
||||
|
||||
const isOnchainIndexed = item.status.onchain?.indexed ?? false;
|
||||
const isUnindexed = !isOnchainIndexed;
|
||||
|
||||
const conversionDone =
|
||||
conversionState.includes('converted') ||
|
||||
conversionState.includes('ready') ||
|
||||
derivativeStates.some((state) => state.includes('ready') || state.includes('converted') || state.includes('complete'));
|
||||
|
||||
const ipfsDone =
|
||||
ipfsState.includes('pinned') ||
|
||||
ipfsState.includes('ready') ||
|
||||
pinState.includes('pinned') ||
|
||||
pinState.includes('ready');
|
||||
|
||||
const isReady = !hasIssue && conversionDone && ipfsDone && isOnchainIndexed;
|
||||
|
||||
const hasProcessingKeywords =
|
||||
uploadState.includes('pending') ||
|
||||
uploadState.includes('process') ||
|
||||
uploadState.includes('queue') ||
|
||||
uploadState.includes('upload') ||
|
||||
conversionState.includes('pending') ||
|
||||
conversionState.includes('process') ||
|
||||
conversionState.includes('queue') ||
|
||||
conversionState.includes('convert') ||
|
||||
ipfsState.includes('pin') ||
|
||||
ipfsState.includes('sync') ||
|
||||
pinState.includes('pin') ||
|
||||
pinState.includes('sync') ||
|
||||
derivativeStates.some((state) => state.includes('pending') || state.includes('process') || state.includes('queue'));
|
||||
|
||||
const isProcessingCandidate = !isReady && !hasIssue && hasProcessingKeywords;
|
||||
const isProcessing = isProcessingCandidate || (!isReady && !hasIssue && !isProcessingCandidate);
|
||||
|
||||
return {
|
||||
issues: hasIssue,
|
||||
processing: isProcessing,
|
||||
ready: isReady,
|
||||
unindexed: isUnindexed,
|
||||
};
|
||||
};
|
||||
|
||||
const classifyUpload = (item: AdminUploadsContent): UploadDecoration => {
|
||||
const flags = item.flags ?? computeFlags(item);
|
||||
const categories = new Set<UploadCategory>();
|
||||
if (flags.issues) {
|
||||
categories.add('issues');
|
||||
}
|
||||
if (flags.processing) {
|
||||
categories.add('processing');
|
||||
}
|
||||
if (flags.ready) {
|
||||
categories.add('ready');
|
||||
}
|
||||
if (flags.unindexed) {
|
||||
categories.add('unindexed');
|
||||
}
|
||||
if (categories.size === 0) {
|
||||
if (!(item.status.onchain?.indexed ?? false)) {
|
||||
categories.add('unindexed');
|
||||
}
|
||||
categories.add('processing');
|
||||
}
|
||||
|
||||
const searchParts: Array<string | number | null | undefined> = [
|
||||
item.title,
|
||||
item.description,
|
||||
item.encrypted_cid,
|
||||
item.metadata_cid,
|
||||
item.content_hash,
|
||||
item.status.onchain?.item_address,
|
||||
item.stored?.owner_address,
|
||||
];
|
||||
if (item.stored?.user) {
|
||||
const user = item.stored.user;
|
||||
searchParts.push(user.id, user.telegram_id, user.username, user.first_name, user.last_name);
|
||||
}
|
||||
const searchText = searchParts
|
||||
.filter((value) => value !== null && value !== undefined && `${value}`.length > 0)
|
||||
.map((value) => `${value}`.toLowerCase())
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
item,
|
||||
categories,
|
||||
searchText,
|
||||
hasIssue: Boolean(flags.issues),
|
||||
isReady: Boolean(flags.ready),
|
||||
isProcessing: Boolean(flags.processing),
|
||||
isUnindexed: Boolean(flags.unindexed),
|
||||
flags: item.flags ?? flags,
|
||||
};
|
||||
};
|
||||
|
||||
const decoratedContents = contents.map((item) => classifyUpload(item));
|
||||
|
||||
const searchNeedle = normalizedUploadsSearch.toLowerCase();
|
||||
const filteredEntries = decoratedContents.filter((entry) => {
|
||||
if (uploadsFilter !== 'all' && !entry.categories.has(uploadsFilter)) {
|
||||
return false;
|
||||
}
|
||||
if (searchNeedle && !entry.searchText.includes(searchNeedle)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const filteredContents = filteredEntries.map((entry) => entry.item);
|
||||
const totalMatches = typeof matchingTotalRaw === 'number' ? matchingTotalRaw : filteredEntries.length;
|
||||
const filtersActive = isUploadsFilterActive || hasUploadsSearch;
|
||||
const noMatches = totalMatches === 0;
|
||||
const issueEntries = filteredEntries.filter((entry) => entry.categories.has('issues'));
|
||||
const filterOptions: Array<{ id: UploadFilter; label: string; count: number }> = [
|
||||
{ id: 'all', label: 'Все', count: totalMatches },
|
||||
{ id: 'issues', label: 'Ошибки', count: categoryCounts.issues },
|
||||
{ id: 'processing', label: 'В обработке', count: categoryCounts.processing },
|
||||
{ id: 'ready', label: 'Готово', count: categoryCounts.ready },
|
||||
{ id: 'unindexed', label: 'Без on-chain', count: categoryCounts.unindexed },
|
||||
];
|
||||
const handleResetFilters = () => {
|
||||
setUploadsFilter('all');
|
||||
setUploadsSearch('');
|
||||
};
|
||||
const getProblemMessages = (item: AdminUploadsContent): string[] => {
|
||||
const messages: string[] = [];
|
||||
for (let index = item.upload_history.length - 1; index >= 0; index -= 1) {
|
||||
const event = item.upload_history[index];
|
||||
if (event.error) {
|
||||
messages.push(`Загрузка: ${event.error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const derivativeError = item.derivatives.find((derivative) => Boolean(derivative.error));
|
||||
if (derivativeError?.error) {
|
||||
messages.push(`Дериватив ${derivativeError.kind}: ${derivativeError.error}`);
|
||||
}
|
||||
if (item.ipfs?.pin_error) {
|
||||
messages.push(`IPFS: ${item.ipfs.pin_error}`);
|
||||
}
|
||||
const conversionState = item.status.conversion_state;
|
||||
if (!messages.length && conversionState && conversionState.toLowerCase().includes('fail')) {
|
||||
messages.push(`Конверсия: ${conversionState}`);
|
||||
}
|
||||
const uploadState = item.status.upload_state;
|
||||
if (!messages.length && uploadState && uploadState.toLowerCase().includes('fail')) {
|
||||
messages.push(`Загрузка: ${uploadState}`);
|
||||
}
|
||||
if (!messages.length) {
|
||||
messages.push('Причина не указана. Проверьте логи.');
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
||||
const toneByState = (state: string | null | undefined): BadgeTone => {
|
||||
const normalized = (state ?? '').toLowerCase();
|
||||
if (!normalized) {
|
||||
return 'neutral';
|
||||
}
|
||||
if (normalized.includes('fail') || normalized.includes('error') || normalized.includes('timeout')) {
|
||||
return 'danger';
|
||||
}
|
||||
if (
|
||||
normalized.includes('process') ||
|
||||
normalized.includes('pending') ||
|
||||
normalized.includes('pin') ||
|
||||
normalized.includes('upload') ||
|
||||
normalized.includes('queue')
|
||||
) {
|
||||
return 'warn';
|
||||
}
|
||||
if (
|
||||
normalized.includes('ready') ||
|
||||
normalized.includes('pinned') ||
|
||||
normalized.includes('converted') ||
|
||||
normalized.includes('indexed')
|
||||
) {
|
||||
return 'success';
|
||||
}
|
||||
return 'neutral';
|
||||
};
|
||||
|
||||
const renderContentCard = (item: AdminUploadsContent) => {
|
||||
const statusBadges = [
|
||||
{ label: 'Загрузка', value: item.status.upload_state },
|
||||
{ label: 'Конверсия', value: item.status.conversion_state },
|
||||
{ label: 'IPFS', value: item.status.ipfs_state },
|
||||
{
|
||||
label: 'On-chain',
|
||||
value: item.status.onchain?.indexed ? `Индекс ${item.status.onchain.onchain_index ?? '≥8'}` : 'Локально',
|
||||
toneOverride: item.status.onchain?.indexed ? ('success' as BadgeTone) : ('neutral' as BadgeTone),
|
||||
},
|
||||
];
|
||||
|
||||
const actionButtons: Array<{ label: string; href: string; download?: boolean }> = [];
|
||||
if (item.links.web_view) {
|
||||
actionButtons.push({ label: 'Открыть', href: item.links.web_view });
|
||||
}
|
||||
if (item.links.api_view) {
|
||||
actionButtons.push({ label: 'API', href: item.links.api_view });
|
||||
}
|
||||
if (item.links.download_primary) {
|
||||
actionButtons.push({ label: 'Скачать', href: item.links.download_primary, download: true });
|
||||
}
|
||||
|
||||
const derivativeDownloads = item.links.download_derivatives.filter(
|
||||
(entry, index, arr) => entry.url && arr.findIndex((comp) => comp.url === entry.url) === index,
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={item.encrypted_cid} className="rounded-2xl bg-slate-950/60 p-5 shadow-inner shadow-black/40 ring-1 ring-slate-800">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold text-slate-100">
|
||||
{item.title || item.encrypted_cid.slice(0, 12)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
{item.content_type} · {formatBytes(item.size.encrypted ?? 0)} enc · {formatBytes(item.size.plain ?? 0)} plain
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{statusBadges.map((badge) => (
|
||||
<Badge key={`${item.encrypted_cid}-${badge.label}`} tone={badge.toneOverride ?? toneByState(badge.value)}>
|
||||
{badge.label}: {badge.value ?? '—'}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
Обновлено {formatDate(item.updated_at)} · Создано {formatDate(item.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
{actionButtons.length > 0 ? (
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{actionButtons.map((button) => (
|
||||
<a
|
||||
key={`${item.encrypted_cid}-${button.label}`}
|
||||
href={button.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download={button.download}
|
||||
className="inline-flex items-center justify-center rounded-lg bg-slate-800 px-3 py-2 text-xs font-semibold text-slate-200 ring-1 ring-slate-700 transition hover:bg-slate-700"
|
||||
>
|
||||
{button.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Идентификаторы</h4>
|
||||
<dl className="mt-2 space-y-1 text-xs text-slate-300">
|
||||
<InfoRow label="Encrypted CID"><span className="break-all font-mono">{item.encrypted_cid}</span></InfoRow>
|
||||
<InfoRow label="Metadata CID"><span className="break-all font-mono">{item.metadata_cid ?? '—'}</span></InfoRow>
|
||||
<InfoRow label="Content hash"><span className="break-all font-mono">{item.content_hash ?? '—'}</span></InfoRow>
|
||||
<InfoRow label="On-chain индекс">
|
||||
{item.status.onchain?.indexed
|
||||
? `#${item.status.onchain.onchain_index ?? '≥8'} · лицензий ${item.status.onchain.license_count ?? 0}`
|
||||
: 'Не опубликован'}
|
||||
</InfoRow>
|
||||
<InfoRow label="On-chain адрес">
|
||||
<span className="break-all font-mono">{item.status.onchain?.item_address ?? '—'}</span>
|
||||
</InfoRow>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">История загрузок</h4>
|
||||
{item.upload_history.length === 0 ? (
|
||||
<p className="mt-2 text-xs text-slate-500">Нет событий.</p>
|
||||
) : (
|
||||
<ul className="mt-2 space-y-1 text-xs text-slate-300">
|
||||
{item.upload_history.map((event, index) => (
|
||||
<li key={`${item.encrypted_cid}-upload-${index}`} className="flex items-start justify-between gap-3">
|
||||
<span className="font-semibold">
|
||||
<Badge tone={toneByState(event.state)}>{event.state}</Badge>
|
||||
</span>
|
||||
<div className="flex-1 text-right">
|
||||
<div>{formatDate(event.at)}</div>
|
||||
{event.filename ? <div className="text-slate-400">{event.filename}</div> : null}
|
||||
{event.error ? <div className="text-rose-300">{event.error}</div> : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Деривативы</h4>
|
||||
{item.derivatives.length === 0 ? (
|
||||
<p className="mt-2 text-xs text-slate-500">Деривативы отсутствуют.</p>
|
||||
) : (
|
||||
<div className="mt-2 space-y-2 text-xs text-slate-300">
|
||||
{item.derivatives.map((derivative) => (
|
||||
<div key={`${item.encrypted_cid}-${derivative.kind}`} className="rounded-lg bg-slate-950/50 p-3 ring-1 ring-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-semibold text-slate-100">{derivative.kind}</div>
|
||||
<Badge tone={toneByState(derivative.status)}>{derivative.status}</Badge>
|
||||
</div>
|
||||
<div className="mt-1 grid gap-1 text-xs text-slate-400 sm:grid-cols-2">
|
||||
<span>Размер: {derivative.size_bytes ? formatBytes(derivative.size_bytes) : '—'}</span>
|
||||
<span>Обновлено: {formatDate(derivative.updated_at)}</span>
|
||||
</div>
|
||||
{derivative.error ? <div className="mt-1 text-rose-300">{derivative.error}</div> : null}
|
||||
{derivative.download_url ? (
|
||||
<a
|
||||
href={derivative.download_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-2 inline-flex items-center gap-2 text-xs font-semibold text-sky-300 hover:text-sky-200"
|
||||
>
|
||||
Скачать
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">IPFS</h4>
|
||||
{item.ipfs ? (
|
||||
<div className="mt-2 space-y-1 text-xs text-slate-300">
|
||||
<div>
|
||||
<Badge tone={toneByState(item.ipfs.pin_state)}>{item.ipfs.pin_state ?? '—'}</Badge>
|
||||
</div>
|
||||
{item.ipfs.pin_error ? <div className="text-rose-300">{item.ipfs.pin_error}</div> : null}
|
||||
<div>
|
||||
{formatBytes(item.ipfs.bytes_fetched ?? 0)} / {formatBytes(item.ipfs.bytes_total ?? 0)}
|
||||
</div>
|
||||
<div className="text-slate-500">{formatDate(item.ipfs.updated_at)}</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-xs text-slate-500">Нет информации.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Хранение</h4>
|
||||
{item.stored ? (
|
||||
<div className="mt-2 space-y-1 text-xs text-slate-300">
|
||||
<div>ID: {item.stored.stored_id ?? '—'}</div>
|
||||
<div>Владелец: {item.stored.owner_address ?? '—'}</div>
|
||||
<div>Тип: {item.stored.type ?? '—'}</div>
|
||||
{item.stored.user ? (
|
||||
<div className="text-slate-400">
|
||||
Пользователь #{item.stored.user.id} · TG {item.stored.user.telegram_id}
|
||||
</div>
|
||||
) : null}
|
||||
{item.stored.download_url ? (
|
||||
<a
|
||||
href={item.stored.download_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-xs font-semibold text-sky-300 hover:text-sky-200"
|
||||
>
|
||||
Скачать оригинал
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-xs text-slate-500">В хранилище не найден.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{derivativeDownloads.length > 0 ? (
|
||||
<div className="rounded-xl bg-slate-900/40 p-4 ring-1 ring-slate-800">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Прямые ссылки</h4>
|
||||
<ul className="mt-2 space-y-1 text-xs text-slate-300">
|
||||
{derivativeDownloads.map((entry) => (
|
||||
<li key={`${item.encrypted_cid}-link-${entry.kind}`}>
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sky-300 hover:text-sky-200"
|
||||
>
|
||||
{entry.kind} · {entry.size_bytes ? formatBytes(entry.size_bytes) : 'размер неизвестен'}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section
|
||||
id="uploads"
|
||||
title="Загрузки"
|
||||
description="Статистика по состояниям и последние сессии"
|
||||
description="Полная картина обработки контента: цепочка загрузки, конверсия, IPFS и публикация"
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -636,13 +1086,133 @@ export const AdminPage = () => {
|
|||
</button>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{filterOptions.map((option) => {
|
||||
const isActive = option.id === uploadsFilter;
|
||||
return (
|
||||
<button
|
||||
key={`upload-filter-${option.id}`}
|
||||
type="button"
|
||||
onClick={() => setUploadsFilter(option.id)}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold transition',
|
||||
isActive
|
||||
? 'border-sky-500 bg-sky-500/20 text-sky-100 shadow-inner shadow-sky-500/20'
|
||||
: 'border-slate-700 text-slate-300 hover:border-slate-500 hover:text-slate-100'
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
<span className="rounded-full bg-slate-900/60 px-2 py-0.5 text-[10px] font-semibold tracking-wide text-slate-300">
|
||||
{numberFormatter.format(option.count)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filtersActive ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetFilters}
|
||||
className="inline-flex items-center rounded-full border border-slate-700 px-3 py-1 text-xs font-semibold text-slate-300 transition hover:border-slate-500 hover:text-slate-100"
|
||||
>
|
||||
Сбросить
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative w-full md:w-72">
|
||||
<input
|
||||
type="search"
|
||||
value={uploadsSearch}
|
||||
onChange={(event) => setUploadsSearch(event.target.value)}
|
||||
placeholder="Поиск по названию, CID или адресу"
|
||||
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100 shadow-inner shadow-black/40 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500/40"
|
||||
/>
|
||||
{uploadsSearch ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUploadsSearch('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-slate-800 px-2 py-1 text-[10px] font-semibold text-slate-300 transition hover:text-slate-100"
|
||||
>
|
||||
Очистить
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs text-slate-400">
|
||||
Найдено {numberFormatter.format(filteredContents.length)} из {numberFormatter.format(totalMatches)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricCard label="Всего сессий" value={numberFormatter.format(total)} />
|
||||
{Object.entries(states).map(([state, count]) => (
|
||||
<MetricCard key={state} label={state} value={numberFormatter.format(count)} />
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricCard label="Ошибки" value={numberFormatter.format(categoryCounts.issues)} />
|
||||
<MetricCard label="В обработке" value={numberFormatter.format(categoryCounts.processing)} />
|
||||
<MetricCard label="Готово" value={numberFormatter.format(categoryCounts.ready)} />
|
||||
<MetricCard label="Без on-chain" value={numberFormatter.format(categoryCounts.unindexed)} />
|
||||
</div>
|
||||
|
||||
{issueEntries.length > 0 ? (
|
||||
<div className="mt-6 rounded-2xl border border-rose-800/40 bg-rose-900/15 p-4 shadow-inner shadow-rose-900/40">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-sm font-semibold text-rose-100">
|
||||
Проблемные загрузки · {numberFormatter.format(issueEntries.length)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUploadsFilter('issues')}
|
||||
className="inline-flex items-center rounded-full border border-rose-700 px-3 py-1 text-xs font-semibold text-rose-100 transition hover:border-rose-500 hover:text-rose-50"
|
||||
>
|
||||
Показать все ошибки
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 space-y-3">
|
||||
{issueEntries.slice(0, 5).map(({ item }) => {
|
||||
const problems = getProblemMessages(item);
|
||||
return (
|
||||
<div key={`issue-${item.encrypted_cid}`} className="rounded-xl bg-rose-900/25 p-3 ring-1 ring-rose-800/50">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-sm font-semibold text-rose-100">{item.title || `${item.encrypted_cid.slice(0, 8)}…`}</span>
|
||||
<span className="text-xs text-rose-200">{formatDate(item.updated_at)}</span>
|
||||
</div>
|
||||
<div className="mt-1 break-all text-[11px] text-rose-200">CID: {item.encrypted_cid}</div>
|
||||
<ul className="mt-2 space-y-1 text-xs text-rose-100">
|
||||
{problems.map((problem, index) => (
|
||||
<li key={`${item.encrypted_cid}-problem-${index}`}>{problem}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{issueEntries.length > 5 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUploadsFilter('issues')}
|
||||
className="text-xs font-semibold text-rose-200 transition hover:text-rose-100"
|
||||
>
|
||||
Показать оставшиеся {numberFormatter.format(issueEntries.length - 5)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 space-y-6">
|
||||
{noMatches ? (
|
||||
<p className="text-sm text-slate-400">
|
||||
{filtersActive ? 'Нет контента по выбранным фильтрам.' : 'Контент ещё не загружался.'}
|
||||
</p>
|
||||
) : (
|
||||
filteredContents.map(renderContentCard)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-slate-800 text-left text-sm">
|
||||
<thead className="bg-slate-950/70 text-xs uppercase tracking-wide text-slate-400">
|
||||
<tr>
|
||||
|
|
@ -657,9 +1227,9 @@ export const AdminPage = () => {
|
|||
{recent.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-900/50">
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-300">{item.id}</td>
|
||||
<td className="px-3 py-2">{item.filename || "—"}</td>
|
||||
<td className="px-3 py-2">{item.size_bytes ? formatBytes(item.size_bytes) : "—"}</td>
|
||||
<td className="px-3 py-2"><Badge tone={item.state === "completed" ? "success" : item.state === "failed" ? "danger" : "neutral"}>{item.state}</Badge></td>
|
||||
<td className="px-3 py-2">{item.filename || '—'}</td>
|
||||
<td className="px-3 py-2">{item.size_bytes ? formatBytes(item.size_bytes) : '—'}</td>
|
||||
<td className="px-3 py-2"><Badge tone={toneByState(item.state)}>{item.state}</Badge></td>
|
||||
<td className="px-3 py-2">{formatDate(item.updated_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -669,7 +1239,6 @@ export const AdminPage = () => {
|
|||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSystem = () => {
|
||||
if (!systemQuery.data) {
|
||||
return systemQuery.isLoading ? (
|
||||
|
|
@ -875,7 +1444,7 @@ export const AdminPage = () => {
|
|||
<Section id="status" title="Статус & лимиты" description="IPFS, пины и лимиты">Загрузка…</Section>
|
||||
) : null;
|
||||
}
|
||||
const { ipfs, pin_counts, derivatives, convert_backlog, limits } = statusQuery.data;
|
||||
const { ipfs, pin_counts, derivatives, convert_backlog } = statusQuery.data;
|
||||
return (
|
||||
<Section id="status" title="Статус & лимиты" description="Информация о синхронизации, кэше и лимитах">
|
||||
<div className="grid gap-6 xl:grid-cols-[1.8fr_1fr]">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { AudioPlayer } from '~/shared/ui/audio-player';
|
|||
import { useAuth } from '~/shared/services/auth';
|
||||
import { CongratsModal } from './components/congrats-modal';
|
||||
import { ErrorModal } from './components/error-modal';
|
||||
import { resolveStartPayload } from '~/shared/utils/start-payload';
|
||||
|
||||
type InvoiceStatus = 'paid' | 'failed' | 'cancelled' | 'pending';
|
||||
|
||||
|
|
@ -21,9 +22,10 @@ interface InvoiceEvent {
|
|||
|
||||
export const ViewContentPage = () => {
|
||||
const WebApp = useWebApp();
|
||||
const { contentId } = resolveStartPayload();
|
||||
|
||||
const { data: content, refetch: refetchContent } = useViewContent(
|
||||
WebApp.initDataUnsafe?.start_param
|
||||
contentId
|
||||
);
|
||||
|
||||
const { mutateAsync: purchaseContent } = usePurchaseContent();
|
||||
|
|
@ -34,7 +36,32 @@ export const ViewContentPage = () => {
|
|||
const [isCongratsModal, setIsCongratsModal] = useState(false);
|
||||
const [isErrorModal, setIsErrorModal] = useState(false);
|
||||
|
||||
const statusState = content?.data?.status?.state ?? "uploaded";
|
||||
const conversionState = content?.data?.conversion?.state;
|
||||
const uploadState = content?.data?.upload?.state;
|
||||
const statusLabel = useMemo(() => {
|
||||
switch (statusState) {
|
||||
case "ready":
|
||||
return "Готово";
|
||||
case "processing":
|
||||
return "Обрабатывается";
|
||||
case "failed":
|
||||
return "Ошибка";
|
||||
default:
|
||||
return "Загружено";
|
||||
}
|
||||
}, [statusState]);
|
||||
|
||||
const mediaUrl = content?.data?.display_options?.content_url ?? null;
|
||||
const isAudio = Boolean(content?.data?.content_type?.startsWith('audio'));
|
||||
const isReady = Boolean(mediaUrl);
|
||||
const metadataName = content?.data?.display_options?.metadata?.name;
|
||||
|
||||
const handleBuyContentTON = useCallback(async () => {
|
||||
if (!contentId) {
|
||||
console.error('No content identifier available for purchase');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Если не подключен, начинаем процесс подключения через auth
|
||||
if (!tonConnectUI.connected) {
|
||||
|
|
@ -79,7 +106,7 @@ export const ViewContentPage = () => {
|
|||
// Теперь продолжаем с покупкой
|
||||
console.log('DEBUG: Proceeding with purchase');
|
||||
const contentResponse = await purchaseContent({
|
||||
content_address: WebApp.initDataUnsafe?.start_param,
|
||||
content_address: contentId,
|
||||
license_type: 'resale',
|
||||
});
|
||||
|
||||
|
|
@ -106,7 +133,7 @@ export const ViewContentPage = () => {
|
|||
setIsErrorModal(true);
|
||||
console.error('Error handling Ton Connect subscription:', error);
|
||||
}
|
||||
}, [auth, purchaseContent, refetchContent, tonConnectUI, WebApp.initDataUnsafe?.start_param]);
|
||||
}, [auth, contentId, purchaseContent, refetchContent, tonConnectUI]);
|
||||
|
||||
const handleBuyContentStars = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -157,12 +184,16 @@ export const ViewContentPage = () => {
|
|||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentId) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void refetchContent();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}, [contentId, refetchContent]);
|
||||
|
||||
const handleConfirmCongrats = () => {
|
||||
setIsCongratsModal(!isCongratsModal);
|
||||
|
|
@ -190,7 +221,7 @@ export const ViewContentPage = () => {
|
|||
<main className={'min-h-screen flex w-full flex-col gap-[50px] px-4 '}>
|
||||
{isCongratsModal && <CongratsModal onConfirm={handleConfirmCongrats} />}
|
||||
{isErrorModal && <ErrorModal onConfirm={handleErrorModal} />}
|
||||
{content?.data?.content_type.startsWith('audio') &&
|
||||
{isAudio &&
|
||||
content?.data?.display_options?.metadata?.image && (
|
||||
<div className={'mt-[30px] h-[314px] w-full'}>
|
||||
<img
|
||||
|
|
@ -201,31 +232,45 @@ export const ViewContentPage = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{content?.data?.content_type.startsWith('audio') ? (
|
||||
<AudioPlayer src={content?.data?.display_options?.content_url} />
|
||||
) : (
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
width="100%"
|
||||
config={{
|
||||
file: {
|
||||
attributes: {
|
||||
playsInline: true,
|
||||
autoPlay: true,
|
||||
poster:
|
||||
content?.data?.display_options?.metadata?.image || undefined,
|
||||
{isReady ? (
|
||||
isAudio ? (
|
||||
<AudioPlayer src={mediaUrl ?? ''} />
|
||||
) : (
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
width="100%"
|
||||
config={{
|
||||
file: {
|
||||
attributes: {
|
||||
playsInline: true,
|
||||
autoPlay: true,
|
||||
poster: content?.data?.display_options?.metadata?.image || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
url={content?.data?.display_options?.content_url}
|
||||
/>
|
||||
}}
|
||||
url={mediaUrl}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className={'rounded-xl bg-slate-900/60 p-4 text-center text-sm text-slate-200'}>
|
||||
<p>{statusLabel}. Конвертация может занять несколько минут.</p>
|
||||
{conversionState && (
|
||||
<p className={'mt-2 text-xs text-slate-400'}>
|
||||
Статус конвертера: {conversionState}
|
||||
</p>
|
||||
)}
|
||||
{uploadState && (
|
||||
<p className={'mt-1 text-xs text-slate-400'}>
|
||||
Загрузка: {uploadState}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className={'flex flex-col'}>
|
||||
<h1 className={'text-[20px] font-bold'}>
|
||||
{content?.data?.display_options?.metadata?.name}
|
||||
</h1>
|
||||
<h1 className={'text-[20px] font-bold'}>{metadataName}</h1>
|
||||
<span className={'mt-1 text-xs text-slate-400'}>{statusLabel}</span>
|
||||
{/*<h2>Russian</h2>*/}
|
||||
{/*<h2>2022</h2>*/}
|
||||
<p className={'mt-2 text-[12px]'}>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,106 @@ export type AdminStorageResponse = {
|
|||
};
|
||||
};
|
||||
|
||||
export type AdminUploadsContentDerivative = {
|
||||
kind: string;
|
||||
status: string;
|
||||
size_bytes: number | null;
|
||||
error: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
attempts: number;
|
||||
download_url: string | null;
|
||||
};
|
||||
|
||||
export type AdminUploadsContentUploadHistoryItem = {
|
||||
state: string;
|
||||
at: string | null;
|
||||
error: string | null;
|
||||
filename: string | null;
|
||||
};
|
||||
|
||||
export type AdminUploadsContentIpfs = {
|
||||
pin_state: string | null;
|
||||
pin_error: string | null;
|
||||
bytes_total: number | null;
|
||||
bytes_fetched: number | null;
|
||||
pinned_at: string | null;
|
||||
updated_at: string | null;
|
||||
} | null;
|
||||
|
||||
export type AdminUploadsContentStored = {
|
||||
stored_id: number | null;
|
||||
type: string | null;
|
||||
owner_address: string | null;
|
||||
user_id: number | null;
|
||||
status: string | null;
|
||||
content_url: string | null;
|
||||
download_url: string | null;
|
||||
created: string | null;
|
||||
updated: string | null;
|
||||
user?: {
|
||||
id: number;
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
};
|
||||
} | null;
|
||||
|
||||
export type AdminUploadsContentLinks = {
|
||||
web_view: string | null;
|
||||
start_app: string | null;
|
||||
api_view: string | null;
|
||||
download_primary: string | null;
|
||||
download_derivatives: Array<{
|
||||
kind: string;
|
||||
url: string;
|
||||
size_bytes: number | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type AdminUploadsContentFlags = {
|
||||
issues: boolean;
|
||||
processing: boolean;
|
||||
ready: boolean;
|
||||
unindexed: boolean;
|
||||
};
|
||||
|
||||
export type AdminUploadsContentStatus = {
|
||||
upload_state: string | null;
|
||||
conversion_state: string | null;
|
||||
ipfs_state: string | null;
|
||||
onchain: {
|
||||
indexed: boolean;
|
||||
onchain_index: number | null;
|
||||
item_address: string | null;
|
||||
license_count?: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type AdminUploadsContent = {
|
||||
encrypted_cid: string;
|
||||
metadata_cid: string | null;
|
||||
content_hash: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
content_type: string;
|
||||
size: {
|
||||
encrypted: number | null;
|
||||
plain: number | null;
|
||||
};
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
status: AdminUploadsContentStatus;
|
||||
upload_history: AdminUploadsContentUploadHistoryItem[];
|
||||
derivative_summary: Record<string, number>;
|
||||
derivatives: AdminUploadsContentDerivative[];
|
||||
ipfs: AdminUploadsContentIpfs;
|
||||
stored: AdminUploadsContentStored;
|
||||
links: AdminUploadsContentLinks;
|
||||
flags?: AdminUploadsContentFlags;
|
||||
};
|
||||
|
||||
export type AdminUploadsResponse = {
|
||||
total: number;
|
||||
states: Record<string, number>;
|
||||
|
|
@ -91,8 +191,25 @@ export type AdminUploadsResponse = {
|
|||
updated_at: string;
|
||||
created_at: string;
|
||||
}>;
|
||||
contents: AdminUploadsContent[];
|
||||
matching_total?: number;
|
||||
filter?: string[];
|
||||
search?: string | null;
|
||||
limit?: number;
|
||||
scan?: number;
|
||||
scanned?: number;
|
||||
category_totals?: Record<string, number>;
|
||||
};
|
||||
|
||||
export type AdminUploadsQueryParams = {
|
||||
filter?: string | string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
scan?: number;
|
||||
};
|
||||
|
||||
type AdminUploadsQueryKey = ['admin', 'uploads', string | null, string | null, number | null, number | null];
|
||||
|
||||
export type AdminSystemResponse = {
|
||||
env: Record<string, string | null | undefined>;
|
||||
service_config: Array<{
|
||||
|
|
@ -232,12 +349,34 @@ export const useAdminStorage = (
|
|||
};
|
||||
|
||||
export const useAdminUploads = (
|
||||
options?: QueryOptions<AdminUploadsResponse, ['admin', 'uploads']>,
|
||||
params?: AdminUploadsQueryParams,
|
||||
options?: QueryOptions<AdminUploadsResponse, AdminUploadsQueryKey>,
|
||||
) => {
|
||||
return useQuery<AdminUploadsResponse, AxiosError, AdminUploadsResponse, ['admin', 'uploads']>(
|
||||
['admin', 'uploads'],
|
||||
const filterParts = Array.isArray(params?.filter)
|
||||
? params.filter.filter(Boolean)
|
||||
: params?.filter
|
||||
? [params.filter]
|
||||
: [];
|
||||
const filterValue = filterParts.length > 0 ? filterParts.join(',') : null;
|
||||
const searchValue = params?.search?.trim() || null;
|
||||
const limitValue = params?.limit ?? null;
|
||||
const scanValue = params?.scan ?? null;
|
||||
|
||||
const queryKey: AdminUploadsQueryKey = ['admin', 'uploads', filterValue, searchValue, limitValue, scanValue];
|
||||
|
||||
const queryParams: Record<string, string | number | undefined> = {
|
||||
filter: filterValue ?? undefined,
|
||||
search: searchValue ?? undefined,
|
||||
limit: limitValue ?? undefined,
|
||||
scan: scanValue ?? undefined,
|
||||
};
|
||||
|
||||
return useQuery<AdminUploadsResponse, AxiosError, AdminUploadsResponse, AdminUploadsQueryKey>(
|
||||
queryKey,
|
||||
async () => {
|
||||
const { data } = await request.get<AdminUploadsResponse>('/admin.uploads');
|
||||
const { data } = await request.get<AdminUploadsResponse>('/admin.uploads', {
|
||||
params: queryParams,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useTonConnectUI } from '@tonconnect/ui-react';
|
|||
import { useMutation } from 'react-query';
|
||||
import { request } from '~/shared/libs';
|
||||
import { useWebApp } from '@vkruglikov/react-telegram-web-app';
|
||||
import { appendReferral } from '~/shared/utils/start-payload';
|
||||
|
||||
const sessionStorageKey = 'auth_v1_token';
|
||||
const tonProofStorageKey = 'stored_ton_proof';
|
||||
|
|
@ -47,7 +48,7 @@ export const useAuth = () => {
|
|||
ton_balance: string;
|
||||
};
|
||||
auth_v1_token: string;
|
||||
}>('/auth.twa', params);
|
||||
}>('/auth.twa', appendReferral(params));
|
||||
|
||||
if (res?.data?.auth_v1_token) {
|
||||
localStorage.setItem(sessionStorageKey, res.data.auth_v1_token);
|
||||
|
|
@ -95,9 +96,9 @@ export const useAuth = () => {
|
|||
|
||||
try {
|
||||
// Get the payload/token from backend
|
||||
const value = await request.post<{ auth_v1_token: string }>('/auth.twa', {
|
||||
const value = await request.post<{ auth_v1_token: string }>('/auth.twa', appendReferral({
|
||||
twa_data: WebApp.initData,
|
||||
});
|
||||
}));
|
||||
|
||||
if (value?.data?.auth_v1_token) {
|
||||
console.log('DEBUG: Got token for connect params');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { request } from "~/shared/libs";
|
||||
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
|
||||
import { appendReferral } from "~/shared/utils/start-payload";
|
||||
|
||||
const sessionStorageKey = "auth_v1_token";
|
||||
|
||||
|
|
@ -10,9 +11,9 @@ export const useAuthTwa = () => {
|
|||
const makeAuthRequest = async () => {
|
||||
const res = await request.post<{
|
||||
auth_v1_token: string;
|
||||
}>("/auth.twa", {
|
||||
}>("/auth.twa", appendReferral({
|
||||
twa_data: WebApp.initData,
|
||||
});
|
||||
}));
|
||||
|
||||
if (res?.data?.auth_v1_token) {
|
||||
localStorage.setItem(sessionStorageKey, res.data.auth_v1_token);
|
||||
|
|
@ -23,4 +24,4 @@ export const useAuthTwa = () => {
|
|||
};
|
||||
|
||||
return useMutation(["auth"], makeAuthRequest);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,14 +45,20 @@ export const useCreateNewContent = () => {
|
|||
// );
|
||||
// };
|
||||
|
||||
export const useViewContent = (contentId: string) => {
|
||||
return useQuery(["view", "content", contentId], () => {
|
||||
return request.get(`/content.view/${contentId}`, {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('auth_v1_token') ?? ""
|
||||
}
|
||||
});
|
||||
});
|
||||
export const useViewContent = (contentId: string | null | undefined) => {
|
||||
return useQuery(
|
||||
["view", "content", contentId],
|
||||
() => {
|
||||
return request.get(`/content.view/${contentId}`, {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('auth_v1_token') ?? ""
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: Boolean(contentId),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const usePurchaseContent = () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue