Commit Graph

1818 Commits

Author SHA1 Message Date
c0mrade
cf469a1a9f chore(types): replace deprecated FormEvent with SyntheticEvent
React 19's @types/react flags FormEvent as deprecated — quote: 'FormEvent
doesn't actually exist. You probably meant to use ChangeEvent, InputEvent,
SubmitEvent, or just SyntheticEvent'. All 17 call sites in this repo
typed form onSubmit handlers and only called e.preventDefault(), so
SyntheticEvent is the correct general replacement.

No behavior change — pure type-level cleanup that clears the chronic
deprecation hint that has been showing up after every edit to any
form-bearing page.
2026-05-26 15:24:01 +03:00
c0mrade
62eaef2f53 fix(permission-route): guard against /admin → /admin redirect loop
PermissionRoute unconditionally sent permission-failing users to /admin.
The current router wraps /admin in AdminRoute (no permission check), so
this never triggered. But if /admin ever picks up a PermissionRoute
guard — even by accident in a future change — the user would loop on
itself with replace history (no back-button escape).

Detect that case explicitly: if we're already on /admin and still failing
the permission check, fall back to / instead. Costs nothing today,
prevents a hard-to-diagnose lock-out later.
2026-05-26 15:18:21 +03:00
c0mrade
28c682500c fix(i18n,a11y): sync <html lang> + dir with i18n language
index.html ships <html lang='ru'> and that attribute never changed at
runtime, regardless of the user's selected language. Consequences:
- screen readers pronounced en/zh/fa content using Russian phonics
- browsers offered to translate already-translated content
- the Telegram login button and an OAuth widget read documentElement.lang
  to set their own locale and got the wrong one

LanguageSwitcher had a half-fix that only touched dir for fa, and only
locally — i18n changes from anywhere else (initial detect, Telegram
adopt) were not covered.

Move both lang and dir updates into a single languageChanged subscriber
inside i18n.ts, run once for the initial detected language, and drop
the now-redundant dir-setting in LanguageSwitcher. Layout direction
flips automatically for the full RTL set (fa + future ar/he/ur).
2026-05-26 15:15:26 +03:00
c0mrade
7f14c499ad perf(build): split recharts + tiptap into dedicated vendor chunks
Previously, recharts (337 KB raw) lived inside an auto-generated shared
chunk named after the small useChartColors hook because that hook was
the first import path pulling recharts in. Any change to the hook
busted the chunk hash and forced users to re-download the whole
charting library.

Same story for @tiptap and the prosemirror peers — they were grouped
under a 'tiptap-video' chunk named after the local extension file.

Explicit vendor-recharts and vendor-tiptap buckets give those libs
stable cache-keys and predictable names that match the rest of the
vendor-* chunking scheme. Bundle sizes are net-neutral; what improves
is cache stability across releases.
2026-05-26 15:12:50 +03:00
c0mrade
ff5156f36a refactor(routing): drop now-redundant per-route ErrorBoundary wrappers
Six routes were wrapped in <ErrorBoundary level='app'> around LazyPage.
After the previous commit moved a level='page' boundary inside LazyPage,
the outer 'app' boundary became unreachable — the inner one catches
first.

Strip the dead wrappers (PurchaseSuccess, QuickPurchase, AutoLogin,
TopUpResult, GiftSubscription, GiftResult). Behavior is identical, the
route blocks are 25 fewer lines, and there's only one boundary to
reason about per route.
2026-05-26 14:04:47 +03:00
c0mrade
87e2e82136 fix(routing): give every lazy page its own ErrorBoundary
A render error in any route used to bubble all the way up to the single
page-level boundary at AppWithNavigator, replacing the entire shell
(sidebar, header, blocking overlays) with the contained error UI — even
if only one sub-page was broken.

Embed an ErrorBoundary inside LazyPage so the smallest meaningful unit
(a route's lazy chunk + its rendered tree) is the boundary scope. Shell
chrome stays alive, the user can navigate elsewhere, and chunk-load
failures still get the lazyWithRetry reload path because the boundary
sits outside Suspense.
2026-05-26 14:01:56 +03:00
c0mrade
23e4e9b4d1 fix(profile,connected-accounts): drop dead ['user'] invalidate calls
queryClient.invalidateQueries({ queryKey: ['user'] }) appears twice in
the codebase but no useQuery with that key is registered anywhere — the
auth user is a zustand store, not a React Query cache entry. The
invalidations were silent no-ops that suggested wiring that did not
exist.

The actual refresh path (await getMe + setUser) was already in place
right above each invalidate, so the cleanup is purely cosmetic +
documentation: replace with a comment explaining where the user state
actually lives, so the next reader doesn't try to invalidate it again.
2026-05-26 13:58:33 +03:00
c0mrade
bef984d72a fix(scope-selector): drop role=dialog from non-modal popover (a11y)
The scope add-popover wrapped its tabs/search/list in role=dialog with
no aria-modal and no focus trap. That promises modality to AT but
delivers a non-modal popover users can click past — confusing.

Remove the wrapper role. The trigger button already exposes the popup
via aria-haspopup=listbox + aria-expanded, and the inner div carries
role=listbox aria-multiselectable. Nothing for AT to misinterpret now.
2026-05-26 13:50:22 +03:00
c0mrade
576b4d5601 fix(admin): surface clipboard copy failures instead of swallowing
Two admin paths previously had empty catch blocks around copyToClipboard:
the user-detail copy helper and AdminLandings handleCopyUrl. The adapter
already attempts the legacy execCommand fallback, so reaching the catch
means the operation truly failed — staying silent left admins thinking
their click worked when it hadn't.

Both now fire notify.error so the failure is visible.
2026-05-26 13:48:10 +03:00
c0mrade
bc34e65aac fix(admin-bulk-actions): trap focus + label modal dialog (a11y)
Bulk-action modal had role=dialog on the overlay but no focus trap,
no aria-labelledby, no scroll lock, and an ad-hoc Escape listener
that conflicted with anything else listening to document keydown.

Move role=dialog/aria-modal onto the focus-trapped content element,
wire aria-labelledby to the h3 title, and replace the ad-hoc
keydown effect with useFocusTrap (Tab cycle + Esc + body scroll
lock + focus restore on close). Loading state suppresses Esc but
keeps focus trapped, so Tab still cycles inside the progress view.
2026-05-26 13:44:46 +03:00
c0mrade
7817243253 fix(admin-bulk-actions): plug stale-closure bug in columns useMemo
Header checkbox for 'select all subscriptions' read allVisibleSubscriptionIds,
subscriptionSelection, isMultiTariff, toggleAllSubscriptions from a stale
closure because those values were declared AFTER the columns useMemo. The
warned-about deps were absent, so the header only refreshed by accident
when expandedRows or getFilteredSubs changed.

Relocate filteredUsers / allVisibleSubscriptionIds / toggleAllSubscriptions
above the columns block, list them as deps. Header now reflects current
selection without piggybacking on unrelated re-renders.

Same commit clears the matching AdminInfoPageEditor warning with an
explicit eslint-disable-line comment — the activeLocale omission was
already intentional (initial-content lock), just not silenced.
2026-05-26 13:41:22 +03:00
c0mrade
7426e1e6d1 perf(admin-panel): migrate system info + stats poll to React Query
60s setInterval becomes refetchInterval, dropping the cancelled-flag
ceremony and the manual loading/setState plumbing. Two independent
queries — one failure no longer poisons the other (Promise.all
previously rejected the whole batch on a single 5xx).
2026-05-26 13:35:39 +03:00
c0mrade
bc5e95a6e5 perf(admin-bulk-actions): migrate users + 4 lookup loaders to React Query
User list query keys all 8 filter inputs so pagination/search/filter
changes auto-refetch with proper dedup. Tariffs/promoGroups/campaigns/
partners become long-staleTime lookup queries — fired once and cached
across remounts of the page. Existing setUsers/setTariffs/etc. setters
stay; sync useEffects copy query.data into them so downstream selection
and handler code is untouched. loadUsers becomes a thin refetch wrapper
for handleRefresh and mutation handlers.
2026-05-26 13:32:43 +03:00
c0mrade
6f7bd10d61 perf(admin-user-detail): migrate remaining 9 leaf loaders to React Query
Drops manual activeTab/userId loader triggers — query enabled
gating now handles all per-tab fetching. Wrappers around refetch
keep mutation handler call sites unchanged. Removes 4 dead
wrappers (loadTariffs/Referrals/Gifts/PromoGroups) only called
by the now-deleted activeTab useEffect.
2026-05-26 13:19:28 +03:00
c0mrade
5ee97e0a9f perf(admin): migrate AdminUserDetail loadUser to React Query (caching + cache key)
Replace the 13-line manual fetch with a useQuery hook keyed on userId. loadUser is
kept as a thin wrapper around userQuery.refetch() so all 25+ existing call sites in
mutation handlers continue to work unchanged. Sync useEffects copy data → setUser,
isFetching → setLoading, and isError → navigate, preserving the original
'load fails → redirect to /admin/users' behavior.

Remaining 12 loaders (loadSyncStatus/loadTariffs/loadTickets/loadReferrals/loadPanelInfo/
loadNodeUsage/loadDevices/loadSubscriptionData/loadGifts/loadPromoGroups/loadTicketDetail/
loadRequestHistory) NOT migrated in this commit — some are parameterized, some chain via
Promise.all in loadSubscriptionData; they need a dedicated refactor with functional
testing across all 7 tabs and the mutation flows. This commit ships the lowest-risk,
highest-leverage piece (userQuery has the most call sites).
2026-05-26 13:03:04 +03:00
c0mrade
1e4d6dad47 perf(admin): migrate AdminBanSystem tab fetching to React Query
All 10 tab data fetches + the initial status load move from manual useState +
useEffect + loadTabData(switch) pattern to per-tab useQuery hooks with enabled
gating. Each query syncs into the existing state vars via useEffect so the JSX
and mutation handlers stay unchanged; handleSearch/handleUnban/handleToggle/handleSet
now call query.refetch() directly; refresh button uses refetchActiveTab helper.

Net effect: tab switches return to cached data instantly (background revalidate),
no duplicate fetches, no manual loading/error wiring. The reports query has
reportHours in its queryKey so changing the period auto-refetches without the
extra useEffect that previously handled it.
2026-05-26 12:55:28 +03:00
c0mrade
7e9e51ba5a fix(a11y): announce Login form-level error via role=alert 2026-05-26 12:09:57 +03:00
c0mrade
92ec9c3c42 fix(a11y): announce form-level errors via role=alert in Login + ResetPassword
Wrap the error message blocks in role=alert so assistive technology announces
authentication failures and reset-password errors when they appear (WCAG 4.1.3).
2026-05-26 12:08:43 +03:00
c0mrade
9f06526238 fix(a11y): aria-label on icon-only buttons (Subscription copy + InstallationGuide back/QR)
Subscription copy-URL button only had a title attribute; back and QR-open buttons in
InstallationGuide had no accessible name. Add aria-label so screen readers announce
the action (WCAG 2.4.6).
2026-05-26 12:06:15 +03:00
c0mrade
0b07af82d4 perf(admin): migrate AdminDashboard to React Query (refetchInterval, no setInterval)
Replace manual fetchStats/fetchExtendedStats (useState + useEffect + setInterval(30s)
+ console.error) with two useQuery hooks using refetchInterval. Keeps existing variable
names (stats/loading/error/referrers/campaigns/payments/systemInfo) so JSX is unchanged.
handleRestartNode/handleToggleNode/retry button now refetch via the query.
2026-05-26 11:52:14 +03:00
c0mrade
dadf08d005 perf(admin): migrate AdminUsers fetch to React Query
Replace manual useState + useEffect + useCallback + console.error fetch pattern with
useQuery for the users list and stats. Adds caching, dedupe, stale-time, automatic
loading/error state — and the refresh button now uses refetch() (single source of truth).
2026-05-26 11:36:02 +03:00
c0mrade
27ba50747a fix(a11y): radiogroup semantics for Campaign bonus-type selector
AdminCampaignCreate + AdminCampaignEdit: bonus-type button grid now uses
role=radiogroup + aria-labelledby + role=radio + aria-checked (WCAG 4.1.2).
2026-05-26 11:32:32 +03:00
c0mrade
30c4c2ecc6 fix(a11y): radiogroup semantics for AdminInfoPageEditor button groups
Wrap page-type and locale button groups in role=radiogroup + aria-labelledby; mark
each option as role=radio with aria-checked (WCAG 4.1.2).
2026-05-26 11:30:18 +03:00
c0mrade
581a2ffb52 fix(a11y): radiogroup semantics for AdminPromoOfferSend button groups
Wrap template-selection and send-mode button groups in role=radiogroup +
aria-labelledby; mark each option button as role=radio with aria-checked
(WCAG 4.1.2). Labels get matching id for the labelledby relationship.
2026-05-26 11:28:37 +03:00
c0mrade
8d6baf8ad0 refactor(theming): TelegramLoginButton muted text → dark-400 token 2026-05-26 11:26:47 +03:00
c0mrade
a2efb7aa48 refactor(theming): blue → accent in PageLoader + ButtonsTab style picker 2026-05-26 11:25:27 +03:00
c0mrade
9bb1c2339a refactor(theming): blue-* → accent-* in admin/internal usages
Sweep non-brand blue across admin pages, AnalyticsTab, TvQuickConnect, and menuLayout
style picker to accent tokens. Telegram-brand blue kept in BroadcastPreview (TG chat
mockup), the blocking screens (TG channel/bot CTAs), and the SuccessNotificationModal hero.
2026-05-26 11:24:28 +03:00
c0mrade
3df2134cba refactor(theming): orange-* → warning-* for promo/highlight semantics
Sweep orange across SubscriptionPurchase/GiftSubscription/PromoOffersSection/
AdminTrafficUsage/AnalyticsTab/withdrawalUtils to warning-* tokens. Stars brand
yellow→orange gradient kept in TopUpAmount.
2026-05-26 11:22:19 +03:00
c0mrade
d7cbbe077a refactor(theming): yellow-* → warning-* for status/pending semantics
Sweep yellow color classes that signal pending/warning status (admin pending pills,
withdrawal status, risk indicators) → warning-* tokens. Telegram Stars / fortune wheel
yellow kept as brand (TopUpAmount + Wheel + FortuneWheel).
2026-05-26 11:21:16 +03:00
c0mrade
ae7da0c86a refactor(theming): replace gray-* with dark-* tokens on blocking screens
Map text/bg/border-gray-N → dark-N across the four blocking screens (Maintenance,
Blacklisted, ChannelSubscription, AccountDeleted) so muted text + decorative surfaces
follow the design tokens. BroadcastPreview's email mockup and MessageMediaGrid's
fullscreen overlay intentionally keep their literal gray/white (mock chrome).
2026-05-26 11:19:49 +03:00
c0mrade
8a442e8aaa refactor(design): flatten remaining success-gradient CTA in SuccessNotificationModal 2026-05-26 11:18:48 +03:00
c0mrade
122f050a1b refactor(design): flatten accent gradient CTAs to solid bg per design law
Replace gradient CTAs that fade between same-token shades with flat backgrounds
(TopUpAmount accent submit, ChannelSubscriptionScreen single-channel button, three
SuccessNotificationModal action buttons). Stars yellow→orange gradient kept as a
Telegram brand cue; decorative card-header gradients left as visual identity.
2026-05-26 11:17:42 +03:00
c0mrade
feee9f9c05 fix(theming): replace off-token TelegramCallback/VerifyEmail with design tokens
Both pages were rendering with raw Tailwind gray-* + a non-existent border-primary-600
class (invisible spinner). Map: bg-gray-50 → bg-dark-950, text-gray-900 → text-dark-50,
text-gray-500 → text-dark-400, border-primary-600 → border-accent-500. Pages now match
the dark theme and the spinner is visible.
2026-05-26 11:14:58 +03:00
c0mrade
bd8a4fad6e refactor(theming): replace amber-* with warning-* tokens
Sweep all amber-N color classes to warning-N (uniformly pending/warning semantics).
Yellow and orange left for per-file review (mixed Stars brand + semantic uses).
2026-05-26 11:10:50 +03:00
c0mrade
cb7693e3ff refactor(theming): replace green-* / emerald-* with success-* tokens
Sweep: text/bg/border/ring/from/to/via/fill/stroke/shadow-{green,emerald}-N → success-N
(all were semantic success/active/paid states).
2026-05-26 11:09:28 +03:00
c0mrade
9c5e38b594 refactor(theming): replace red-* palette with semantic error-* tokens
Sweep 30 files: text/bg/border/ring/from/to/via/fill/stroke/shadow/divide/decoration/
outline/placeholder-red-N → -error-N. All red usages were semantically error/danger
(no brand red), so tokens now flow through the design-system CSS variables and respond
to palette overrides.
2026-05-26 11:08:22 +03:00
c0mrade
e1b48c1ccb fix(a11y): label associations for AdminTariffCreate fields
Add htmlFor/id pairs for name, description, daily price, traffic limit, device limit
and tier level (WCAG 1.3.1).
2026-05-26 10:48:46 +03:00
c0mrade
1ec6d7e979 fix(a11y): label associations for AdminPromoOfferTemplateEdit fields
Add htmlFor/id pairs for template name, message text, button text, valid hours,
discount percent, test duration, and active discount hours (WCAG 1.3.1). Test-squads
section is a checkbox group, not a single control.
2026-05-26 10:45:29 +03:00
c0mrade
d42fcfa5dd fix(a11y): label associations for AdminServerEdit fields
Add htmlFor/id pairs for display name, description, country code, price, max users
and sort order (WCAG 1.3.1). Original name is a read-only display, not a control.
2026-05-26 10:42:15 +03:00
c0mrade
5a8195f83f fix(a11y): label associations for AdminPromoGroupCreate + AdminPinnedMessageCreate
Add htmlFor/id pairs for the name + auto-assign inputs (AdminPromoGroupCreate) and the
content textarea (AdminPinnedMessageCreate). AdminPromoOfferSend skipped — its labels
sit above button-groups, not native controls (a separate role=group/radiogroup pass).
2026-05-26 10:39:22 +03:00
c0mrade
a635ca2316 fix(a11y): label associations for AdminPromocodeCreate fields
Add htmlFor/id pairs for code, type, balance bonus, days count, tariff, promo group,
discount percent, discount validity, max uses and valid-until inputs (WCAG 1.3.1).
2026-05-26 10:37:01 +03:00
c0mrade
6f66cb6759 fix(a11y): label associations for AdminCampaignEdit fields
Add htmlFor/id pairs for tariff selector, partner selector, name, start parameter,
subscription days/traffic/devices and tariff duration days inputs (WCAG 1.3.1).
2026-05-26 10:24:58 +03:00
c0mrade
4d18be848d fix(a11y): label associations for AdminCampaignCreate fields
Add htmlFor/id pairs for tariff selector, name, start parameter, trial subscription
days/traffic/devices and tariff duration days inputs (WCAG 1.3.1). Group labels (server
selector, bonus-type buttons) deferred — they need role=group, not htmlFor.
2026-05-26 10:20:56 +03:00
c0mrade
a4897243ba fix(a11y): label associations for AdminInfoPageEditor fields
Add htmlFor/id pairs for slug, icon, sort-order, replaces-tab and title inputs (WCAG 1.3.1).
2026-05-26 00:40:12 +03:00
c0mrade
fefc52fb20 fix(a11y): label associations for AdminNewsCreate fields
- htmlFor/id pairs for title, slug, read-time, excerpt and featured-image inputs
- add an ariaLabel prop to ColoredItemCombobox (applied to the trigger button) and
  pass the category/tag labels so the comboboxes have an accessible name (WCAG 1.3.1/4.1.2)
2026-05-26 00:20:30 +03:00
c0mrade
1b40c3856d fix(a11y): associate referral form labels with their inputs
Add htmlFor/id pairs across the partner-apply form (company, channel, website,
description, expected referrals, desired commission) and the withdrawal-request form
(amount, payment details) so labels are programmatically tied to their controls (WCAG 1.3.1).
2026-05-26 00:06:59 +03:00
c0mrade
7b0a72db5b fix(a11y): switch semantics for admin form toggles
Add role=switch + aria-checked + aria-label to 12 ad-hoc toggle buttons across admin
create/edit forms (AdminCampaignCreate/Edit, AdminPromocodeCreate, AdminPromoGroupCreate,
AdminPromoOfferTemplateEdit, AdminPaymentMethodEdit, AdminTariffCreate) so screen readers
announce on/off state (WCAG 4.1.2).
2026-05-26 00:03:17 +03:00
c0mrade
491f48a331 fix(a11y): switch semantics for user-flow toggles
Add role=switch + aria-checked + aria-label to the custom toggle buttons so screen
readers announce on/off state (WCAG 4.1.2): autopay (Subscription) and the custom-days
/ custom-traffic toggles (SubscriptionPurchase).
2026-05-25 23:57:36 +03:00
c0mrade
90888db48c fix(a11y): associate Support ticket form labels with their inputs
Add htmlFor/id pairs for the create-ticket subject and message fields so screen
readers announce the label and clicking the label focuses the control (WCAG 1.3.1).
2026-05-25 23:53:42 +03:00
c0mrade
5c7d4b4888 feat(a11y): dialog semantics + focus trap for the onboarding tour
- role=dialog/aria-modal/aria-labelledby/aria-describedby on the onboarding tooltip
- trap focus inside the tooltip while the tour runs; Esc skips (lockScroll off so
  scrollIntoView can still bring each step's target into view)
- merge the focus-trap ref with the existing measurement ref via a callback ref
- aria-hidden on the click-to-advance target overlay (redundant with the Next button)
2026-05-25 23:51:26 +03:00