mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat: add admin updates page with release history
Shows bot and cabinet releases with version badges, changelogs, and update detection via __APP_VERSION__.
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -70,6 +70,7 @@ const AdminRemnawave = lazy(() => import('./pages/AdminRemnawave'));
|
||||
const AdminRemnawaveSquadDetail = lazy(() => import('./pages/AdminRemnawaveSquadDetail'));
|
||||
const AdminEmailTemplates = lazy(() => import('./pages/AdminEmailTemplates'));
|
||||
const AdminTrafficUsage = lazy(() => import('./pages/AdminTrafficUsage'));
|
||||
const AdminUpdates = lazy(() => import('./pages/AdminUpdates'));
|
||||
const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail'));
|
||||
const AdminBroadcastDetail = lazy(() => import('./pages/AdminBroadcastDetail'));
|
||||
const AdminEmailTemplatePreview = lazy(() => import('./pages/AdminEmailTemplatePreview'));
|
||||
@@ -656,6 +657,16 @@ function App() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/updates"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<LazyPage>
|
||||
<AdminUpdates />
|
||||
</LazyPage>
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/users/:id"
|
||||
element={
|
||||
|
||||
32
src/api/adminUpdates.ts
Normal file
32
src/api/adminUpdates.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
export interface ReleaseItem {
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string;
|
||||
published_at: string;
|
||||
prerelease: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectReleasesInfo {
|
||||
current_version: string;
|
||||
has_updates: boolean;
|
||||
releases: ReleaseItem[];
|
||||
repo_url: string;
|
||||
}
|
||||
|
||||
export interface ReleasesResponse {
|
||||
bot: ProjectReleasesInfo;
|
||||
cabinet: ProjectReleasesInfo;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
|
||||
export const adminUpdatesApi = {
|
||||
getReleases: async (): Promise<ReleasesResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/updates/releases');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -762,7 +762,8 @@
|
||||
"promoOffers": "Promo Offers",
|
||||
"promocodes": "Promo Codes",
|
||||
"promoGroups": "Discount Groups",
|
||||
"trafficUsage": "Traffic Usage"
|
||||
"trafficUsage": "Traffic Usage",
|
||||
"updates": "Updates"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Admin Panel",
|
||||
@@ -785,7 +786,8 @@
|
||||
"promoOffersDesc": "Personal discounts",
|
||||
"promocodesDesc": "Manage promo codes",
|
||||
"promoGroupsDesc": "Discount groups for users",
|
||||
"trafficUsageDesc": "Per-user traffic by nodes"
|
||||
"trafficUsageDesc": "Per-user traffic by nodes",
|
||||
"updatesDesc": "Bot & cabinet versions"
|
||||
},
|
||||
"trafficUsage": {
|
||||
"title": "Traffic Usage",
|
||||
@@ -2113,6 +2115,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"adminUpdates": {
|
||||
"title": "Updates",
|
||||
"subtitle": "Bot and cabinet release history",
|
||||
"bot": "Bot",
|
||||
"cabinet": "Cabinet",
|
||||
"currentVersion": "Current version",
|
||||
"upToDate": "Up to date",
|
||||
"updateAvailable": "Update available",
|
||||
"published": "Published",
|
||||
"prerelease": "Pre-release",
|
||||
"refresh": "Check for updates",
|
||||
"noReleases": "No release data"
|
||||
},
|
||||
"adminDashboard": {
|
||||
"title": "Statistics Dashboard",
|
||||
"subtitle": "Real-time system overview",
|
||||
|
||||
@@ -643,7 +643,8 @@
|
||||
"promoGroups": "گروههای تخفیف",
|
||||
"remnawave": "RemnaWave",
|
||||
"users": "کاربران",
|
||||
"trafficUsage": "مصرف ترافیک"
|
||||
"trafficUsage": "مصرف ترافیک",
|
||||
"updates": "بهروزرسانیها"
|
||||
},
|
||||
"panel": {
|
||||
"title": "پنل مدیریت",
|
||||
@@ -666,7 +667,8 @@
|
||||
"promoGroupsDesc": "گروههای تخفیف برای کاربران",
|
||||
"remnawaveDesc": "مدیریت پنل و آمار",
|
||||
"usersDesc": "مدیریت کاربران ربات",
|
||||
"trafficUsageDesc": "ترافیک هر کاربر بر اساس نود"
|
||||
"trafficUsageDesc": "ترافیک هر کاربر بر اساس نود",
|
||||
"updatesDesc": "نسخههای ربات و کابینت"
|
||||
},
|
||||
"trafficUsage": {
|
||||
"title": "مصرف ترافیک",
|
||||
@@ -1928,6 +1930,19 @@
|
||||
"saturation": "اشباع"
|
||||
}
|
||||
},
|
||||
"adminUpdates": {
|
||||
"title": "بهروزرسانیها",
|
||||
"subtitle": "تاریخچه انتشار ربات و کابینت",
|
||||
"bot": "ربات",
|
||||
"cabinet": "کابینت",
|
||||
"currentVersion": "نسخه فعلی",
|
||||
"upToDate": "بهروز است",
|
||||
"updateAvailable": "بهروزرسانی موجود",
|
||||
"published": "منتشر شده",
|
||||
"prerelease": "پیشانتشار",
|
||||
"refresh": "بررسی بهروزرسانی",
|
||||
"noReleases": "دادهای موجود نیست"
|
||||
},
|
||||
"adminDashboard": {
|
||||
"title": "پنل آمار",
|
||||
"subtitle": "نمای کلی سیستم در لحظه",
|
||||
|
||||
@@ -783,7 +783,8 @@
|
||||
"promoOffers": "Промопредложения",
|
||||
"promocodes": "Промокоды",
|
||||
"promoGroups": "Группы скидок",
|
||||
"trafficUsage": "Расход трафика"
|
||||
"trafficUsage": "Расход трафика",
|
||||
"updates": "Обновления"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Панель администратора",
|
||||
@@ -806,7 +807,8 @@
|
||||
"promoOffersDesc": "Персональные скидки",
|
||||
"promocodesDesc": "Управление промокодами",
|
||||
"promoGroupsDesc": "Группы скидок для пользователей",
|
||||
"trafficUsageDesc": "Трафик пользователей по нодам"
|
||||
"trafficUsageDesc": "Трафик пользователей по нодам",
|
||||
"updatesDesc": "Версии бота и кабинета"
|
||||
},
|
||||
"trafficUsage": {
|
||||
"title": "Расход трафика",
|
||||
@@ -2659,6 +2661,19 @@
|
||||
"saturation": "Насыщенность"
|
||||
}
|
||||
},
|
||||
"adminUpdates": {
|
||||
"title": "Обновления",
|
||||
"subtitle": "История релизов бота и кабинета",
|
||||
"bot": "Бот",
|
||||
"cabinet": "Кабинет",
|
||||
"currentVersion": "Текущая версия",
|
||||
"upToDate": "Актуальная версия",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"published": "Опубликовано",
|
||||
"prerelease": "Предрелиз",
|
||||
"refresh": "Проверить обновления",
|
||||
"noReleases": "Нет данных о релизах"
|
||||
},
|
||||
"adminDashboard": {
|
||||
"title": "Панель статистики",
|
||||
"subtitle": "Обзор системы в реальном времени",
|
||||
|
||||
@@ -643,7 +643,8 @@
|
||||
"promoGroups": "折扣组",
|
||||
"remnawave": "RemnaWave",
|
||||
"users": "用户",
|
||||
"trafficUsage": "流量使用"
|
||||
"trafficUsage": "流量使用",
|
||||
"updates": "更新"
|
||||
},
|
||||
"panel": {
|
||||
"title": "管理面板",
|
||||
@@ -666,7 +667,8 @@
|
||||
"promoGroupsDesc": "用户折扣组",
|
||||
"remnawaveDesc": "面板管理和统计",
|
||||
"usersDesc": "管理机器人用户",
|
||||
"trafficUsageDesc": "按节点统计用户流量"
|
||||
"trafficUsageDesc": "按节点统计用户流量",
|
||||
"updatesDesc": "机器人和面板版本"
|
||||
},
|
||||
"trafficUsage": {
|
||||
"title": "流量使用",
|
||||
@@ -1927,6 +1929,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"adminUpdates": {
|
||||
"title": "更新",
|
||||
"subtitle": "机器人和面板发布历史",
|
||||
"bot": "机器人",
|
||||
"cabinet": "面板",
|
||||
"currentVersion": "当前版本",
|
||||
"upToDate": "已是最新",
|
||||
"updateAvailable": "有可用更新",
|
||||
"published": "发布于",
|
||||
"prerelease": "预发布",
|
||||
"refresh": "检查更新",
|
||||
"noReleases": "没有发布数据"
|
||||
},
|
||||
"adminDashboard": {
|
||||
"title": "统计面板",
|
||||
"subtitle": "实时系统概览",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RemnawaveIcon } from '../components/icons';
|
||||
import { RemnawaveIcon, ArrowPathIcon } from '../components/icons';
|
||||
|
||||
// Group header icons
|
||||
const AnalyticsGroupIcon = () => (
|
||||
@@ -473,6 +473,12 @@ export default function AdminPanel() {
|
||||
title: t('admin.nav.emailTemplates'),
|
||||
description: t('admin.panel.emailTemplatesDesc'),
|
||||
},
|
||||
{
|
||||
to: '/admin/updates',
|
||||
icon: <ArrowPathIcon className="h-5 w-5" />,
|
||||
title: t('admin.nav.updates'),
|
||||
description: t('admin.panel.updatesDesc'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
310
src/pages/AdminUpdates.tsx
Normal file
310
src/pages/AdminUpdates.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { adminUpdatesApi, ReleaseItem, ProjectReleasesInfo } from '../api/adminUpdates';
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
||||
// ============ Icons ============
|
||||
|
||||
const BackIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const RefreshIcon = () => (
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.992 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.185M2.985 14.652"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BotIcon = () => (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h9a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0015.75 4.5h-7.5A2.25 2.25 0 006 6.75v10.5A2.25 2.25 0 008.25 19.5z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CabinetIcon = () => (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TagIcon = () => (
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CalendarIcon = () => (
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ExternalLinkIcon = () => (
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ============ Helpers ============
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
const date = new Date(iso);
|
||||
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function stripVPrefix(tag: string): string {
|
||||
return tag.replace(/^v/, '');
|
||||
}
|
||||
|
||||
// ============ Components ============
|
||||
|
||||
function VersionBadge({ hasUpdate }: { hasUpdate: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (hasUpdate) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/20 px-2.5 py-0.5 text-xs font-medium text-amber-400">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
|
||||
{t('adminUpdates.updateAvailable')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/20 px-2.5 py-0.5 text-xs font-medium text-emerald-400">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||
{t('adminUpdates.upToDate')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseCard({ release }: { release: ReleaseItem }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="border-b border-dark-700/30 px-4 py-3 last:border-b-0">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-1.5 text-dark-200">
|
||||
<TagIcon />
|
||||
<span className="text-sm font-semibold">{release.tag_name}</span>
|
||||
</div>
|
||||
{release.name !== release.tag_name && (
|
||||
<span className="text-sm text-dark-400">{release.name}</span>
|
||||
)}
|
||||
{release.prerelease && (
|
||||
<span className="rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-400">
|
||||
{t('adminUpdates.prerelease')}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1 text-dark-500 sm:ml-auto">
|
||||
<CalendarIcon />
|
||||
<span className="text-xs">{formatDate(release.published_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{release.body ? (
|
||||
<pre className="max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-lg bg-dark-900/50 p-3 font-mono text-xs leading-relaxed text-dark-300">
|
||||
{release.body}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectSection({
|
||||
icon,
|
||||
title,
|
||||
info,
|
||||
currentVersion,
|
||||
hasUpdate,
|
||||
repoUrl,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
info: ProjectReleasesInfo;
|
||||
currentVersion: string;
|
||||
hasUpdate: boolean;
|
||||
repoUrl: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-dark-700/50 bg-dark-800/40">
|
||||
{/* Header */}
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-dark-700/50 px-4 py-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-dark-700/50 text-dark-300">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-base font-semibold text-dark-100">{title}</h2>
|
||||
<VersionBadge hasUpdate={hasUpdate} />
|
||||
</div>
|
||||
<p className="text-xs text-dark-500">
|
||||
{t('adminUpdates.currentVersion')}:{' '}
|
||||
<span className="font-mono font-medium text-dark-300">{currentVersion || '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={repoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 rounded-lg border border-dark-700/50 px-2.5 py-1.5 text-xs text-dark-400 transition-colors hover:border-dark-600 hover:text-dark-300"
|
||||
>
|
||||
GitHub
|
||||
<ExternalLinkIcon />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Releases list */}
|
||||
{info.releases.length > 0 ? (
|
||||
<div className="max-h-[600px] overflow-y-auto">
|
||||
{info.releases.map((release) => (
|
||||
<ReleaseCard key={release.tag_name} release={release} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 py-8 text-center text-sm text-dark-500">
|
||||
{t('adminUpdates.noReleases')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Page ============
|
||||
|
||||
export default function AdminUpdates() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, isLoading, refetch, isFetching } = useQuery({
|
||||
queryKey: ['admin', 'releases'],
|
||||
queryFn: adminUpdatesApi.getReleases,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
// Cabinet has_updates: compare __APP_VERSION__ with latest release
|
||||
const cabinetHasUpdate = (() => {
|
||||
if (!data?.cabinet.releases.length) return false;
|
||||
try {
|
||||
const latestTag = data.cabinet.releases.find((r) => !r.prerelease)?.tag_name;
|
||||
if (!latestTag) return false;
|
||||
return stripVPrefix(latestTag) !== stripVPrefix(__APP_VERSION__);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in space-y-4">
|
||||
{/* Top bar */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/admin')}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg border border-dark-700/50 bg-dark-800/40 transition-colors hover:border-dark-600 hover:bg-dark-800"
|
||||
>
|
||||
<BackIcon />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-xl font-bold text-dark-100">{t('adminUpdates.title')}</h1>
|
||||
<p className="text-xs text-dark-500">{t('adminUpdates.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-dark-700/50 bg-dark-800/40 px-3 py-2 text-xs text-dark-300 transition-colors hover:border-dark-600 hover:bg-dark-800 disabled:opacity-50"
|
||||
>
|
||||
<span className={isFetching ? 'animate-spin' : ''}>
|
||||
<RefreshIcon />
|
||||
</span>
|
||||
<span className="hidden sm:inline">{t('adminUpdates.refresh')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Loading skeleton */}
|
||||
{isLoading && (
|
||||
<div className="space-y-4">
|
||||
{[0, 1].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-64 animate-pulse rounded-xl border border-dark-700/50 bg-dark-800/40"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
<ProjectSection
|
||||
icon={<BotIcon />}
|
||||
title={t('adminUpdates.bot')}
|
||||
info={data.bot}
|
||||
currentVersion={data.bot.current_version}
|
||||
hasUpdate={data.bot.has_updates}
|
||||
repoUrl={data.bot.repo_url}
|
||||
/>
|
||||
<ProjectSection
|
||||
icon={<CabinetIcon />}
|
||||
title={t('adminUpdates.cabinet')}
|
||||
info={data.cabinet}
|
||||
currentVersion={__APP_VERSION__}
|
||||
hasUpdate={cabinetHasUpdate}
|
||||
repoUrl={data.cabinet.repo_url}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user