From a65e67ce5dca3a498a8b9fcceec8ef14f4b6697f Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 17 Jan 2026 09:49:01 +0300 Subject: [PATCH] Add files via upload --- src/components/PromoOffersSection.tsx | 255 ++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/components/PromoOffersSection.tsx diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx new file mode 100644 index 0000000..29aa452 --- /dev/null +++ b/src/components/PromoOffersSection.tsx @@ -0,0 +1,255 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' +import { promoApi, PromoOffer, ActiveDiscount } from '../api/promo' + +// Icons +const GiftIcon = () => ( + + + +) + +const ClockIcon = () => ( + + + +) + +const SparklesIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + +const ServerIcon = () => ( + + + +) + +// Helper functions +const formatTimeLeft = (expiresAt: string): string => { + const now = new Date() + const expires = new Date(expiresAt) + const diffMs = expires.getTime() - now.getTime() + + if (diffMs <= 0) return 'Истекло' + + const hours = Math.floor(diffMs / (1000 * 60 * 60)) + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) + + if (hours > 24) { + const days = Math.floor(hours / 24) + return `${days} дн.` + } + if (hours > 0) { + return `${hours}ч ${minutes}м` + } + return `${minutes}м` +} + +const getOfferIcon = (effectType: string) => { + if (effectType === 'test_access') return + return +} + +const getOfferTitle = (offer: PromoOffer): string => { + if (offer.effect_type === 'test_access') { + return 'Тестовый доступ' + } + if (offer.discount_percent) { + return `Скидка ${offer.discount_percent}%` + } + return 'Специальное предложение' +} + +const getOfferDescription = (offer: PromoOffer): string => { + if (offer.effect_type === 'test_access') { + const squads = offer.extra_data?.test_squad_uuids?.length || 0 + return squads > 0 ? `Доступ к ${squads} серверам` : 'Доступ к дополнительным серверам' + } + return 'Активируйте скидку на покупку подписки' +} + +interface PromoOffersSectionProps { + className?: string +} + +export default function PromoOffersSection({ className = '' }: PromoOffersSectionProps) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const [claimingId, setClaimingId] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + + // Fetch available offers + const { data: offers = [], isLoading: offersLoading } = useQuery({ + queryKey: ['promo-offers'], + queryFn: promoApi.getOffers, + staleTime: 30000, + }) + + // Fetch active discount + const { data: activeDiscount } = useQuery({ + queryKey: ['active-discount'], + queryFn: promoApi.getActiveDiscount, + staleTime: 30000, + }) + + // Claim offer mutation + const claimMutation = useMutation({ + mutationFn: promoApi.claimOffer, + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['promo-offers'] }) + queryClient.invalidateQueries({ queryKey: ['active-discount'] }) + queryClient.invalidateQueries({ queryKey: ['subscription'] }) + setSuccessMessage(result.message) + setClaimingId(null) + setTimeout(() => setSuccessMessage(null), 5000) + }, + onError: (error: any) => { + setErrorMessage(error.response?.data?.detail || 'Не удалось активировать предложение') + setClaimingId(null) + setTimeout(() => setErrorMessage(null), 5000) + }, + }) + + const handleClaim = (offerId: number) => { + setClaimingId(offerId) + setErrorMessage(null) + setSuccessMessage(null) + claimMutation.mutate(offerId) + } + + // Filter unclaimed and active offers + const availableOffers = offers.filter(o => o.is_active && !o.is_claimed) + const claimedOffers = offers.filter(o => o.is_claimed) + + // Don't render if no offers and no active discount + if (!offersLoading && availableOffers.length === 0 && (!activeDiscount || !activeDiscount.is_active)) { + return null + } + + return ( +
+ {/* Active Discount Banner */} + {activeDiscount && activeDiscount.is_active && activeDiscount.discount_percent > 0 && ( +
+
+
+ +
+
+
+

+ Скидка {activeDiscount.discount_percent}% активна +

+ + Действует + +
+
+ {activeDiscount.expires_at && ( +
+ + Истекает: {formatTimeLeft(activeDiscount.expires_at)} +
+ )} +
+
+
+
+ )} + + {/* Success/Error Messages */} + {successMessage && ( +
+ + {successMessage} +
+ )} + + {errorMessage && ( +
+ {errorMessage} +
+ )} + + {/* Available Offers */} + {availableOffers.length > 0 && ( +
+ {availableOffers.map((offer) => ( +
+
+
+ {getOfferIcon(offer.effect_type)} +
+
+
+

+ {getOfferTitle(offer)} +

+ {offer.effect_type === 'test_access' && ( + + Тест + + )} +
+

+ {getOfferDescription(offer)} +

+
+
+ + Осталось: {formatTimeLeft(offer.expires_at)} +
+ +
+
+
+
+ ))} +
+ )} + + {/* Loading State */} + {offersLoading && ( +
+
+
+
+
+
+
+
+
+ )} +
+ ) +}