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 {
|
||||
channel: BroadcastChannel;
|
||||
target: string;
|
||||
// Broadcast category for user notification preference filtering
|
||||
category?: 'system' | 'news' | 'promo';
|
||||
// Telegram fields
|
||||
message_text?: string;
|
||||
selected_buttons?: string[];
|
||||
@@ -111,6 +113,8 @@ export interface Broadcast {
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
progress_percent: number;
|
||||
// Broadcast category
|
||||
category?: 'system' | 'news' | 'promo';
|
||||
// New fields for channel support
|
||||
channel?: BroadcastChannel;
|
||||
email_subject?: string | null;
|
||||
|
||||
@@ -97,6 +97,7 @@ export const authApi = {
|
||||
first_name?: string;
|
||||
language?: string;
|
||||
referral_code?: string;
|
||||
campaign_slug?: string;
|
||||
}): Promise<RegisterResponse> => {
|
||||
const response = await apiClient.post<RegisterResponse>(
|
||||
'/cabinet/auth/email/register/standalone',
|
||||
|
||||
@@ -573,8 +573,10 @@ export function MenuEditorTab() {
|
||||
setDraftConfig(data);
|
||||
queryClient.setQueryData(['menu-layout'], data);
|
||||
},
|
||||
onError: () => {
|
||||
notify.error(t('common.error'));
|
||||
onError: (err: unknown) => {
|
||||
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 btn of row.buttons) {
|
||||
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'));
|
||||
return;
|
||||
}
|
||||
|
||||
10
src/main.tsx
10
src/main.tsx
@@ -27,6 +27,16 @@ import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegra
|
||||
import './i18n';
|
||||
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
|
||||
const isTelegramEnv =
|
||||
!!(window as unknown as Record<string, unknown>).TelegramWebviewProxy ||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AdminBackButton } from '../components/admin/AdminBackButton';
|
||||
import {
|
||||
banSystemApi,
|
||||
type BanSystemStatus,
|
||||
@@ -557,6 +558,7 @@ export default function AdminBanSystem() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<AdminBackButton />
|
||||
<div className="rounded-xl bg-error-500/20 p-3">
|
||||
<ShieldIcon />
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,9 @@ export default function AdminBroadcastCreate() {
|
||||
const [showTelegramFilters, setShowTelegramFilters] = useState(false);
|
||||
const [showEmailFilters, setShowEmailFilters] = useState(false);
|
||||
|
||||
// Broadcast category (system/news/promo)
|
||||
const [category, setCategory] = useState<'system' | 'news' | 'promo'>('system');
|
||||
|
||||
// Telegram-specific state
|
||||
const [messageText, setMessageText] = useState('');
|
||||
const [selectedButtons, setSelectedButtons] = useState<string[]>(['home']);
|
||||
@@ -395,6 +398,7 @@ export default function AdminBroadcastCreate() {
|
||||
message_text: messageText,
|
||||
selected_buttons: selectedButtons,
|
||||
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
||||
category,
|
||||
};
|
||||
if (uploadedFileId) {
|
||||
data.media = { type: mediaType, file_id: uploadedFileId };
|
||||
@@ -409,6 +413,7 @@ export default function AdminBroadcastCreate() {
|
||||
target: emailTarget,
|
||||
email_subject: emailSubject,
|
||||
email_html_content: emailContent,
|
||||
category,
|
||||
};
|
||||
createMutation.mutate(data);
|
||||
return;
|
||||
@@ -423,6 +428,7 @@ export default function AdminBroadcastCreate() {
|
||||
message_text: messageText,
|
||||
selected_buttons: selectedButtons,
|
||||
custom_buttons: customButtons.length > 0 ? customButtons : undefined,
|
||||
category,
|
||||
};
|
||||
if (uploadedFileId) {
|
||||
telegramData.media = { type: mediaType, file_id: uploadedFileId };
|
||||
@@ -433,6 +439,7 @@ export default function AdminBroadcastCreate() {
|
||||
target: emailTarget,
|
||||
email_subject: emailSubject,
|
||||
email_html_content: emailContent,
|
||||
category,
|
||||
};
|
||||
|
||||
await adminBroadcastsApi.createCombined(telegramData);
|
||||
@@ -583,6 +590,36 @@ export default function AdminBroadcastCreate() {
|
||||
)}
|
||||
</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 */}
|
||||
{telegramEnabled && (
|
||||
<div className="card space-y-6">
|
||||
|
||||
@@ -720,6 +720,16 @@ export default function Login() {
|
||||
value={password}
|
||||
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>
|
||||
|
||||
{authMode === 'register' && (
|
||||
|
||||
@@ -584,7 +584,7 @@ export default function Subscription() {
|
||||
|
||||
{/* Status badge */}
|
||||
<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={{
|
||||
background: subscription.is_active
|
||||
? `${zone.mainHex}15`
|
||||
|
||||
@@ -359,12 +359,14 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
registerWithEmail: async (email, password, firstName, referralCode) => {
|
||||
const code = referralCode || getPendingReferralCode() || undefined;
|
||||
const campaignSlug = getPendingCampaignSlug() || undefined;
|
||||
const response = await authApi.registerEmailStandalone({
|
||||
email,
|
||||
password,
|
||||
first_name: firstName,
|
||||
language: navigator.language.split('-')[0] || 'ru',
|
||||
referral_code: code,
|
||||
campaign_slug: campaignSlug,
|
||||
});
|
||||
consumeReferralCode();
|
||||
return response;
|
||||
|
||||
Reference in New Issue
Block a user