@@ -0,0 +1,12 @@ + + + + + + + + + + + + @@ -0,0 +1,5 @@ + + + + + @@ -0,0 +1,7 @@ + + + + + + + @@ -0,0 +1,3 @@ + + + Binary files /dev/null and b/public/subscription/bg_1.png differ Binary files /dev/null and b/public/subscription/bg_2.png differ @@ -234,29 +234,6 @@ const InfoBar: React.FC = ({ device }) => { )} - - - - router.push('/account?scope=subscribe')} - > - {''} - - {' '} - Оплата{' '} - - - - { @@ -2,8 +2,6 @@ import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import styles from './card.module.scss' - import BlockedSvg from '#/assets/svg/blocked.svg?react' import LockSvg from '#/assets/svg/lock.svg?react' import { IShortModel } from '#/entities/model-entity' @@ -12,13 +10,15 @@ import { useThemeAndDevice } from '#/shared/lib/hooks' import { CommonButton } from '#/shared/ui/button' import { SvgIcon } from '#/shared/ui/svg' +import styles from './card.module.scss' + export interface AudioModelCardProps extends IShortModel { accessed_models: string[] | null } export function AudioModelCard({ description, image, title, slug, accessed_models, blocked, tags }: AudioModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `audio/${slug}` + return accessed_models && !accessed_models.includes(slug) ? '/subscription' : `audio/${slug}` }, [accessed_models]) const { theme } = useThemeAndDevice() @@ -1,17 +1,16 @@ -import React, { useEffect, useMemo } from 'react' -import { Avatar, Box, Button, Typography } from '@mui/material' +import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import LockSvg from '#/assets/svg/lock.svg?react' -import BlockedSvg from '#/assets/svg/blocked.svg?react' -import styles from './card-chat.module.scss' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import { c } from '#/shared/lib/helpers' +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import LockSvg from '#/assets/svg/lock.svg?react' import { IShortModel } from '#/entities/model-entity' -import { SvgIcon } from '#/shared/ui/svg' +import { c } from '#/shared/lib/helpers' +import { useThemeAndDevice } from '#/shared/lib/hooks' import { CommonButton } from '#/shared/ui/button' -import { uid } from 'chart.js/dist/helpers/helpers.core' +import { SvgIcon } from '#/shared/ui/svg' + +import styles from './card-chat.module.scss' export interface ChatModelCardProps extends IShortModel { accessed_models: string[] | null @@ -19,7 +18,7 @@ export interface ChatModelCardProps extends IShortModel { export function ChatModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ChatModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` + return accessed_models && !accessed_models.includes(slug) ? '/subscription' : `chat-bot/${slug}` }, [accessed_models]) const { theme } = useThemeAndDevice() @@ -1,10 +1,7 @@ -import React, { useEffect, useMemo } from 'react' -import { Avatar, Box, Button, Typography } from '@mui/material' +import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import styles from './card.module.scss' - import BlockedSvg from '#/assets/svg/blocked.svg?react' import LockSvg from '#/assets/svg/lock.svg?react' import { IShortModel } from '#/entities/model-entity' @@ -13,13 +10,15 @@ import { useThemeAndDevice } from '#/shared/lib/hooks' import { CommonButton } from '#/shared/ui/button' import { SvgIcon } from '#/shared/ui/svg' +import styles from './card.module.scss' + export interface ImageModelCardProps extends IShortModel { accessed_models: string[] | null } export function ImageModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ImageModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `images/${slug}` + return accessed_models && !accessed_models.includes(slug) ? '/subscription' : `images/${slug}` }, [accessed_models]) const { theme } = useThemeAndDevice() @@ -1,10 +1,7 @@ -import React, { useEffect, useMemo } from 'react' -import { Avatar, Box, Button, Typography } from '@mui/material' +import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import styles from './card.module.scss' - import BlockedSvg from '#/assets/svg/blocked.svg?react' import LockSvg from '#/assets/svg/lock.svg?react' import { IShortModel } from '#/entities/model-entity' @@ -13,13 +10,15 @@ import { useThemeAndDevice } from '#/shared/lib/hooks' import { CommonButton } from '#/shared/ui/button' import { SvgIcon } from '#/shared/ui/svg' +import styles from './card.module.scss' + export interface VideoModelCardProps extends IShortModel { accessed_models: string[] | null } export function VideoModelCard({ description, image, title, slug, accessed_models, blocked, tags }: VideoModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `videos/${slug}` + return accessed_models && !accessed_models.includes(slug) ? '/subscription' : `videos/${slug}` }, [accessed_models]) const { theme } = useThemeAndDevice() @@ -0,0 +1,6 @@ +import SubscriptionPage from '#/views/subscription/ui/subscription' +import { getDefaultLayout } from '#/widgets/layouts' + +SubscriptionPage.getLayout = getDefaultLayout() + +export default SubscriptionPage @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { User } from 'next-auth' import { API_URL } from '#/shared/lib/constants' -import { IOffer } from '#/widgets/payment/model/payment' +import { IOffer } from '#/views/subscription' interface IUserBalance { current_token_balance: number @@ -1,32 +0,0 @@ -export const subMenuTitle = { - color: '#5A5A5A', - fontSize: '19px', - fontWeight: '600px', - lineHeight: '28.5px', -} - -export const username = { - color: '#7F7DF3', - fontSize: '19px', - fontWeight: '600px', - lineHeight: '28.5px', -} -export const subscriptionColumn = { - color: '#868686', - fontSize: '15px', - fontWeight: '600px', - lineHeight: '17.61px', -} -export const subMenuTitleNight = { - color: '#E1E1E1', - fontSize: '19px', - fontWeight: '600px', - lineHeight: '28.5px', -} - -export const subscriptionColumnNight = { - color: '#A6A5A5', - fontSize: '15px', - fontWeight: '600px', - lineHeight: '17.61px', -} @@ -90,17 +90,6 @@ export const AccountMenu: React.FC = ({ anchorEl, changeVisible, d > Личный кабинет - - Оплата - = ({ anchorEl, changeVisible, d > Личный кабинет - - Оплата - { Вам доступны 5 токенов, которые можно потратить на любые продукты в AIR. Чтобы продолжить пользоваться платформой вы можете купить токены. - + + + + + + Отмена подписки + + + + Внимание! Вы отменяете подписку на тариф 10 000 токенов. Следующее списание будет отменено, вы можете продолжать пользоваться возможностями тарифа до истечения его срока. + + + + + + + + + ) +} \ No newline at end of file @@ -0,0 +1,73 @@ +import { Box, LinearProgress, linearProgressClasses, Stack, styled, Typography } from '@mui/material' + +import { commaSeparated } from '#/shared' + +import styles from './subscription.module.scss' + +const BorderLinearProgress = styled(LinearProgress)(() => ({ + height: 18, + borderRadius: 6, + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: '#EFF0F2', + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 6, + backgroundColor: '#8280FF', + }, +})) + +interface CurrentPlanAndBalanceProps { + balance: number + planTokenLimit: string + planPrice: string +} + +export const CurrentPlanAndBalance = ({ balance, planTokenLimit, planPrice }: CurrentPlanAndBalanceProps) => { + + const planTokenLimitNumber = Number(planTokenLimit) + const percentage = planTokenLimitNumber > 0 + ? Math.min(100, Math.max(0, (balance / planTokenLimitNumber) * 100)) + : 0 + + return ( + + + + МОЙ ТАРИФ + + + + { commaSeparated(Math.round(Number(planTokenLimit))) } токенов + + {/* ПОКА ПРЯЧЕМ. НЕ ВЫПИЛИВАТЬ! */} + {/* + + {planPrice} ₽ / мес + + + Следующее списание: 03.09.2024 + + */} + + + + + МОЙ БАЛАНС + + + {balance} токенов + {/* {balance >= 100 ? 100 : balance} % */} + {Math.round(percentage)} % + + + + + ) +} + @@ -0,0 +1,60 @@ +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '@mui/material' + +import { faqInfo } from '../lib/faq-data' + +import styles from './subscription.module.scss' + +export const FaqSection = () => { + return ( + + + Частые вопросы + + {faqInfo.map((el, index) => { + return ( + + + } + aria-controls='panel1a-content' + id='panel1a-header' + > + {el.title} + + + {el.text} + + + {index < faqInfo.length - 1 && ( + + )} + + ) + })} + + ) +} + @@ -0,0 +1,95 @@ +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { Box, MenuItem, Select, SelectChangeEvent, Stack, Typography } from '@mui/material' + +import { IFeatures } from '../api' +import { categoryIconMap } from '../lib/category-icon-map' +import { formatFeatureValue } from '../lib/format-feature-value' + +import styles from './subscription.module.scss' + +interface FeaturesSectionProps { + currentTokenLimitUID: string + onCurrentTokenLimitUIDChange: (limit: string) => void + features: IFeatures[] | null + tokenLimits:Array<{value:string, label:string}> +} + +export const FeaturesSection = ({ currentTokenLimitUID, onCurrentTokenLimitUIDChange, features, tokenLimits }: FeaturesSectionProps) => { + return ( + + + + Возможности с подпиской + + + + + {features?.map((featureGroup, idx) => ( + + + {categoryIconMap[featureGroup.name] && ( + + {categoryIconMap[featureGroup.name]} + + )} + + {featureGroup.name} + + + + + {featureGroup.features.map((feature, featureIdx) => { + const formatVal = formatFeatureValue(feature.quantity, feature.measurement_unit) + return ( + + + {feature.name} + + + + {formatVal.number} {formatVal.unit} + + + + ) + })} + + + ))} + + + ) +} + @@ -0,0 +1 @@ +export { default as SubscriptionPage } from './subscription' @@ -0,0 +1,164 @@ +.tarif { + background-color: #1D1D21; + padding: 30px; + border-radius: 30px; + width: 100%; + display: flex; + text-align: left; + flex-direction: column; + position: relative; + transition: all 0.3s ease; + + &::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 30px; + border: 8px solid transparent; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: 0; + } + + &:hover::before { + opacity: 0.4; + border-color: #8280FF; + } + + .tarif_btn { + background-color: #FFFFFF; + color: #151518; + border: 3px solid #FFFFFF; + padding-block: 20px; + border-radius: 20px; + cursor: pointer; + font-weight: 600; + font-style: SemiBold; + font-size: 16px; + line-height: 100%; + letter-spacing: 0px; + vertical-align: middle; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; + + &.tarif_btn_current { + background-color: transparent; + border: 3px solid #FFFFFF; + color: #FFFFFF; + cursor: default; + } + } + + .tarif_price { + font-weight: 600; + font-style: Semi Bold; + font-size: 24px; + line-height: 100%; + letter-spacing: -2%; + vertical-align: middle; + margin-bottom: 15px; + + span { + font-weight: 600; + font-style: Semi Bold; + font-size: 16px; + line-height: 100%; + letter-spacing: -2%; + text-align: center; + vertical-align: middle; + color: #A4AAB5; + } + } + + .tarif_list { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: 20px; + gap: 4px; + margin-bottom: 25px; + position: relative; + z-index: 1; + + font-weight: 400; + font-style: Regular; + font-size: 15px; + line-height: 170%; + letter-spacing: -1%; + + .point { + color: #FFFFFF; + text-align: left; + &::marker { + color: #FFFFFF4D !important; + } + } + } + + .tarif_header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 25px; + position: relative; + z-index: 1; + width: 100%; + } + + .tarif_badge { + display: flex; + align-items: center; + gap: 5px; + width: max-content; + flex: 1; + position: relative; + z-index: 2; + padding-block: 10px; + + span { + font-weight: 700; + font-style: Bold; + font-size: 32px; + line-height: 100%; + letter-spacing: -1%; + text-align: center; + vertical-align: middle; + color: #8280FF; + + @media (max-width: 768px) { + font-size: 25px; + } + } + } + + .tarif_blur_area { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background-color: rgba(130, 128, 255, 0.1); // или другой цвет + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-radius: 12px; + z-index: 1; + pointer-events: none; + } + + &.tarif_bg_1 { + background-image: url('/subscription/bg_1.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + + &.tarif_bg_2 { + background-image: url('/subscription/bg_2.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + +} @@ -0,0 +1,90 @@ +import React from 'react' +import { Box } from '@mui/material' +import Router from 'next/router' +import { useSession } from 'next-auth/react' + +import LightningSvg from '#/assets/svg/lightning.svg?react' +import { accountApi } from '#/shared/api/account-endpoints' +import { commaSeparated } from '#/shared/lib/helpers' +import { IFeatures, IOffer } from '#/views/subscription' + +import styles from './offer.module.scss' + +interface IOfferProps extends IOffer { + device: 'desktop' | 'mobile' + isCurrentSubscription?: boolean + index?: number +} + +const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_features, isCurrentSubscription, index }) => { + const { data } = useSession() + + const unitMap: Record = { + 'Изображения': 'изображений', + 'Чат-боты': 'страниц текста', + 'Видео': 'видео', + 'Аудио': 'аудиозаписей', + } + + + const formatFeatures = (groupedFeatures: IFeatures[] | undefined): string[] => { + if (!groupedFeatures || !Array.isArray(groupedFeatures)) { + return [] + } + + return groupedFeatures.flatMap((group) => { + if (!group?.features || !Array.isArray(group.features)) { + return [] + } + return group.features.map((feature) => { + const group_name = unitMap[group.name] || group.name + + return `${feature.quantity} ${group_name} в ${feature.name} ` + }) + }) + } + + const formattedFeatures = formatFeatures(grouped_features) + + const pay = async (uid: string) => { + const urlForPay = await accountApi.payProduct(data!.access, uid) + urlForPay ? await Router.push(urlForPay) : null + } + + return ( + + + + + {commaSeparated(Math.round(Number(tokens_per_plan)))} токенов + + + + +
    + {formattedFeatures.map((feature, idx) => ( +
  • + {feature} +
  • + ))} +
+

+ {commaSeparated(parseInt(price) ?? 0)} ₽ /мес +

+ { + e.stopPropagation() + if (!isCurrentSubscription) { + pay(uid) + } + }} + > + {isCurrentSubscription ? 'Текущая подписка' : 'Сменить подписку'} + +
+ ) +} + +export default Offer @@ -0,0 +1,37 @@ +import { Box } from '@mui/material' + +import Offer from '#/views/subscription/ui/offer' + +import { IOffer } from '../api' + +import styles from './subscription.module.scss' + +interface PlansSectionProps { + offers: IOffer[] | null + currentOfferUID:string +} + +export const PlansSection = ({ + offers, + currentOfferUID +}: PlansSectionProps) => { + + return ( + + + {offers?.map((offer, idx) => { + return ( + + ) + })} + + + ) +} + @@ -0,0 +1,330 @@ +.container { + width: 85%; + padding-bottom: 100px; +} + +.title{ + font-weight: 600; + font-style: Semi Bold; + font-size: 28px; + line-height: 100%; + letter-spacing: -1%; + vertical-align: bottom; +} + +.blocksMapper{ + display: flex; + flex-direction: row; + justify-content: center; + gap: 20px; + flex: 1; +} + + +// Первый блок +.balanceBox{ + padding: 30px; + background-color: #1D1D21; + flex: 1; + border-radius: 15px; + + + .subTitle{ + font-weight: 600; + font-style: Semi Bold; + font-size: 15px; + line-height: 100%; + letter-spacing: 3%; + vertical-align: middle; + color: #A4AAB5; + } +} + + +// Третий блок +.subscriptionFeatures { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 20px; +} + +.selectContainer { + display: flex; + align-items: center; + gap: 20px; + padding-bottom: 20px; +} + +.tokenSelect { + background-color: #8280FF26; + border: none; + border-radius: 8px; + color: #8280FF; + min-width: 120px; + display: flex; + align-items: center; + + :global(.MuiOutlinedInput-notchedOutline) { + border: none; + } + + &:hover :global(.MuiOutlinedInput-notchedOutline) { + border: none; + } + + &:global(.Mui-focused) :global(.MuiOutlinedInput-notchedOutline) { + border: none; + } + + :global(.MuiSelect-icon) { + color: #8280FF; + font-size: 28px; + } + + :global(.MuiSelect-select) { + padding: 8px 50px 8px 12px; + font-weight: 600; + font-size: 28px; + line-height: 100%; + letter-spacing: -0.01em; + display: flex; + align-items: center; + } +} + +.selectMenuPaper { + background-color: #1D1D21; + border-radius: 8px; + border: none; + margin-top: 8px; + box-shadow: none; +} + +.selectMenuList { + padding: 4px; +} + +.selectMenuItem { + color: #8280FF; + line-height: 100%; + letter-spacing: -0.01em; + border-radius: 4px; + + &:hover { + background-color: #8280FF40; + } + + &:global(.Mui-selected) { + background-color: #8280FF40; + + &:hover { + background-color: #8280FF40; + } + } +} + +// Стили для блоков features +.featureBlock { + background-color: #1D1D21; + border-radius: 15px; + padding: 30px; + flex: 1; +} + +.featureHeader { + margin-bottom: 15px; +} + +.featureIcon { + display: flex; + align-items: center; +} + +.featureTitle { + font-size: 20px; + font-weight: 600; +} + +.featureDivider { + border: none; + border-top: 1px solid #646464; + margin-bottom: 20px; +} + +.featureItem { + display: flex; + justify-content: space-between; + align-items: center; +} + +.featureText { + font-size: 16px; + font-weight: 400; +} + +.featureUnit { + font-weight: 500; + font-style: Medium; + font-size: 12px; + letter-spacing: -1%; + color: #9D9E9E; +} + +// Четвертый блок +.accordionTitle { + font-weight: 600; + font-style: Semi Bold; + font-size: 20px; + line-height: 120%; + letter-spacing: -3%; + color: #8280FF; +} + +.accordionText { + font-weight: 400; + font-style: Regular; + font-size: 16px; + line-height: 140%; + letter-spacing: -3%; + color: #8B8B8B; +} + +.accordionDivider { + border: none; + border-top: 1px solid #646464; + margin: 20px 0; +} + +// Пятый блок +.cancelSubscriptionButtonWrapper { + width: 100%; + text-align: start; + margin-top: 50px; +} + +.cancelSubscriptionButton { + background-color: #B01E1E1A; + padding: 15px 0; + font-weight: 600; + font-style: Semi Bold; + font-size: 20px; + line-height: 100%; + letter-spacing: -1%; + text-align: right; + vertical-align: middle; + color: #B01E1E; + text-transform: none; + border-radius: 0; + + &:hover { + background-color: #B01E1E1A; + } +} + +.cancelSubscriptionModalTitle { + font-size: 30px; + font-weight: 600; + line-height: 100%; + letter-spacing: -0.02em; + vertical-align: bottom; + margin-bottom: 20px; + text-align: left; +} + +.cancelSubscriptionModalText { + font-size: 15px; + font-weight: 700; + line-height: 150%; + letter-spacing: -0.01em; + vertical-align: bottom; + color: #868686; + margin-bottom: 30px; + text-align: left; +} + +.cancelSubscriptionModalButtons { + display: flex; + gap: 8px; + justify-content: flex-start; + margin-top: 20px; +} + +.cancelSubscriptionButtonKeep { + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 140%; + letter-spacing: -0.01em; + vertical-align: middle; + color: #FFFFFF; + background-color: #8280FF; + border: none; + border-radius: 12px; + padding: 15px 25px; + text-transform: none; + + &:hover { + background-color: #6d6ae6; + } +} + +.cancelSubscriptionButtonCancel { + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 140%; + letter-spacing: -0.01em; + vertical-align: middle; + color: #FFFFFF; + background-color: #5D5A5A; + border: none; + border-radius: 12px; + padding: 15px 25px; + text-transform: none; + + &:hover { + background-color: #4a4747; + } +} + +// Класс для контейнера баланса и тарифа +.balanceContainer { + display: flex; + flex-direction: row; + gap: 20px; +} + +// Мобильная версия +@media (max-width: 768px) { + .container { + width: 100%; + padding: 0 16px 50px; + } + + .title{ + font-size: 20px; + } + + .blocksMapper { + flex-direction: column; + } + + .balanceContainer { + flex-direction: column; + } + + .selectContainer { + flex-direction: column; + align-items: flex-start; + } + + .toggleBadge { + position: static; + margin-bottom: 10px; + } + + .balanceBox { + padding: 20px; + } +} \ No newline at end of file @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react' +import { Box, Typography } from '@mui/material' +import { useSession } from 'next-auth/react' + +import { useAppSelector } from '#/app/store/store' +import { NextPageWithLayout } from '#/pages/_app' +import { commaSeparated } from '#/shared' +import { accountApi } from '#/shared/api/account-endpoints' +import { balanceSelector } from '#/shared/lib/selectors' + +import { IFeatures, IOffer } from '../api' + +import { CurrentPlanAndBalance } from './current-plan-and-balance' +import { FeaturesSection } from './features-section' +import { PlansSection } from './plans-section' + +import styles from './subscription.module.scss' + +const SubscriptionPage: NextPageWithLayout = () => { + + const [offers, setOffers] = useState(null) + const [currentTokenLimitUID, setCurrentTokenLimitUID] = useState('') + const [tokenLimits, setTokenLimits] = useState>([]) + const [currentFeatures, setCurrentFeatures] = useState([]) + const {uid:paymentPlanUid, tokens_per_plan:planTokenLimit, price:planPrice} = useAppSelector(state => state.user.payment_plan.plan) + const balance = useAppSelector(balanceSelector) + const { data } = useSession() + + useEffect(() => { + if (data?.access) { + accountApi.getPaymentsPlans(data?.access).then((res) => { + if (res){ + setOffers(res) + setCurrentTokenLimitUID(res[0].uid) + setTokenLimits(res.map(offer => ({ + value: offer.uid, + label: `${commaSeparated(Math.round(Number(offer.tokens_per_plan)))} токенов` + }))) + } + }) + } + }, [data?.access]) + + useEffect(() => { + if (offers && currentTokenLimitUID){ + const foundOffer = offers.find(offer => offer.uid === currentTokenLimitUID) + if (foundOffer) { + setCurrentFeatures(foundOffer.grouped_features) + } + } + }, [currentTokenLimitUID, offers]) + + return ( + + + Подписка + + + + + + + + + {/* ПОКА НЕ ИСПОЛЬЗУЙЕТСЯ. НЕ ВЫПИЛИВАТЬ!! */} + {/* */} + {/* */} + + ) +} + +export default SubscriptionPage @@ -0,0 +1,3 @@ +export * from './ui' +export * from './api' +export * from './lib' \ No newline at end of file @@ -5,11 +5,6 @@ export const staticLinks: NavigationSearchLink[] = [ label: 'Дашборд', url: '/', category: 'Навигация' - }, - { - label: 'Оплата', - url: '/account?scope=subscribe', - category: 'Навигация', }, { label: 'Настройки', @@ -1,3 +0,0 @@ -export const convertTokenValue = (value: string): string => { - return Math.floor(+value) / 1000 + ' 000' -} @@ -1,199 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { - Accordion, - AccordionDetails, - AccordionSummary, - Box, - Button, - MenuItem, - Stack, - Typography, -} from '@mui/material' -import Router, { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' - -import { useAppSelector } from '#/app/store/store' -import { CheckBoxAgreeWithRules, Loader, Select } from '#/shared' -import { accountApi } from '#/shared/api/account-endpoints' -import { getPaymentsHistory, PaymentHistory } from '#/shared/api/endpoints' -import Offer from '#/widgets/payment/ui/offer' - -import styles from '../ui/payment.module.scss' - -export interface IOffer { - uid: string - price: string - tokens_per_plan: string - title: string - duration: string - accessed_models: string[] - points: string[] -} - -interface IPaymentsProps { - device: 'mobile' | 'desktop' -} - -export const Subscription: React.FC = ({ device }) => { - const { status, data } = useSession() - - const [isChangeWindow, setIsChangeWindow] = useState(false) - - const [loading, setLoading] = useState(false) - - if (status === 'loading' || loading) { - return - } - - return setIsChangeWindow(false)} device={device} /> -} - -export const Payment: React.FC = ({ device, changeClose }) => { - const desktop = device === 'desktop' - - const [offers, setOffers] = useState(null) - - const [selectedOffer, setSelectedOffer] = useState(offers?.length ? offers[0].uid : '') - - const { data } = useSession() - - const [messages, setMessages] = useState(null) - - useEffect(() => { - if (data?.access) { - getPaymentsHistory(data.access).then((res) => setMessages(res)) - } - }, [data?.access]) - - useEffect(() => { - if (data?.access) accountApi.getPaymentsPlans(data?.access).then((res) => setOffers(res)) - }, [data?.access]) - - const pay = async (uid: string) => { - const urlForPay = await accountApi.payProduct(data!.access, uid) - - urlForPay ? await Router.push(urlForPay) : null - } - - return ( - - - - {offers?.map((offer) => { - return ( - setSelectedOffer(uid)} - key={offer.uid} - /> - ) - })} - - {/* {messages && ( - - - История - {messages.map((el) => ( - - ))} - - - )} */} - - - - Часто задаваемые вопросы - - {info.map((el) => { - return ( - - p': { - fontSize: '18px !important', - }, - }} - expandIcon={} - aria-controls='panel1a-content' - id='panel1a-header' - > - {el.title} - - - {el.text} - - - ) - })} - - - ) -} - -interface MessageProps { - price: string - tokens: string - time: string -} - -const HistoryMessage: React.FC = ({ tokens, time, price }) => { - function formatDateToRussian(inputDate: string) { - const date = new Date(inputDate) - const formattedDate = date.toLocaleDateString('ru-RU') - - return formattedDate.replace(/(\d{2})\.(\d{2})\.(\d{4})/, '$1.$2.$3') - } - - return ( - - Оплата - - {price} ₽, {Math.floor(+tokens)} токенов - - - {formatDateToRussian(time)} - - ) -} - -var info = [ - { - title: 'Что такое токены?', - text: 'Токен — это виртуальная валюта AIR, которую пользователи тратят на генерации. Их цена зависит от длины запроса, настроек нейросети и типа контента. Например, текст в ChatGPT будет стоить примерно 1 токен, а картинка в Kandinsky — примерно 4 токена.\n', - }, - { - title: 'Где проверять баланс токенов? Как следить за расходами?\n', - text: 'Траты по всем категориям и текущий баланс можно проверять в разделе «‎Дашборд».', - }, -] @@ -1,99 +0,0 @@ -.tarif { - background-color: var(--new-ui-main-color); - padding: 30px; - border-radius: 15px; - width: 100%; - display: flex; - text-align: left; - flex-direction: column; - position: relative; - &__btn { - transition: all 0.8s ease-in-out !important; - text-align: center; - } - &::before { - content: ''; - position: absolute; - top: 0; - - visibility: hidden; - left: 0; - width: 100%; - height: 100%; - border: 2px solid var(--air-color); - border-radius: 15px; - transition: all 0.6s ease-in-out; - opacity: 0; - } - &:hover { - &::before { - opacity: 1; - visibility: visible; - } - .tarif__btn { - background-color: var(--air-color); - color: white; - border: unset; - } - } - &__header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 45px; - position: relative; - z-index: 1; - } - - &__badge { - display: flex; - align-items: center; - gap: 5px; - width: max-content; - span { - font-size: 36px; - font-weight: 700; - } - } - - &__quote { - font-size: 18px; - font-weight: 700; - color: var(--new-ui-gray-color); - } - - &__price { - font-weight: 700; - font-size: 25px; - letter-spacing: -2%; - margin-bottom: 8px; - position: relative; - z-index: 1; - } - - &__list { - display: flex; - flex-direction: column; - align-items: flex-start; - padding-left: 20px; - gap: 4px; - margin-bottom: 45px; - position: relative; - z-index: 1; - } - - &__btn { - border-color: var(--air-color); - color: var(--air-color); - position: relative; - z-index: 1; - } -} - -.point { - color: var(--new-ui-text-color); - text-align: left; - &::marker { - color: var(--new-ui-gray-color) !important; - } -} @@ -1,48 +0,0 @@ -import React from 'react' - -import { IOffer } from '#/widgets/payment/model/payment' -import styles from './offer.module.scss' -import { CommonBagde } from '#/shared/ui/badge' -import LightningSvg from '#/assets/svg/lightning.svg?react' -import { CommonButton } from '#/shared/ui/button' -import { c, commaSeparated } from '#/shared/lib/helpers' - -interface IOfferProps extends IOffer { - pickOffer: (uid: string) => void - selectedOffer: string - device: 'desktop' | 'mobile' - pay: (uid: string) => void -} - -const Offer: React.FC = ({ pickOffer, uid, tokens_per_plan, price, points, pay }) => { - - return ( -
pickOffer(uid)}> -
- - - - {commaSeparated(Math.round(Number(tokens_per_plan)))} - - -

токенов

-
- -
    - {points.map((el, idx) => ( -
  • - {el} -
  • - ))} -
- -

{commaSeparated(parseInt(price) ?? 0)}₽

- - pay(uid)} variant={'outline'}> - Оплатить - -
- ) -} - -export default Offer @@ -1,51 +0,0 @@ -.wrap { - display: flex; - justify-content: space-between; - align-items: flex-start; - width: 100%; - - @media (max-width: 768px) { - flex-direction: column; - } - .main { - width: 69%; - - @media (max-width: 768px) { - width: 100%; - } - - div { - height: 176px; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - - .manage { - margin-top: 35px; - display: flex; - flex-direction: row; - width: 100%; - align-items: center; - justify-content: space-between; - @media (max-width: 768px) { - margin-top: 15px; - flex-direction: column-reverse; - } - > button { - width: 49.5%; - @media (max-width: 768px) { - width: 100%; - } - } - } - } - } -} -.messages { - @media (max-width: 768px) { - margin-top: 15px; - margin-left: 0; - width: 100%; - } -} @@ -1 +0,0 @@ -export { Payment } from './model/payment' @@ -13,17 +13,17 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useSession } from 'next-auth/react' -import '../styles/styles.module.css' -import styles from '../styles/styles.module.css' - import { useUserSettingsContext } from '#/entities/user-account' import { BUSINESS_ERROR_REPORT, ERROR_REPORT, getModalById } from '#/features/modals' +import '../styles/styles.module.css' +import styles from '../styles/styles.module.css' + export const menuListTop = [ { title: 'Дашборд', link: '/', icon: '/svg/side-menu/market', activeList: [] }, { - title: 'Оплата', - link: '/account?scope=subscribe', + title: 'Подписка', + link: '/subscription', icon: '/svg/side-menu/star', activeList: ['subscribe'], },