feat: add multi-channel subscription blocking UI and admin management

- Channel subscription blocking screen with per-channel status display
- Safe URL handling with protocol validation (no open redirect)
- Race-condition-safe subscription check with useRef guard
- Admin channel management page with create, toggle, delete
- Error handlers on all admin mutations with haptic feedback
- Proper type guard for channel subscription error detection
- Accessibility: aria-labels on icon-only buttons
- Localization: en, ru, zh, fa translations
This commit is contained in:
Fringg
2026-02-24 02:50:52 +03:00
parent 8ea9a1d6a0
commit a767fe96d3
12 changed files with 758 additions and 113 deletions

68
src/api/adminChannels.ts Normal file
View File

@@ -0,0 +1,68 @@
import apiClient from './client';
// Types
export interface RequiredChannel {
id: number;
channel_id: string;
channel_link: string | null;
title: string | null;
is_active: boolean;
sort_order: number;
}
export interface ChannelListResponse {
items: RequiredChannel[];
total: number;
}
export interface CreateChannelRequest {
channel_id: string;
channel_link?: string;
title?: string;
}
export interface UpdateChannelRequest {
channel_id?: string;
channel_link?: string;
title?: string;
is_active?: boolean;
sort_order?: number;
}
export const adminChannelsApi = {
list: async (): Promise<ChannelListResponse> => {
const { data } = await apiClient.get<ChannelListResponse>(
'/cabinet/admin/channel-subscriptions',
);
return data;
},
create: async (req: CreateChannelRequest): Promise<RequiredChannel> => {
const { data } = await apiClient.post<RequiredChannel>(
'/cabinet/admin/channel-subscriptions',
req,
);
return data;
},
update: async (id: number, req: UpdateChannelRequest): Promise<RequiredChannel> => {
const { data } = await apiClient.patch<RequiredChannel>(
`/cabinet/admin/channel-subscriptions/${id}`,
req,
);
return data;
},
toggle: async (id: number): Promise<RequiredChannel> => {
const { data } = await apiClient.post<RequiredChannel>(
`/cabinet/admin/channel-subscriptions/${id}/toggle`,
);
return data;
},
delete: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/channel-subscriptions/${id}`);
},
};
export default adminChannelsApi;

View File

@@ -133,6 +133,12 @@ export interface ChannelSubscriptionError {
code: 'channel_subscription_required';
message: string;
channel_link?: string;
channels?: Array<{
channel_id: string;
channel_link?: string;
title?: string;
is_subscribed: boolean;
}>;
}
export interface BlacklistedError {
@@ -189,6 +195,7 @@ apiClient.interceptors.response.use(
useBlockingStore.getState().setChannelSubscription({
message: detail.message,
channel_link: detail.channel_link,
channels: detail.channels,
});
return Promise.reject(error);
}

View File

@@ -12,6 +12,7 @@ export { infoApi } from './info';
export { adminSettingsApi } from './adminSettings';
export { adminApi, statsApi } from './admin';
export { adminAppsApi } from './adminApps';
export { adminChannelsApi } from './adminChannels';
export { adminBroadcastsApi } from './adminBroadcasts';
export { adminEmailTemplatesApi } from './adminEmailTemplates';
export { adminPaymentMethodsApi } from './adminPaymentMethods';