perf: replace @tabler packages with lucide-react and static CSS (-70 MB node_modules)
Docker / build (push) Failing after 16s

Made-with: Cursor
This commit is contained in:
Denozordec
2026-03-21 02:11:28 +07:00
parent 5cd11fc454
commit a1227a13e8
13 changed files with 77 additions and 159 deletions
+1
View File
@@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vps-tracker</title>
<link rel="stylesheet" href="/css/tabler.min.css" />
</head>
<body>
<div id="root"></div>
+11 -79
View File
@@ -14,8 +14,6 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tabler/core": "^1.4.0",
"@tabler/icons-react": "^3.40.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
@@ -25,6 +23,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
@@ -1021,17 +1020,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -1389,52 +1377,6 @@
"win32"
]
},
"node_modules/@tabler/core": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.4.0.tgz",
"integrity": "sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "5.3.7"
},
"engines": {
"node": ">=20"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.40.0.tgz",
"integrity": "sha512-V/Q4VgNPKubRTiLdmWjV/zscYcj5IIk+euicUtaVVqF6luSC9rDngYWgST5/yh3Mrg/mYUwRv1YVTk71Jp0twQ==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-react": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.40.0.tgz",
"integrity": "sha512-oO5+6QCnna4a//mYubx4euZfECtzQZFDGsDMIdzZUhbdyBCT+3bRVFBPueGIcemWld4Vb/0UQ39C/cmGfGylAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tabler/icons": "3.40.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"react": ">= 16"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1686,26 +1628,6 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/bootstrap": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3030,6 +2952,16 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+1 -2
View File
@@ -17,8 +17,6 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tabler/core": "^1.4.0",
"@tabler/icons-react": "^3.40.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
@@ -28,6 +26,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
+9
View File
File diff suppressed because one or more lines are too long
+22 -22
View File
@@ -1,28 +1,28 @@
import { NavLink, useLocation } from 'react-router-dom'
import {
IconBuildingSkyscraper,
IconChartBar,
IconChartHistogram,
IconCoin,
IconCreditCardPay,
IconLayoutDashboard,
IconSettings,
IconServer,
IconServer2,
IconWallet,
} from '@tabler/icons-react'
Building2,
BarChart,
BarChart2,
Coins,
CreditCard,
LayoutDashboard,
Settings,
Server,
ServerCog,
Wallet,
} from 'lucide-react'
const menuItems = [
{ to: '/dashboard', label: 'Дашборд', icon: IconLayoutDashboard },
{ to: '/vps', label: 'VPS', icon: IconServer },
{ to: '/tariffs', label: 'Активные тарифы', icon: IconServer2 },
{ to: '/providers', label: 'Хостеры', icon: IconBuildingSkyscraper },
{ to: '/accounts', label: 'Аккаунты хостеров', icon: IconWallet },
{ to: '/payments', label: 'Платежи', icon: IconCreditCardPay },
{ to: '/balance', label: 'Баланс и списания', icon: IconCoin },
{ to: '/reports', label: 'Отчёты', icon: IconChartHistogram },
{ to: '/resources', label: 'Ресурсы', icon: IconChartBar },
{ to: '/settings', label: 'Настройки', icon: IconSettings },
{ to: '/dashboard', label: 'Дашборд', icon: LayoutDashboard },
{ to: '/vps', label: 'VPS', icon: Server },
{ to: '/tariffs', label: 'Активные тарифы', icon: ServerCog },
{ to: '/providers', label: 'Хостеры', icon: Building2 },
{ to: '/accounts', label: 'Аккаунты хостеров', icon: Wallet },
{ to: '/payments', label: 'Платежи', icon: CreditCard },
{ to: '/balance', label: 'Баланс и списания', icon: Coins },
{ to: '/reports', label: 'Отчёты', icon: BarChart2 },
{ to: '/resources', label: 'Ресурсы', icon: BarChart },
{ to: '/settings', label: 'Настройки', icon: Settings },
]
export function AppLayout({ children }) {
@@ -44,7 +44,7 @@ export function AppLayout({ children }) {
}`}
>
<span className="nav-link-icon d-md-none d-lg-inline-block">
<item.icon size={18} stroke={1.75} />
<item.icon size={18} strokeWidth={1.75} />
</span>
<span className="nav-link-title">{item.label}</span>
</NavLink>
+2 -2
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { IconFolder } from '@tabler/icons-react'
import { Folder } from 'lucide-react'
import { fetchProjectSuggestions } from '../lib/api'
import { noBrowserSuggestProps } from '../lib/noBrowserSuggestProps'
@@ -184,7 +184,7 @@ export function ProjectSuggestInput({
onMouseEnter={() => setActiveIndex(idx)}
>
<span className="text-secondary project-suggest-folder">
<IconFolder size={18} stroke={1.5} />
<Folder size={18} strokeWidth={1.5} />
</span>
<span className="text-truncate">{highlightMatch(name, value)}</span>
</button>
-2
View File
@@ -2,8 +2,6 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import '@tabler/core/dist/css/tabler.min.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
+5 -5
View File
@@ -17,7 +17,7 @@ import {
accountBillmanagerUiReady,
accountUsesBillmanagerBalanceApi,
} from '../lib/billmanager-ui'
import { IconRefresh, IconPlugConnected } from '@tabler/icons-react'
import { RefreshCw, PlugZap } from 'lucide-react'
const emptyForm = {
providerId: '',
@@ -314,7 +314,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
{syncLoadingAll ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconRefresh size={16} className="me-1" />
<RefreshCw size={16} className="me-1" />
)}
Синхронизировать VPS
</button>
@@ -400,7 +400,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
{balanceLoadingId === account.id ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconRefresh size={14} className="me-1" />
<RefreshCw size={14} className="me-1" />
)}
Баланс
</button>
@@ -414,7 +414,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
{syncLoadingId === account.id ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconRefresh size={14} className="me-1" />
<RefreshCw size={14} className="me-1" />
)}
Синхронизировать
</button>
@@ -568,7 +568,7 @@ export function AccountsPage({ db, actions, settings, ratesData }) {
{testConnectionLoading ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconPlugConnected size={14} className="me-1" />
<PlugZap size={14} className="me-1" />
)}
Проверить соединение
</button>
+5 -10
View File
@@ -9,12 +9,7 @@ import { EmptyState } from '../components/EmptyState'
import { ExpenseChart } from '../components/ExpenseChart'
import { PageHeader } from '../components/PageHeader'
import { ProviderPieChart } from '../components/ProviderPieChart'
import {
IconCash,
IconClockHour4,
IconServer,
IconWallet,
} from '@tabler/icons-react'
import { Banknote, Clock4, Server, Wallet } from 'lucide-react'
export function DashboardPage({ db = {}, settings, ratesData }) {
const [dashTab, setDashTab] = useState('overview')
@@ -273,7 +268,7 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
<div className="d-flex align-items-center justify-content-between">
<div className="text-secondary">Активные VPS</div>
<span className="metric-icon bg-blue-lt text-blue">
<IconServer size={18} />
<Server size={18} />
</span>
</div>
<div className="stat-value">{activeVpsCount}</div>
@@ -286,7 +281,7 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
<div className="d-flex align-items-center justify-content-between">
<div className="text-secondary">Расходы за месяц</div>
<span className="metric-icon bg-green-lt text-green">
<IconCash size={18} />
<Banknote size={18} />
</span>
</div>
<div className="stat-value">{formatCurrency(monthExpenses, baseCurrency)}</div>
@@ -305,7 +300,7 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
<div className="d-flex align-items-center justify-content-between">
<div className="text-secondary">Аккаунтов хостеров</div>
<span className="metric-icon bg-yellow-lt text-yellow">
<IconClockHour4 size={18} />
<Clock4 size={18} />
</span>
</div>
<div className="stat-value">{providerAccounts.length}</div>
@@ -318,7 +313,7 @@ export function DashboardPage({ db = {}, settings, ratesData }) {
<div className="d-flex align-items-center justify-content-between">
<div className="text-secondary">Суммарный баланс</div>
<span className="metric-icon bg-purple-lt text-purple">
<IconWallet size={18} />
<Wallet size={18} />
</span>
</div>
<div className="stat-value">{formatCurrency(totalBalance, baseCurrency)}</div>
+2 -2
View File
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react'
import { IconEdit } from '@tabler/icons-react'
import { Pencil } from 'lucide-react'
import { paymentTypeLabel } from '../lib/utils'
import { UiModal } from '../components/UiModal'
import { EmptyState } from '../components/EmptyState'
@@ -155,7 +155,7 @@ export function PaymentsPage({ db, actions, settings, ratesData }) {
className="btn btn-sm btn-outline-primary me-1"
onClick={() => onEdit(payment)}
>
<IconEdit size={14} className="me-1" />
<Pencil size={14} className="me-1" />
Изменить
</button>
<button
+4 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { IconPlus, IconSend, IconTrash } from '@tabler/icons-react'
import { Plus, Send, Trash2 } from 'lucide-react'
import { PageHeader } from '../components/PageHeader'
import {
downloadBackupDatabaseBlob,
@@ -231,7 +231,7 @@ export function SettingsPage({ db, actions, ratesData, ratesError }) {
className="btn btn-primary"
onClick={addCustomField}
>
<IconPlus size={16} />
<Plus size={16} />
</button>
</div>
{customFields.length > 0 ? (
@@ -244,7 +244,7 @@ export function SettingsPage({ db, actions, ratesData, ratesError }) {
className="btn btn-sm btn-outline-danger"
onClick={() => removeCustomField(f.key)}
>
<IconTrash size={14} />
<Trash2 size={14} />
</button>
</li>
))}
@@ -422,7 +422,7 @@ export function SettingsPage({ db, actions, ratesData, ratesError }) {
</>
) : (
<>
<IconSend size={16} className="me-1" />
<Send size={16} className="me-1" />
Тестовое уведомление
</>
)}
+6 -13
View File
@@ -1,13 +1,6 @@
import { useMemo, useState } from 'react'
import { convertCurrency, formatCurrency, faviconUrlFromWebsite, normalizeWebsiteUrl } from '../lib/utils'
import {
IconArrowDown,
IconArrowUp,
IconMapPin,
IconRefresh,
IconSearch,
IconServer,
} from '@tabler/icons-react'
import { ArrowDown, ArrowUp, MapPin, RefreshCw, Search, Server } from 'lucide-react'
import { syncAccount } from '../lib/api'
import { EmptyState } from '../components/EmptyState'
import { PageHeader } from '../components/PageHeader'
@@ -26,7 +19,7 @@ function SortHeader({ column, children, onSort, sortBy, sortDir }) {
style={SORT_COLUMNS.includes(column) ? { cursor: 'pointer', userSelect: 'none' } : undefined}
>
{children}
{sortBy === column && (sortDir === 'asc' ? <IconArrowUp size={14} className="ms-1" /> : <IconArrowDown size={14} className="ms-1" />)}
{sortBy === column && (sortDir === 'asc' ? <ArrowUp size={14} className="ms-1" /> : <ArrowDown size={14} className="ms-1" />)}
</th>
)
}
@@ -201,7 +194,7 @@ export function TariffsPage({ db, actions, settings, ratesData }) {
<div className="card">
<div className="card-header">
<h3 className="card-title">
<IconMapPin size={18} className="me-1" />
<MapPin size={18} className="me-1" />
Доступные датацентры и страны
</h3>
</div>
@@ -249,7 +242,7 @@ export function TariffsPage({ db, actions, settings, ratesData }) {
<div className="col-xl-3 col-lg-4 col-md-6">
<div className="input-icon">
<span className="input-icon-addon">
<IconSearch size={16} />
<Search size={16} />
</span>
<input {...noBrowserSuggestProps}
className="form-control"
@@ -370,7 +363,7 @@ export function TariffsPage({ db, actions, settings, ratesData }) {
{syncLoading ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconRefresh size={16} className="me-1" />
<RefreshCw size={16} className="me-1" />
)}
Синхронизировать
</button>
@@ -423,7 +416,7 @@ export function TariffsPage({ db, actions, settings, ratesData }) {
<td>
<div className="d-flex align-items-center gap-2">
<span className="avatar avatar-sm bg-blue-lt">
<IconServer size={16} />
<Server size={16} />
</span>
<div>
<div className="fw-medium">{item.name || '—'}</div>
+9 -18
View File
@@ -9,16 +9,7 @@ import {
tariffTypeLabel,
vpsStatusLabel,
} from '../lib/utils'
import {
IconEdit,
IconFilter,
IconMapPin,
IconPlus,
IconRefresh,
IconSearch,
IconServer,
IconTrash,
} from '@tabler/icons-react'
import { Pencil, Filter, MapPin, Plus, RefreshCw, Search, Server, Trash2 } from 'lucide-react'
import { syncAccount, bulkUpdateVps } from '../lib/api'
import { UiModal } from '../components/UiModal'
import { ConvertedAmount } from '../components/ConvertedAmount'
@@ -689,7 +680,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
<div className="col-xl-3 col-lg-4 col-md-6">
<div className="input-icon">
<span className="input-icon-addon">
<IconSearch size={16} />
<Search size={16} />
</span>
<input {...noBrowserSuggestProps}
className="form-control"
@@ -822,7 +813,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
<div className="col-xl-2 col-lg-4 col-md-6">
<div className="input-icon">
<span className="input-icon-addon">
<IconFilter size={16} />
<Filter size={16} />
</span>
<input {...noBrowserSuggestProps}
className="form-control"
@@ -840,7 +831,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
<div className="col-xl-2 col-lg-4 col-md-6">
<div className="input-icon">
<span className="input-icon-addon">
<IconMapPin size={16} />
<MapPin size={16} />
</span>
<input {...noBrowserSuggestProps}
className="form-control"
@@ -1086,7 +1077,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
{syncLoading ? (
<span className="spinner-border spinner-border-sm me-1" role="status" />
) : (
<IconRefresh size={16} className="me-1" />
<RefreshCw size={16} className="me-1" />
)}
Синхронизировать
</button>
@@ -1104,7 +1095,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
setIsModalOpen(true)
}}
>
<IconPlus size={16} className="me-1" />
<Plus size={16} className="me-1" />
Добавить VPS
</button>
</div>
@@ -1332,7 +1323,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
className="btn btn-sm btn-outline-primary"
onClick={() => onEdit(item)}
>
<IconEdit size={14} className="me-1" />
<Pencil size={14} className="me-1" />
Изменить
</button>
<button
@@ -1340,7 +1331,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
className="btn btn-sm btn-outline-danger"
onClick={() => actions.remove('vps', item.id)}
>
<IconTrash size={14} className="me-1" />
<Trash2 size={14} className="me-1" />
Удалить
</button>
</div>
@@ -1379,7 +1370,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
Отмена
</button>
<button type="submit" form="vps-form" className="btn btn-primary">
<IconPlus size={16} className="me-1" />
<Plus size={16} className="me-1" />
{editingId ? 'Сохранить' : 'Добавить'}
</button>
</div>