From bce4d94229c81d67a7621faccf9a69bd6b61d5e9 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Sun, 1 Feb 2026 23:03:25 +0300 Subject: [PATCH] feat: replace campaign creation modal with dedicated page - Create AdminCampaignCreate.tsx with full form for new campaigns - Update AdminCampaigns.tsx to navigate to create page - Keep edit functionality as modal - Add route /admin/campaigns/create in App.tsx --- src/App.tsx | 11 + src/pages/AdminCampaignCreate.tsx | 447 ++++++++++++++++++++++++++++++ src/pages/AdminCampaigns.tsx | 64 ++--- 3 files changed, 481 insertions(+), 41 deletions(-) create mode 100644 src/pages/AdminCampaignCreate.tsx diff --git a/src/App.tsx b/src/App.tsx index 2a9cd0f..60665f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ const AdminBroadcasts = lazy(() => import('./pages/AdminBroadcasts')); const AdminBroadcastCreate = lazy(() => import('./pages/AdminBroadcastCreate')); const AdminPromocodes = lazy(() => import('./pages/AdminPromocodes')); const AdminCampaigns = lazy(() => import('./pages/AdminCampaigns')); +const AdminCampaignCreate = lazy(() => import('./pages/AdminCampaignCreate')); const AdminUsers = lazy(() => import('./pages/AdminUsers')); const AdminPayments = lazy(() => import('./pages/AdminPayments')); const AdminPaymentMethods = lazy(() => import('./pages/AdminPaymentMethods')); @@ -358,6 +359,16 @@ function App() { } /> + + + + + + } + /> ( + + + +); + +const CheckIcon = () => ( + + + +); + +const RefreshIcon = () => ( + + + +); + +// Bonus type config +const bonusTypeConfig: Record< + CampaignBonusType, + { labelKey: string; color: string; bgColor: string; borderColor: string } +> = { + balance: { + labelKey: 'admin.campaigns.bonusType.balance', + color: 'text-success-400', + bgColor: 'bg-success-500/10', + borderColor: 'border-success-500/30', + }, + subscription: { + labelKey: 'admin.campaigns.bonusType.subscription', + color: 'text-accent-400', + bgColor: 'bg-accent-500/10', + borderColor: 'border-accent-500/30', + }, + tariff: { + labelKey: 'admin.campaigns.bonusType.tariff', + color: 'text-accent-400', + bgColor: 'bg-accent-500/10', + borderColor: 'border-accent-500/30', + }, + none: { + labelKey: 'admin.campaigns.bonusType.none', + color: 'text-dark-400', + bgColor: 'bg-dark-500/10', + borderColor: 'border-dark-500/30', + }, +}; + +// Server selector component +function ServerSelector({ + servers, + selected, + onToggle, +}: { + servers: ServerSquadInfo[]; + selected: string[]; + onToggle: (uuid: string) => void; +}) { + const { t } = useTranslation(); + + if (servers.length === 0) return null; + + return ( +
+ +
+ {servers.map((server) => ( + + ))} +
+
+ ); +} + +// Tariff selector component +function TariffSelector({ + tariffs, + value, + onChange, +}: { + tariffs: TariffListItem[]; + value: number | null; + onChange: (id: number | null) => void; +}) { + const { t } = useTranslation(); + + return ( +
+ + +
+ ); +} + +export default function AdminCampaignCreate() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + // Form state + const [name, setName] = useState(''); + const [startParameter, setStartParameter] = useState(''); + const [bonusType, setBonusType] = useState('balance'); + const [isActive, setIsActive] = useState(true); + + // Balance bonus + const [balanceBonusRubles, setBalanceBonusRubles] = useState(0); + + // Subscription bonus + const [subscriptionDays, setSubscriptionDays] = useState(7); + const [subscriptionTraffic, setSubscriptionTraffic] = useState(10); + const [subscriptionDevices, setSubscriptionDevices] = useState(1); + const [selectedSquads, setSelectedSquads] = useState([]); + + // Tariff bonus + const [tariffId, setTariffId] = useState(null); + const [tariffDays, setTariffDays] = useState(30); + + // Fetch servers + const { data: servers = [] } = useQuery({ + queryKey: ['admin-campaigns-servers'], + queryFn: () => campaignsApi.getAvailableServers(), + }); + + // Fetch tariffs + const { data: tariffs = [] } = useQuery({ + queryKey: ['admin-campaigns-tariffs'], + queryFn: () => campaignsApi.getAvailableTariffs(), + }); + + // Create mutation + const createMutation = useMutation({ + mutationFn: campaignsApi.createCampaign, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] }); + queryClient.invalidateQueries({ queryKey: ['admin-campaigns-overview'] }); + navigate('/admin/campaigns'); + }, + }); + + const toggleServer = (uuid: string) => { + setSelectedSquads((prev) => + prev.includes(uuid) ? prev.filter((s) => s !== uuid) : [...prev, uuid], + ); + }; + + const handleSubmit = () => { + const data: CampaignCreateRequest = { + name, + start_parameter: startParameter, + bonus_type: bonusType, + is_active: isActive, + }; + + if (bonusType === 'balance') { + data.balance_bonus_kopeks = Math.round(balanceBonusRubles * 100); + } else if (bonusType === 'subscription') { + data.subscription_duration_days = subscriptionDays; + data.subscription_traffic_gb = subscriptionTraffic; + data.subscription_device_limit = subscriptionDevices; + data.subscription_squads = selectedSquads; + } else if (bonusType === 'tariff') { + data.tariff_id = tariffId || undefined; + data.tariff_duration_days = tariffDays; + } + + createMutation.mutate(data); + }; + + const isValid = name.trim() && startParameter.trim() && /^[a-zA-Z0-9_-]+$/.test(startParameter); + + return ( +
+ {/* Header */} +
+ +
+
+ +
+
+

+ {t('admin.campaigns.modal.createTitle')} +

+

{t('admin.campaigns.subtitle')}

+
+
+
+ + {/* Basic Info */} +
+ {/* Name */} +
+ + setName(e.target.value)} + className="input" + placeholder={t('admin.campaigns.form.namePlaceholder')} + /> +
+ + {/* Start Parameter */} +
+ + setStartParameter(e.target.value.replace(/[^a-zA-Z0-9_-]/g, ''))} + className="input font-mono" + placeholder="instagram_jan2024" + /> +

+ {t('admin.campaigns.form.startParameterHint')} +

+
+ + {/* Active toggle */} +
+ + {t('admin.campaigns.form.active')} + + +
+
+ + {/* Bonus Type */} +
+

+ {t('admin.campaigns.form.bonusType')} +

+ +
+ {(Object.keys(bonusTypeConfig) as CampaignBonusType[]).map((type) => ( + + ))} +
+
+ + {/* Bonus Settings */} + {bonusType === 'balance' && ( +
+

+ {t('admin.campaigns.form.balanceBonus')} +

+
+ setBalanceBonusRubles(Math.max(0, parseFloat(e.target.value) || 0))} + className="input w-32" + min={0} + step={1} + /> + +
+
+ )} + + {bonusType === 'subscription' && ( +
+

+ {t('admin.campaigns.form.trialSubscription')} +

+ +
+
+ + setSubscriptionDays(Math.max(1, parseInt(e.target.value) || 1))} + className="input" + min={1} + /> +
+
+ + setSubscriptionTraffic(Math.max(0, parseInt(e.target.value) || 0))} + className="input" + min={0} + /> +
+
+ + setSubscriptionDevices(Math.max(1, parseInt(e.target.value) || 1))} + className="input" + min={1} + /> +
+
+ + +
+ )} + + {bonusType === 'tariff' && ( +
+

+ {t('admin.campaigns.form.tariff')} +

+ + + +
+ + setTariffDays(Math.max(1, parseInt(e.target.value) || 1))} + className="input w-32" + min={1} + /> +
+
+ )} + + {bonusType === 'none' && ( +
+

{t('admin.campaigns.form.noBonusDescription')}

+
+ )} + + {/* Footer */} +
+ + +
+
+ ); +} diff --git a/src/pages/AdminCampaigns.tsx b/src/pages/AdminCampaigns.tsx index c161fb2..7cb1139 100644 --- a/src/pages/AdminCampaigns.tsx +++ b/src/pages/AdminCampaigns.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; @@ -7,7 +8,6 @@ import { CampaignListItem, CampaignDetail, CampaignStatistics, - CampaignCreateRequest, CampaignUpdateRequest, CampaignBonusType, ServerSquadInfo, @@ -148,12 +148,12 @@ const formatDate = (dateStr: string | null) => { }); }; -// Campaign Modal +// Campaign Edit Modal interface CampaignModalProps { - campaign?: CampaignDetail | null; + campaign: CampaignDetail; servers: ServerSquadInfo[]; tariffs: TariffListItem[]; - onSave: (data: CampaignCreateRequest | CampaignUpdateRequest) => void; + onSave: (data: CampaignUpdateRequest) => void; onClose: () => void; isLoading?: boolean; } @@ -167,38 +167,37 @@ function CampaignModal({ isLoading, }: CampaignModalProps) { const { t } = useTranslation(); - const isEdit = !!campaign; - const [name, setName] = useState(campaign?.name || ''); - const [startParameter, setStartParameter] = useState(campaign?.start_parameter || ''); - const [bonusType, setBonusType] = useState(campaign?.bonus_type || 'balance'); - const [isActive, setIsActive] = useState(campaign?.is_active ?? true); + const [name, setName] = useState(campaign.name || ''); + const [startParameter, setStartParameter] = useState(campaign.start_parameter || ''); + const [bonusType, setBonusType] = useState(campaign.bonus_type || 'balance'); + const [isActive, setIsActive] = useState(campaign.is_active ?? true); // Balance bonus const [balanceBonusRubles, setBalanceBonusRubles] = useState( - (campaign?.balance_bonus_kopeks || 0) / 100, + (campaign.balance_bonus_kopeks || 0) / 100, ); // Subscription bonus const [subscriptionDays, setSubscriptionDays] = useState( - campaign?.subscription_duration_days || 7, + campaign.subscription_duration_days || 7, ); const [subscriptionTraffic, setSubscriptionTraffic] = useState( - campaign?.subscription_traffic_gb || 10, + campaign.subscription_traffic_gb || 10, ); const [subscriptionDevices, setSubscriptionDevices] = useState( - campaign?.subscription_device_limit || 1, + campaign.subscription_device_limit || 1, ); const [selectedSquads, setSelectedSquads] = useState( - campaign?.subscription_squads || [], + campaign.subscription_squads || [], ); // Tariff bonus - const [tariffId, setTariffId] = useState(campaign?.tariff_id || null); - const [tariffDays, setTariffDays] = useState(campaign?.tariff_duration_days || 30); + const [tariffId, setTariffId] = useState(campaign.tariff_id || null); + const [tariffDays, setTariffDays] = useState(campaign.tariff_duration_days || 30); const handleSubmit = () => { - const data: CampaignCreateRequest | CampaignUpdateRequest = { + const data: CampaignUpdateRequest = { name, start_parameter: startParameter, bonus_type: bonusType, @@ -234,7 +233,7 @@ function CampaignModal({ {/* Header */}

- {isEdit ? t('admin.campaigns.modal.editTitle') : t('admin.campaigns.modal.createTitle')} + {t('admin.campaigns.modal.editTitle')}