mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Merge pull request #390 from BEDOLAGA-DEV/dev
Bugfixes: polyfill, UI fixes, campaign tracking, menu editor
This commit is contained in:
@@ -76,6 +76,8 @@ export interface EmailBroadcastCreateRequest {
|
|||||||
export interface CombinedBroadcastCreateRequest {
|
export interface CombinedBroadcastCreateRequest {
|
||||||
channel: BroadcastChannel;
|
channel: BroadcastChannel;
|
||||||
target: string;
|
target: string;
|
||||||
|
// Broadcast category for user notification preference filtering
|
||||||
|
category?: 'system' | 'news' | 'promo';
|
||||||
// Telegram fields
|
// Telegram fields
|
||||||
message_text?: string;
|
message_text?: string;
|
||||||
selected_buttons?: string[];
|
selected_buttons?: string[];
|
||||||
@@ -111,6 +113,8 @@ export interface Broadcast {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
completed_at: string | null;
|
completed_at: string | null;
|
||||||
progress_percent: number;
|
progress_percent: number;
|
||||||
|
// Broadcast category
|
||||||
|
category?: 'system' | 'news' | 'promo';
|
||||||
// New fields for channel support
|
// New fields for channel support
|
||||||
channel?: BroadcastChannel;
|
channel?: BroadcastChannel;
|
||||||
email_subject?: string | null;
|
email_subject?: string | null;
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const authApi = {
|
|||||||
first_name?: string;
|
first_name?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
referral_code?: string;
|
referral_code?: string;
|
||||||
|
campaign_slug?: string;
|
||||||
}): Promise<RegisterResponse> => {
|
}): Promise<RegisterResponse> => {
|
||||||
const response = await apiClient.post<RegisterResponse>(
|
const response = await apiClient.post<RegisterResponse>(
|
||||||
'/cabinet/auth/email/register/standalone',
|
'/cabinet/auth/email/register/standalone',
|
||||||
|
|||||||
@@ -573,8 +573,10 @@ export function MenuEditorTab() {
|
|||||||
setDraftConfig(data);
|
setDraftConfig(data);
|
||||||
queryClient.setQueryData(['menu-layout'], data);
|
queryClient.setQueryData(['menu-layout'], data);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (err: unknown) => {
|
||||||
notify.error(t('common.error'));
|
const error = err as { response?: { data?: { detail?: string } } };
|
||||||
|
const detail = error.response?.data?.detail;
|
||||||
|
notify.error(detail || t('common.error'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -748,7 +750,12 @@ export function MenuEditorTab() {
|
|||||||
for (const row of currentDraft.rows) {
|
for (const row of currentDraft.rows) {
|
||||||
for (const btn of row.buttons) {
|
for (const btn of row.buttons) {
|
||||||
if (btn.type === 'custom') {
|
if (btn.type === 'custom') {
|
||||||
if (!btn.url || (!btn.url.startsWith('http://') && !btn.url.startsWith('https://'))) {
|
if (
|
||||||
|
!btn.url ||
|
||||||
|
(!btn.url.startsWith('http://') &&
|
||||||
|
!btn.url.startsWith('https://') &&
|
||||||
|
!btn.url.startsWith('tg://'))
|
||||||
|
) {
|
||||||
notify.error(t('admin.menuEditor.invalidUrl'));
|
notify.error(t('admin.menuEditor.invalidUrl'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main.tsx
10
src/main.tsx
@@ -27,6 +27,16 @@ import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegra
|
|||||||
import './i18n';
|
import './i18n';
|
||||||
import './styles/globals.css';
|
import './styles/globals.css';
|
||||||
|
|
||||||
|
// Polyfill Object.hasOwn for older iOS/Android WebViews (Safari < 15.4, old Chrome).
|
||||||
|
// @telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn internally.
|
||||||
|
// Without this, init() throws LaunchParamsRetrieveError on affected devices.
|
||||||
|
// See: https://github.com/Telegram-Mini-Apps/tma.js/issues/683
|
||||||
|
if (typeof (Object as { hasOwn?: unknown }).hasOwn !== 'function') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(Object as any).hasOwn = (obj: object, prop: PropertyKey): boolean =>
|
||||||
|
Object.prototype.hasOwnProperty.call(obj, prop);
|
||||||
|
}
|
||||||
|
|
||||||
// Only initialize Telegram SDK when running inside Telegram
|
// Only initialize Telegram SDK when running inside Telegram
|
||||||
const isTelegramEnv =
|
const isTelegramEnv =
|
||||||
!!(window as unknown as Record<string, unknown>).TelegramWebviewProxy ||
|
!!(window as unknown as Record<string, unknown>).TelegramWebviewProxy ||
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AdminBackButton } from '../components/admin/AdminBackButton';
|
||||||
import {
|
import {
|
||||||
banSystemApi,
|
banSystemApi,
|
||||||
type BanSystemStatus,
|
type BanSystemStatus,
|
||||||
@@ -557,6 +558,7 @@ export default function AdminBanSystem() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<AdminBackButton />
|
||||||
<div className="rounded-xl bg-error-500/20 p-3">
|
<div className="rounded-xl bg-error-500/20 p-3">
|
||||||
<ShieldIcon />
|
<ShieldIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ export default function AdminBroadcastCreate() {
|
|||||||
const [showTelegramFilters, setShowTelegramFilters] = useState(false);
|
const [showTelegramFilters, setShowTelegramFilters] = useState(false);
|
||||||
const [showEmailFilters, setShowEmailFilters] = useState(false);
|
const [showEmailFilters, setShowEmailFilters] = useState(false);
|
||||||
|
|
||||||
|
// Broadcast category (system/news/promo)
|
||||||
|
const [category, setCategory] = useState<'system' | 'news' | 'promo'>('system');
|
||||||
|
|
||||||
// Telegram-specific state
|
// Telegram-specific state
|
||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
const [selectedButtons, setSelectedButtons] = useState<string[]>(['home']);
|
const [selectedButtons, setSelectedButtons] = useState<string[]>(['home']);
|
||||||
@@ -395,6 +398,7 @@ export default function AdminBroadcastCreate() {
|
|||||||
message_text: messageText,
|
message_text: messageText,
|
||||||
selected_buttons: selectedButtons,
|
selected_buttons: selectedButtons,
|
||||||
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
||||||
|
category,
|
||||||
};
|
};
|
||||||
if (uploadedFileId) {
|
if (uploadedFileId) {
|
||||||
data.media = { type: mediaType, file_id: uploadedFileId };
|
data.media = { type: mediaType, file_id: uploadedFileId };
|
||||||
@@ -409,6 +413,7 @@ export default function AdminBroadcastCreate() {
|
|||||||
target: emailTarget,
|
target: emailTarget,
|
||||||
email_subject: emailSubject,
|
email_subject: emailSubject,
|
||||||
email_html_content: emailContent,
|
email_html_content: emailContent,
|
||||||
|
category,
|
||||||
};
|
};
|
||||||
createMutation.mutate(data);
|
createMutation.mutate(data);
|
||||||
return;
|
return;
|
||||||
@@ -423,6 +428,7 @@ export default function AdminBroadcastCreate() {
|
|||||||
message_text: messageText,
|
message_text: messageText,
|
||||||
selected_buttons: selectedButtons,
|
selected_buttons: selectedButtons,
|
||||||
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
||||||
|
category,
|
||||||
};
|
};
|
||||||
if (uploadedFileId) {
|
if (uploadedFileId) {
|
||||||
telegramData.media = { type: mediaType, file_id: uploadedFileId };
|
telegramData.media = { type: mediaType, file_id: uploadedFileId };
|
||||||
@@ -433,6 +439,7 @@ export default function AdminBroadcastCreate() {
|
|||||||
target: emailTarget,
|
target: emailTarget,
|
||||||
email_subject: emailSubject,
|
email_subject: emailSubject,
|
||||||
email_html_content: emailContent,
|
email_html_content: emailContent,
|
||||||
|
category,
|
||||||
};
|
};
|
||||||
|
|
||||||
await adminBroadcastsApi.createCombined(telegramData);
|
await adminBroadcastsApi.createCombined(telegramData);
|
||||||
@@ -583,6 +590,36 @@ export default function AdminBroadcastCreate() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Broadcast category */}
|
||||||
|
<div className="card">
|
||||||
|
<label className="mb-3 block text-sm font-medium text-dark-300">
|
||||||
|
{t('admin.broadcasts.category', 'Категория рассылки')}
|
||||||
|
</label>
|
||||||
|
<p className="mb-3 text-xs text-dark-500">
|
||||||
|
{t(
|
||||||
|
'admin.broadcasts.categoryDesc',
|
||||||
|
'Пользователи могут отключить получение новостей и промо в настройках профиля. Системные рассылки доставляются всем.',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{(['system', 'news', 'promo'] as const).map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => setCategory(cat)}
|
||||||
|
className={`flex-1 rounded-lg border p-3 text-sm font-medium transition-all ${
|
||||||
|
category === cat
|
||||||
|
? 'border-accent-500 bg-accent-500/10 text-accent-400'
|
||||||
|
: 'border-dark-700 bg-dark-800 text-dark-300 hover:border-dark-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat === 'system' && t('admin.broadcasts.categorySystem', '⚙️ Системное')}
|
||||||
|
{cat === 'news' && t('admin.broadcasts.categoryNews', '📰 Новости')}
|
||||||
|
{cat === 'promo' && t('admin.broadcasts.categoryPromo', '🎁 Промо')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Telegram section */}
|
{/* Telegram section */}
|
||||||
{telegramEnabled && (
|
{telegramEnabled && (
|
||||||
<div className="card space-y-6">
|
<div className="card space-y-6">
|
||||||
|
|||||||
@@ -720,6 +720,16 @@ export default function Login() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
{authMode === 'register' &&
|
||||||
|
password.length > 0 &&
|
||||||
|
password.length < 8 && (
|
||||||
|
<p className="mt-1.5 text-xs text-error-400">
|
||||||
|
{t(
|
||||||
|
'auth.passwordTooShort',
|
||||||
|
'Password must be at least 8 characters',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{authMode === 'register' && (
|
{authMode === 'register' && (
|
||||||
|
|||||||
@@ -584,7 +584,7 @@ export default function Subscription() {
|
|||||||
|
|
||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
<span
|
<span
|
||||||
className="rounded-full px-3 py-1 font-mono text-[10px] font-semibold uppercase tracking-wider"
|
className="max-w-[55%] shrink-0 rounded-full px-3 py-1 text-center font-mono text-[10px] font-semibold uppercase tracking-wider"
|
||||||
style={{
|
style={{
|
||||||
background: subscription.is_active
|
background: subscription.is_active
|
||||||
? `${zone.mainHex}15`
|
? `${zone.mainHex}15`
|
||||||
|
|||||||
@@ -359,12 +359,14 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
|
|
||||||
registerWithEmail: async (email, password, firstName, referralCode) => {
|
registerWithEmail: async (email, password, firstName, referralCode) => {
|
||||||
const code = referralCode || getPendingReferralCode() || undefined;
|
const code = referralCode || getPendingReferralCode() || undefined;
|
||||||
|
const campaignSlug = getPendingCampaignSlug() || undefined;
|
||||||
const response = await authApi.registerEmailStandalone({
|
const response = await authApi.registerEmailStandalone({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
first_name: firstName,
|
first_name: firstName,
|
||||||
language: navigator.language.split('-')[0] || 'ru',
|
language: navigator.language.split('-')[0] || 'ru',
|
||||||
referral_code: code,
|
referral_code: code,
|
||||||
|
campaign_slug: campaignSlug,
|
||||||
});
|
});
|
||||||
consumeReferralCode();
|
consumeReferralCode();
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
Reference in New Issue
Block a user