payments card and separated /support url

This commit is contained in:
2025-05-22 16:08:40 +03:00
parent 6963fdb17a
commit 29d3af2ea1
16 changed files with 481 additions and 31 deletions

View File

@@ -2,7 +2,9 @@ from rest_framework import serializers
from routes.models import Route, City, Country from routes.models import Route, City, Country
from django.conf import settings from django.conf import settings
from routes.constants.routeChoices import cargo_type_choices, type_transport_choices, owner_type_choices from routes.constants.routeChoices import cargo_type_choices, type_transport_choices, owner_type_choices
from routes.constants.account_types import account_types
from api.models import UserProfile from api.models import UserProfile
from sitemanagement.models import Pricing
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
import pytz import pytz
@@ -204,4 +206,62 @@ class CreateRouteSerializer(serializers.ModelSerializer):
return route return route
except Exception as e: except Exception as e:
raise raise
class PricingSerializer(serializers.ModelSerializer):
isPopular = serializers.BooleanField(source='is_popular')
plan = serializers.CharField()
features = serializers.SerializerMethodField()
class Meta:
model = Pricing
fields = ['plan', 'price', 'features', 'isPopular']
def get_features(self, obj):
return list(obj.membershipfeatures_set.values_list('feature', flat=True))
def to_representation(self, instance):
# преобразуем данные перед отправкой на фронтенд
data = super().to_representation(instance)
data['plan'] = data['plan'].lower() # приводим к нижнему регистру
return data
class PlanChangeSerializer(serializers.Serializer):
account_type = serializers.CharField()
def validate_account_type(self, value):
"""
Проверяем, что тип тарифа соответствует допустимым значениям
"""
valid_types = [t[0] for t in account_types]
if value not in valid_types:
raise serializers.ValidationError(
f"Недопустимый тип тарифа. Допустимые значения: {', '.join(valid_types)}"
)
return value
def validate(self, data):
"""
Проверка возможности перехода с одного тарифа на другой
"""
if not self.instance:
raise serializers.ValidationError("Пользователь не найден")
current_type = getattr(self.instance, 'account_type', None)
if not current_type:
raise serializers.ValidationError("У пользователя не установлен текущий тариф")
new_type = data['account_type']
if current_type == new_type:
raise serializers.ValidationError("Этот тариф уже активен")
return data
def update(self, instance, validated_data):
"""
Обновляем тип тарифа пользователя
"""
instance.account_type = validated_data['account_type']
instance.save()
return instance

View File

@@ -8,14 +8,17 @@ from django.shortcuts import get_object_or_404
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.contrib.auth.models import User
from api.auth.serializers import UserResponseSerializer from api.auth.serializers import UserResponseSerializer
from api.models import UserProfile from api.models import UserProfile
from api.utils.decorators import handle_exceptions from api.utils.decorators import handle_exceptions
from routes.models import Route, City, Country from routes.models import Route, City, Country
from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer from sitemanagement.models import Pricing
from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer, PlanChangeSerializer, PricingSerializer
class UserDataView(ViewSet): class UserDataView(ViewSet):
"""Эндпоинт для наполнения стора фронта данными"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def initial(self, request, *args, **kwargs): def initial(self, request, *args, **kwargs):
@@ -40,6 +43,10 @@ class UserDataView(ViewSet):
) )
class AccountActionsView(ViewSet): class AccountActionsView(ViewSet):
"""Действия в аккаунте пользователя:
- PATCH данных в account/main
- POST новых заявок"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@action(detail=False, methods=['patch']) @action(detail=False, methods=['patch'])
@@ -90,6 +97,8 @@ class AccountActionsView(ViewSet):
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
@handle_exceptions @handle_exceptions
def user_routes(self, request): def user_routes(self, request):
"""Получаем список заявок юзера"""
user = request.user user = request.user
routes = Route.objects.filter(owner=user) routes = Route.objects.filter(owner=user)
return Response(RouteSerializer(routes, many=True).data, status=status.HTTP_200_OK) return Response(RouteSerializer(routes, many=True).data, status=status.HTTP_200_OK)
@@ -97,20 +106,17 @@ class AccountActionsView(ViewSet):
@action(detail=False, methods=['post']) @action(detail=False, methods=['post'])
@handle_exceptions @handle_exceptions
def create_route(self, request): def create_route(self, request):
print("[DEBUG] Входящие данные в create_route view:", request.data) """Создаем новую заявку"""
try:
serializer = CreateRouteSerializer(data=request.data, context={'request': request}) serializer = CreateRouteSerializer(data=request.data, context={'request': request})
if not serializer.is_valid(): if not serializer.is_valid():
print("[DEBUG] Ошибки валидации:", serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) route = serializer.save()
route = serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED)
print("[DEBUG] Маршрут успешно создан в view:", route.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception as e:
print("[DEBUG] Необработанная ошибка в create_route:", str(e))
raise
class CityView(ViewSet): class CityView(ViewSet):
"""Получаем список городов из базы для автокомплита"""
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
@handle_exceptions @handle_exceptions
@@ -136,6 +142,8 @@ class CityView(ViewSet):
return Response(CitySerializer(cities, many=True).data, status=status.HTTP_200_OK) return Response(CitySerializer(cities, many=True).data, status=status.HTTP_200_OK)
class CountryView(ViewSet): class CountryView(ViewSet):
"""Получаем список стран из базы для автокомплита"""
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
@handle_exceptions @handle_exceptions
def get_countries(self, request): def get_countries(self, request):
@@ -153,4 +161,31 @@ class CountryView(ViewSet):
# сортируем по международному названию # сортируем по международному названию
countries = countries.order_by('international_name') countries = countries.order_by('international_name')
return Response(CountrySerializer(countries, many=True).data, status=status.HTTP_200_OK) return Response(CountrySerializer(countries, many=True).data, status=status.HTTP_200_OK)
class ChangeUserMembership(ViewSet):
"""Меняем тарифный план пользователя"""
@action(detail=False, methods=['post'])
@handle_exceptions
def change_plan(self, request):
"""Меняем пользователю тарифный план"""
user_profile = get_object_or_404(User, id=request.user.id)
serializer = PlanChangeSerializer(user_profile, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({"message": "Тариф успешно изменен"}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class GetMembershipData(ViewSet):
"""Получаем все тарифные планы"""
@action(detail=False, methods=['get'])
@handle_exceptions
def get_pricing_data(self, request):
"""Получаем данные по тарифам"""
pricing_data = Pricing.objects.all().order_by('price')
serializer = PricingSerializer(pricing_data, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@@ -1,6 +1,9 @@
from rest_framework import serializers from rest_framework import serializers
from routes.models import Route
from sitemanagement.models import FAQ, News from sitemanagement.models import FAQ, News
from django.conf import settings
import pytz
from routes.constants.routeChoices import cargo_type_choices, type_transport_choices
class FAQMainSerializer(serializers.ModelSerializer): class FAQMainSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@@ -32,4 +35,5 @@ class TelegramSerializer(serializers.Serializer):
message = serializers.CharField(max_length=1000) message = serializers.CharField(max_length=1000)
def create(self, validated_data): def create(self, validated_data):
return type('TelegramMessage', (), validated_data) return type('TelegramMessage', (), validated_data)

View File

@@ -4,8 +4,12 @@ from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from api.utils.decorators import handle_exceptions from api.utils.decorators import handle_exceptions
from django.db.models import Q
from routes.models import Route
from routes.constants.routeChoices import owner_type_choices
from api.main.serializers import FAQMainSerializer, NewsMainSerializer, TelegramSerializer from api.main.serializers import FAQMainSerializer, NewsMainSerializer, TelegramSerializer
from api.account.client.serializers import RouteSerializer
from sitemanagement.models import FAQ, News from sitemanagement.models import FAQ, News
class FAQView(APIView): class FAQView(APIView):
@@ -19,6 +23,7 @@ class FAQView(APIView):
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
class NewsView(APIView): class NewsView(APIView):
@handle_exceptions @handle_exceptions
def get(self, request): def get(self, request):
@@ -30,8 +35,21 @@ class NewsView(APIView):
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
class LatestRoutesView(APIView):
@handle_exceptions
def get(self, request):
"""Получаем последние 5 маршрутов для каждого типа owner_type"""
latest_routes = {}
owner_types = dict(owner_type_choices).keys()
for owner_type in owner_types:
routes = Route.objects.filter(owner_type=owner_type).order_by('-id')[:5]
latest_routes[owner_type] = RouteSerializer(routes, many=True).data
return Response(latest_routes, status=status.HTTP_200_OK)
class TelegramMessageView(APIView): class TelegramMessageView(APIView):
@handle_exceptions @handle_exceptions
def post(self, request): def post(self, request):

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from api.main.views import FAQView, NewsView, TelegramMessageView from api.main.views import FAQView, NewsView, TelegramMessageView, LatestRoutesView
from api.auth.views import ( from api.auth.views import (
RegisterViewSet, RegisterViewSet,
@@ -8,12 +8,18 @@ LoginViewSet,
LogoutView, LogoutView,
RefreshTokenView) RefreshTokenView)
from api.account.client.views import UserDataView, AccountActionsView, CityView, CountryView from api.account.client.views import (
UserDataView,
AccountActionsView,
CityView,
CountryView,
GetMembershipData)
urlpatterns = [ urlpatterns = [
path("v1/faq/", FAQView.as_view(), name='faqMain'), path("v1/faq/", FAQView.as_view(), name='faqMain'),
path("v1/news/", NewsView.as_view(), name="newsmain"), path("v1/news/", NewsView.as_view(), name="newsmain"),
path("v1/send-message", TelegramMessageView.as_view(), name='send_message'), path("v1/send-message", TelegramMessageView.as_view(), name='send_message'),
path("v1/latest-routes/", LatestRoutesView.as_view(), name='latest_routes'),
path("auth/refresh/", RefreshTokenView.as_view(), name="token-refresh"), path("auth/refresh/", RefreshTokenView.as_view(), name="token-refresh"),
path("register/clients/", RegisterViewSet.as_view({'post': 'register_client'}), name="register-client"), path("register/clients/", RegisterViewSet.as_view({'post': 'register_client'}), name="register-client"),
@@ -27,5 +33,7 @@ urlpatterns = [
path("v1/account/create_route/", AccountActionsView.as_view({'post':'create_route'}), name='create_route'), path("v1/account/create_route/", AccountActionsView.as_view({'post':'create_route'}), name='create_route'),
path("v1/cities/", CityView.as_view({'get':'get_cities'}), name='get_cities'), path("v1/cities/", CityView.as_view({'get':'get_cities'}), name='get_cities'),
path("v1/countries/", CountryView.as_view({'get':'get_countries'}), name='get_countries') path("v1/countries/", CountryView.as_view({'get':'get_countries'}), name='get_countries'),
path("v1/plans/", GetMembershipData.as_view({'get':'get_pricing_data'}), name='get_pricing_data'),
] ]

View File

@@ -1,5 +1,21 @@
from django.contrib import admin from django.contrib import admin
from .models import * from .models import *
class FeatureInline(admin.TabularInline):
model = MembershipFeatures
list_display = ['features'] # поля в списке
list_filter = ['features'] # фильтры справа
search_fields = ['features'] # поля для поиска
extra = 0
can_delete = False
verbose_name = 'Параметр тарифных планов'
verbose_name_plural = 'Параметры тарифных планов'
class PricingAdmin(admin.ModelAdmin):
inlines = [FeatureInline]
admin.site.register(FAQ) admin.site.register(FAQ)
admin.site.register(News) admin.site.register(News)
admin.site.register(Pricing, PricingAdmin)

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2.1 on 2025-05-22 12:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sitemanagement', '0009_alter_news_slug_alter_news_titleimage'),
]
operations = [
migrations.CreateModel(
name='Pricing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('plan', models.CharField(choices=[('lite', 'Lite'), ('standart', 'Standart'), ('premium', 'Premium')], max_length=10)),
('price', models.IntegerField()),
('is_popular', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Тарифный план',
'verbose_name_plural': 'Тарифные планы',
},
),
migrations.CreateModel(
name='MembershipFeatures',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feature', models.CharField(max_length=255)),
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sitemanagement.pricing', verbose_name='Тарифный план')),
],
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.1 on 2025-05-22 12:42
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sitemanagement', '0010_pricing_membershipfeatures'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Transactions',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Сумма')),
('status', models.CharField(max_length=255, verbose_name='Статус')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sitemanagement.pricing', verbose_name='Тарифный план')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Транзакция',
'verbose_name_plural': 'Транзакции',
'ordering': ['id'],
},
),
]

View File

@@ -5,6 +5,8 @@ from django.utils.text import slugify
from django.db.models.signals import pre_save from django.db.models.signals import pre_save
from django.dispatch import receiver from django.dispatch import receiver
from transliterate import translit from transliterate import translit
from routes.constants.account_types import account_types
from django.contrib.auth.models import User
class FAQ (models.Model): class FAQ (models.Model):
title = models.CharField(max_length=250) title = models.CharField(max_length=250)
@@ -54,4 +56,37 @@ def generate_slug(sender, instance, **kwargs):
# транслит с русского на латиницу # транслит с русского на латиницу
transliterated_title = translit(instance.title, 'ru', reversed=True) transliterated_title = translit(instance.title, 'ru', reversed=True)
# создаем слаг и заменяем пробелы на дефисы # создаем слаг и заменяем пробелы на дефисы
instance.slug = slugify(transliterated_title) instance.slug = slugify(transliterated_title)
class Pricing(models.Model):
plan = models.CharField(max_length=10, choices=account_types)
price = models.IntegerField()
is_popular = models.BooleanField(default=False)
class Meta:
verbose_name = 'Тарифный план'
verbose_name_plural = 'Тарифные планы'
def __str__(self):
return self.plan
class MembershipFeatures(models.Model):
plan = models.ForeignKey(Pricing, on_delete=models.CASCADE, verbose_name=('Тарифный план'))
feature = models.CharField(max_length=255)
class Transactions(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь')
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Сумма')
plan = models.ForeignKey(Pricing, on_delete=models.CASCADE, verbose_name='Тарифный план')
status = models.CharField(max_length=255, verbose_name='Статус')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
class Meta:
verbose_name = 'Транзакция'
verbose_name_plural = 'Транзакции'
ordering = ['id']
def __str__(self):
return f'{self.user} - {self.amount}'

View File

@@ -7,8 +7,7 @@ import Loader from '@/components/ui/Loader'
import { RiUser3Line } from 'react-icons/ri' import { RiUser3Line } from 'react-icons/ri'
import { FaRoute } from 'react-icons/fa' import { FaRoute } from 'react-icons/fa'
import { GoPackageDependents, GoPackageDependencies } from 'react-icons/go' import { GoPackageDependents, GoPackageDependencies } from 'react-icons/go'
import { MdOutlinePayments } from 'react-icons/md' import { MdOutlinePayments, MdOutlineContactSupport } from 'react-icons/md'
import { CgNotes } from 'react-icons/cg'
import useUserStore from '@/app/store/userStore' import useUserStore from '@/app/store/userStore'
export default function AccountLayout({ children }: { children: React.ReactNode }) { export default function AccountLayout({ children }: { children: React.ReactNode }) {
@@ -47,6 +46,7 @@ export default function AccountLayout({ children }: { children: React.ReactNode
icon: GoPackageDependencies, icon: GoPackageDependencies,
}, },
{ name: 'Тарифы', href: '/account/payments', icon: MdOutlinePayments }, { name: 'Тарифы', href: '/account/payments', icon: MdOutlinePayments },
{ name: 'Поддержка', href: '/account/support', icon: MdOutlineContactSupport },
] ]
return ( return (

View File

@@ -5,7 +5,7 @@ import { useForm } from '@/app/hooks/useForm'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import showToast from '@/components/ui/Toast' import showToast from '@/components/ui/Toast'
import useUserStore from '@/app/store/userStore' import useUserStore from '@/app/store/userStore'
import ContactUs from '@/components/ContactUs'
import TextInput from '@/components/ui/TextInput' import TextInput from '@/components/ui/TextInput'
import PhoneInput from '@/components/ui/PhoneInput' import PhoneInput from '@/components/ui/PhoneInput'
@@ -126,7 +126,6 @@ const AccountPage = () => {
</div> </div>
</div> </div>
</div> </div>
<ContactUs />
</div> </div>
) )
} }

View File

@@ -0,0 +1,113 @@
import React, { useState } from 'react'
import { PricingCardProps } from '@/app/types'
import Button from '@/components/ui/Button'
import showToast from '@/components/ui/Toast'
import useUserStore from '@/app/store/userStore'
const PricingCard: React.FC<PricingCardProps> = ({
plan,
price,
features,
isPopular,
isActive,
onPlanChange,
}) => {
const [isLoading, setIsLoading] = useState(false)
const { user, setUser } = useUserStore()
const handlePlanChange = async () => {
try {
setIsLoading(true)
console.log('Changing plan to:', plan)
// const response = await changePlan(plan) -- тут обработка данных запроса на смену плана
// обновляем данные в сторе
if (user) {
setUser({
...user,
account_type: plan,
})
}
// обновляем данные на странице
if (onPlanChange) {
onPlanChange()
}
showToast({
type: 'success',
duration: 1000,
message: `Тариф успешно изменен на ${plan.toUpperCase()}!`,
})
} catch (error) {
console.error('Error changing plan:', error)
showToast({
type: 'error',
message: 'Не удалось изменить тариф. Попробуйте позже.',
})
} finally {
setIsLoading(false)
}
}
return (
<div
className={`flex h-full translate-y-5 animate-[fadeIn_0.8s_ease-out_forwards] flex-col rounded-xl p-6 shadow-lg ${
isPopular ? 'border-orange border-2' : 'border border-gray-200'
} transition-all duration-300 hover:translate-y-[-8px]`}
>
<div className="flex-1">
{isPopular && (
<div className="bg-orange mb-2 inline-block rounded-2xl px-2 py-1 text-xs text-white">
Популярный выбор
</div>
)}
{isActive && (
<div className="mb-2 inline-block rounded-2xl bg-blue-500 px-2 py-1 text-xs text-white">
Активный план
</div>
)}
<h3 className="mb-2 text-xl font-bold capitalize">{plan}</h3>
<div className="mb-4">
<span className="text-3xl font-bold">{price}</span>
{price > 0 && <span className="text-gray-600"> / месяц</span>}
</div>
<ul className="space-y-2">
{features.map((feature, index) => (
<li key={index} className="flex items-start gap-2">
<svg
className="mt-1 h-[16px] w-[16px] flex-shrink-0 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="leading-6">{feature}</span>
</li>
))}
</ul>
</div>
<div className="mt-6">
<Button
className={`flex w-full items-center justify-center rounded-2xl px-4 py-2 ${
isPopular
? 'bg-orange hover:bg-orange/80 text-white'
: 'bg-gray-100 hover:bg-blue-500 hover:text-white'
} ${isActive ? 'hidden' : ''} ${isLoading ? 'cursor-not-allowed opacity-50' : ''}`}
text={isLoading ? 'Обновление...' : 'Выбрать план'}
onClick={handlePlanChange}
disabled={isLoading}
/>
</div>
</div>
)
}
export default PricingCard

View File

@@ -1,7 +1,58 @@
import React from 'react' 'use client'
const PaymentsPage = () => { import React, { useEffect, useState } from 'react'
return <div>page</div> import PricingCard from './PricingCard'
import { PricingCardProps } from '@/app/types'
import useUserStore from '@/app/store/userStore'
import Loader from '@/components/ui/Loader'
const AdminPayments = () => {
const { user } = useUserStore()
const [plans, setPlans] = useState<PricingCardProps[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchPlans = async () => {
try {
const response = await fetch('/api/account/get-plans')
if (!response.ok) {
throw new Error('Failed to fetch plans')
}
const data = await response.json()
setPlans(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load pricing plans')
} finally {
setIsLoading(false)
}
}
fetchPlans()
}, [])
if (isLoading) {
return <Loader />
}
if (error) {
return <div className="p-8 text-center text-red-500">{error}</div>
}
return (
<div className="container mx-auto translate-y-5 animate-[fadeIn_1.1s_ease-out_forwards] space-y-6">
<h2 className="mb-6 text-center text-3xl font-bold">Тарифные планы</h2>
<div className="grid grid-cols-1 gap-8 rounded-lg bg-white p-8 md:grid-cols-3">
{plans.map(plan => (
<PricingCard
key={plan.plan}
{...plan}
isActive={user?.account_type?.toLowerCase() === plan.plan.toLowerCase()}
/>
))}
</div>
</div>
)
} }
export default PaymentsPage export default AdminPayments

View File

@@ -0,0 +1,8 @@
import React from 'react'
import ContactUs from '@/components/ContactUs'
const SupportPage = () => {
return <ContactUs />
}
export default SupportPage

View File

@@ -0,0 +1,26 @@
import { NextRequest } from 'next/server'
export async function GET(req: NextRequest) {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/plans/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const error = await response.json()
console.error('API error:', error)
return new Response(JSON.stringify(error), { status: response.status })
}
const result = await response.json()
return new Response(JSON.stringify(result), { status: 200 })
} catch (error) {
console.error('Route handler error:', error)
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
status: 500,
})
}
}

View File

@@ -24,6 +24,7 @@ export interface ButtonProps {
leftIcon?: React.ReactNode leftIcon?: React.ReactNode
rightIcon?: React.ReactNode rightIcon?: React.ReactNode
type?: 'button' | 'submit' | 'reset' type?: 'button' | 'submit' | 'reset'
disabled?: boolean
} }
export interface SearchCardProps { export interface SearchCardProps {
@@ -215,3 +216,12 @@ export interface CheckboxProps {
disabledText: string disabledText: string
enabledText: string enabledText: string
} }
export interface PricingCardProps {
plan: 'free' | 'pro' | 'premium'
price: number
features: string[]
isPopular?: boolean
isActive?: boolean
onPlanChange?: () => void
}