route handlers for account/main

This commit is contained in:
2025-05-19 16:56:30 +03:00
parent edd380d78a
commit c0da94f3dc
22 changed files with 601 additions and 150 deletions

View File

@@ -12,6 +12,7 @@ pillow = "*"
transliterate = "*" transliterate = "*"
djangorestframework-simplejwt = "*" djangorestframework-simplejwt = "*"
django-cors-headers = "*" django-cors-headers = "*"
requests = "*"
[dev-packages] [dev-packages]

133
backend/Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "2620449b3502893be81fa9ab722c0beac295dd1dcad39705d6b962eddf45cc21" "sha256": "cb9b63f4897299c7cedf6a49a97a73a97587c60df970d9c7044fec2f6f3861c8"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -24,6 +24,112 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==3.8.1" "version": "==3.8.1"
}, },
"certifi": {
"hashes": [
"sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6",
"sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"
],
"markers": "python_version >= '3.6'",
"version": "==2025.4.26"
},
"charset-normalizer": {
"hashes": [
"sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4",
"sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45",
"sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7",
"sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0",
"sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7",
"sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d",
"sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d",
"sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0",
"sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184",
"sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db",
"sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b",
"sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64",
"sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b",
"sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8",
"sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff",
"sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344",
"sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58",
"sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e",
"sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471",
"sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148",
"sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a",
"sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836",
"sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e",
"sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63",
"sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c",
"sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1",
"sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01",
"sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366",
"sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58",
"sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5",
"sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c",
"sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2",
"sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a",
"sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597",
"sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b",
"sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5",
"sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb",
"sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f",
"sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0",
"sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941",
"sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0",
"sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86",
"sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7",
"sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7",
"sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455",
"sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6",
"sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4",
"sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0",
"sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3",
"sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1",
"sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6",
"sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981",
"sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c",
"sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980",
"sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645",
"sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7",
"sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12",
"sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa",
"sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd",
"sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef",
"sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f",
"sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2",
"sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d",
"sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5",
"sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02",
"sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3",
"sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd",
"sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e",
"sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214",
"sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd",
"sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a",
"sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c",
"sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681",
"sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba",
"sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f",
"sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a",
"sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28",
"sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691",
"sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82",
"sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a",
"sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027",
"sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7",
"sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518",
"sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf",
"sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b",
"sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9",
"sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544",
"sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da",
"sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509",
"sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f",
"sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a",
"sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.2"
},
"django": { "django": {
"hashes": [ "hashes": [
"sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284", "sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284",
@@ -60,6 +166,14 @@
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==5.5.0" "version": "==5.5.0"
}, },
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
},
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928",
@@ -182,6 +296,15 @@
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==1.1.0" "version": "==1.1.0"
}, },
"requests": {
"hashes": [
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.32.3"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
@@ -205,6 +328,14 @@
], ],
"index": "pypi", "index": "pypi",
"version": "==1.10.2" "version": "==1.10.2"
},
"urllib3": {
"hashes": [
"sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466",
"sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"
],
"markers": "python_version >= '3.9'",
"version": "==2.4.0"
} }
}, },
"develop": {} "develop": {}

View File

@@ -2,6 +2,16 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models import UserProfile from .models import UserProfile
from routes.models import Route
class RouteInline(admin.TabularInline):
model = Route
fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT')
readonly_fields = ('owner_type', 'type_transport', 'from_city', 'to_city', 'cargo_type', 'departure_DT', 'arrival_DT')
extra = 0
can_delete = False
verbose_name = 'Маршрут пользователя'
verbose_name_plural = 'Маршруты пользователя'
class UserProfileInline(admin.StackedInline): class UserProfileInline(admin.StackedInline):
model = UserProfile model = UserProfile
@@ -12,7 +22,7 @@ class UserProfileInline(admin.StackedInline):
readonly_fields = ('uuid','authMailCode') # ридонли в админке readonly_fields = ('uuid','authMailCode') # ридонли в админке
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
inlines = (UserProfileInline,) inlines = (UserProfileInline, RouteInline)
# перерегистрируем User # перерегистрируем User
admin.site.unregister(User) admin.site.unregister(User)

View File

@@ -11,3 +11,25 @@ class NewsMainSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = News model = News
fields= "__all__" fields= "__all__"
class TelegramSerializer(serializers.Serializer):
"""Отправляем сообщение в телеграм канал компании с полями:
- Источник
- Имя пользователя
- Номер телефона
- Сообщение"""
SOURCE_CHOICES = [
("main", "Main"),
("admin", "Admin"),
("userAccount", "Account"),
("contact-us", "Contact Us")
]
source = serializers.ChoiceField(choices=SOURCE_CHOICES)
name = serializers.CharField(max_length=255)
phone_number = serializers.CharField(max_length=20)
message = serializers.CharField(max_length=1000)
def create(self, validated_data):
return type('TelegramMessage', (), validated_data)

View File

@@ -1,9 +1,11 @@
import requests
from django.conf import settings
from rest_framework import status 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 api.main.serializers import FAQMainSerializer, NewsMainSerializer from api.main.serializers import FAQMainSerializer, NewsMainSerializer, TelegramSerializer
from sitemanagement.models import FAQ, News from sitemanagement.models import FAQ, News
class FAQView(APIView): class FAQView(APIView):
@@ -28,3 +30,39 @@ class NewsView(APIView):
} }
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
class TelegramMessageView(APIView):
@handle_exceptions
def post(self, request):
serializer = TelegramSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"error": "Invalid data provided", "details": serializer.errors},
status=status.HTTP_400_BAD_REQUEST
)
# формируем сообщение из полей
message_obj = serializer.save()
message = (
f"#{message_obj.source}\n"
f"Name: {message_obj.name}\n"
f"Phone number: {message_obj.phone_number}\n"
f"Message: {message_obj.message}"
)
response = requests.post(
f'https://api.telegram.org/bot{settings.BOT_TOKEN}/sendMessage',
json={
'chat_id': settings.CHAT_ID,
'text': message,
}
)
response.raise_for_status()
return Response(
{"success": "Message sent successfully"},
status=status.HTTP_201_CREATED
)

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from api.main.views import FAQView, NewsView from api.main.views import FAQView, NewsView, TelegramMessageView
from api.auth.views import ( from api.auth.views import (
RegisterViewSet, RegisterViewSet,
@@ -13,6 +13,7 @@ from api.account.client.views import UserDataView
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("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"),

View File

@@ -1,11 +1,17 @@
asgiref==3.8.1 asgiref==3.8.1
certifi==2025.4.26
charset-normalizer==3.4.2
Django==5.2.1 Django==5.2.1
django-cors-headers==4.7.0
djangorestframework==3.16.0 djangorestframework==3.16.0
djangorestframework_simplejwt==5.5.0 djangorestframework_simplejwt==5.5.0
idna==3.10
pillow==11.2.1 pillow==11.2.1
psycopg2==2.9.10 psycopg2==2.9.10
PyJWT==2.9.0 PyJWT==2.9.0
python-dotenv==1.1.0 python-dotenv==1.1.0
requests==2.32.3
six==1.17.0 six==1.17.0
sqlparse==0.5.3 sqlparse==0.5.3
transliterate==1.10.2 transliterate==1.10.2
urllib3==2.4.0

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -1,7 +1,115 @@
import React from 'react' 'use client'
const page = () => { import React from 'react'
return <div>page</div> import { useForm } from '@/app/hooks/useForm'
import Button from '@/components/ui/Button'
import showToast from '@/components/ui/Toast'
import useUserStore from '@/app/store/userStore'
import ContactUs from '@/components/ContactUs'
import TextInput from '@/components/ui/TextInput'
import PhoneInput from '@/components/ui/PhoneInput'
const validationRules = {
firstName: { required: true },
lastName: { required: false },
phone_number: { required: true },
email: { required: true },
} }
export default page const AccountPage = () => {
const { user } = useUserStore()
const { values, handleChange, handleSubmit } = useForm(
{
firstName: user?.name || '',
lastName: user?.surname || '',
phone_number: user?.phone_number || '',
email: user?.email || '',
country: user?.country || '',
city: user?.city || '',
},
validationRules,
async (values) => {
try {
// await updateMainTab(values)
showToast({ type: 'success', message: 'Данные успешно обновлены!' })
} catch {
showToast({ type: 'error', message: 'Ой, что то пошло не так..' })
}
}
)
if (!user) {
return null
}
return (
<div className="space-y-3">
<div className="rounded-2xl shadow overflow-hidden">
<div className="p-6 bg-white sm:p-8">
<div className="space-y-4">
<h1 className="text-2xl">Личные данные</h1>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TextInput
name="firstName"
value={values.firstName}
handleChange={handleChange}
label="Имя"
placeholder="Ваше имя"
style="register"
/>
<TextInput
name="lastName"
value={values.lastName}
handleChange={handleChange}
label="Фамилия"
placeholder="Ваша фамилия"
style="register"
/>
<TextInput
name="email"
value={values.email}
handleChange={handleChange}
label="Email"
placeholder="Ваш email"
style="register"
/>
<PhoneInput
value={values.phone_number}
handleChange={handleChange}
operatorsInfo={false}
/>
<TextInput
name="country"
value={values.country}
handleChange={handleChange}
label="Страна"
placeholder="Ваша страна проживания"
style="register"
/>
<TextInput
name="city"
value={values.city}
handleChange={handleChange}
label="Город"
placeholder="Ваш город проживания"
style="register"
/>
</div>
<Button
text="Сохранить изменения"
className="text-sm font-semibold py-4 px-6 bg-orange text-white flex items-center rounded-2xl whitespace-nowrap"
type="submit"
/>
</form>
</div>
</div>
</div>
<ContactUs />
</div>
)
}
export default AccountPage

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -2,10 +2,10 @@ import React from 'react'
import { useForm } from '@/app/hooks/useForm' import { useForm } from '@/app/hooks/useForm'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
// import LoginButton from '@/app/components/ui/LoginButton' // import LoginButton from '@/app/components/ui/LoginButton'
import { HiOutlineEye, HiOutlineEyeOff } from 'react-icons/hi'
import showToast from '@/components/ui/Toast' import showToast from '@/components/ui/Toast'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { signIn } from 'next-auth/react' import { signIn } from 'next-auth/react'
import TextInput from '@/components/ui/TextInput'
// import PasswordRecovery from '@/app/components/ui/PasswordRecovery' // import PasswordRecovery from '@/app/components/ui/PasswordRecovery'
const validationRules = { const validationRules = {
@@ -52,50 +52,32 @@ const ClientView = () => {
return ( return (
<> <>
<form className="flex flex-col gap-1" onSubmit={handleSubmit}> <form className="flex flex-col gap-1" onSubmit={handleSubmit}>
<div className="mb-2"> <TextInput
<label className="block mb-2 text-gray-700" htmlFor="email"> value={values.email}
Ваш email: name="email"
</label> handleChange={handleChange}
<input placeholder="my_email@gmail.com"
type="email" style="register"
id="email" label="Ваш еmail"
placeholder="my_email@gmail.com" />
value={values.email}
onChange={handleChange}
className="w-full px-3 py-2 border text-black rounded-xl focus:outline-none focus:ring-2 focus:ring-mainblocks"
autoComplete="true"
/>
</div>
<div className="mb-4"> <TextInput
<label className="block mb-2 text-gray-700" htmlFor="email"> value={values.password}
Ваш пароль: name="password"
</label> handleChange={handleChange}
<div className="relative"> placeholder="Не менее 8 символов"
<input style="register"
type={isVisible ? 'text' : 'password'} label="Ваш пароль"
id="password" isPassword={true}
placeholder="Пароль" isVisible={isVisible}
value={values.password} togglePasswordVisibility={togglePasswordVisibility}
onChange={handleChange} />
className="w-full px-3 py-2 border text-black rounded-xl focus:outline-none focus:ring-2 focus:ring-mainblocks"
autoComplete="true"
/>
<button
type="button"
onClick={() => togglePasswordVisibility()}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{isVisible ? <HiOutlineEye /> : <HiOutlineEyeOff />}
</button>
</div>
</div>
{/* <div className="flex flex-row-reverse text-sm justify-between pb-2"> {/* <div className="flex flex-row-reverse text-sm justify-between pb-2">
<PasswordRecovery /> <PasswordRecovery />
</div> */} </div> */}
<Button <Button
text="Войти" text="Войти"
className="flex items-center justify-center bg-black rounded-2xl text-white text-base font-semibold py-3" className="flex items-center justify-center bg-orange rounded-2xl text-white text-base font-semibold py-3 mt-3"
type="submit" type="submit"
/> />
</form> </form>

View File

@@ -0,0 +1,5 @@
import { NextRequest } from 'next/server'
export async function PUT(req: NextRequest) {
// обновляем данные в аккаунте
}

View File

@@ -0,0 +1,17 @@
import { NextRequest } from 'next/server'
export async function GET(req: NextRequest) {
// получить список маршрутов/локаций пользователя
}
export async function POST(req: NextRequest) {
// добавить новый маршрут
}
export async function PUT(req: NextRequest) {
// обновить маршрут
}
export async function DELETE(req: NextRequest) {
// удалить маршрут
}

View File

@@ -75,14 +75,6 @@ const authOptions: NextAuthOptions = {
throw new Error('Все поля обязательны для заполнения') throw new Error('Все поля обязательны для заполнения')
} }
// console.log('Registration data:', {
// email: credentials.email,
// name: credentials.name,
// surname: credentials.surname,
// phone_number: credentials.phone_number,
// privacy_accepted: credentials.privacy_accepted,
// })
const res = await fetch( const res = await fetch(
`${process.env.BACKEND_URL}/register/clients/`, `${process.env.BACKEND_URL}/register/clients/`,
{ {
@@ -125,54 +117,7 @@ const authOptions: NextAuthOptions = {
}, },
}), }),
//регистрация менеджера //логин
CredentialsProvider({
id: 'register-credentials-manager',
name: 'RegisterManager',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
username: { label: 'Username', type: 'text' },
},
async authorize(credentials) {
try {
const res = await fetch(
`${process.env.BACKEND_URL}/register/business/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
username: credentials?.username,
}),
}
)
if (!res.ok) {
const text = await res.text()
console.error('Backend error response:', text)
throw new Error('Registration failed: ' + text)
}
const data = await res.json()
return {
id: data.user.id.toString(),
email: data.user.email,
name: data.user.firstName,
accessToken: data.access,
refreshToken: data.refresh,
userType: data.user.userType || 'manager',
}
} catch (error) {
console.error('Registration error:', error)
return null
}
},
}),
//логин обычный
CredentialsProvider({ CredentialsProvider({
name: 'Credentials', name: 'Credentials',
credentials: { credentials: {
@@ -212,50 +157,6 @@ const authOptions: NextAuthOptions = {
} }
}, },
}), }),
//логин для менеджеров
CredentialsProvider({
id: 'login-credentials-manager',
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
try {
const res = await fetch(
`${process.env.BACKEND_URL}/auth/login/business/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
}),
}
)
const data = await res.json()
// console.log('Business login response:', data)
if (!res.ok) {
throw new Error(data.error || 'Authentication failed')
}
return {
id: data.user.id.toString(),
email: data.user.email || `${data.user.phone_number}@example.com`,
name: data.user.username || data.user.title,
accessToken: data.access,
refreshToken: data.refresh,
userType: data.user.userType || 'manager',
}
} catch (error) {
console.error('Login error:', error)
return null
}
},
}),
], ],
callbacks: { callbacks: {
async jwt({ token, user, account }) { async jwt({ token, user, account }) {

View File

@@ -135,3 +135,21 @@ export interface AccountSidebarProps {
user: User user: User
navigation: NavigationItem[] navigation: NavigationItem[]
} }
export interface TelegramMessage {
source: string
name: string
phone_number: string
message: string
}
export type SourceType = 'account' | 'contact-us'
export interface TextAreaProps {
value: string
handleChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
label: string
name: string
placeholder: string
height?: string | number
}

View File

@@ -85,7 +85,7 @@ const AccountSidebar: React.FC<AccountSidebarProps> = ({
href={item.href} href={item.href}
className={`${ className={`${
isActive isActive
? 'bg-orange/80 text-white' ? 'bg-orange text-white'
: 'text-gray-600 hover:bg-gray-100' : 'text-gray-600 hover:bg-gray-100'
} flex items-center p-4 text-sm font-medium rounded-lg transition-colors`} } flex items-center p-4 text-sm font-medium rounded-lg transition-colors`}
> >

View File

@@ -0,0 +1,94 @@
'use client'
import React from 'react'
import { useForm } from '@/app/hooks/useForm'
import { getSourceFromPath } from '@/lib/utils/pathMapper'
import { usePathname } from 'next/navigation'
import TextInput from './ui/TextInput'
import TextAreaInput from './ui/TextAreaInput'
import PhoneInput from './ui/PhoneInput'
import showToast from './ui/Toast'
import Button from './ui/Button'
import { sendMessage } from '@/lib/telegram/sendMessage'
import useUserStore from '@/app/store/userStore'
const validationRules = {
source: { required: true },
name: { required: true },
phone_number: { required: true },
message: { required: true },
}
const ContactUs = () => {
const pathname = usePathname()
const source = getSourceFromPath(pathname)
const { user } = useUserStore()
const { values, handleChange, handleSubmit, resetField } = useForm(
{
source: source,
name: user?.name || '',
phone_number: user?.phone_number || '',
message: '',
},
validationRules,
async (values) => {
try {
await sendMessage(values)
showToast({
type: 'success',
message: 'Ваше сообщение отправлено!',
})
resetField('message')
} catch {
showToast({ type: 'error', message: 'Ой, что то пошло не так..' })
}
}
)
return (
<div className="rounded-2xl shadow overflow-hidden">
<div className="p-6 bg-white sm:p-8">
<div className="space-y-4">
<h2 className="text-2xl">Хотели бы связаться с нами?</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 h-full">
<div className="space-y-4 h-full flex flex-col">
<TextInput
label="Ваше имя"
value={values.name}
handleChange={handleChange}
placeholder="Имя"
name="name"
style="register"
/>
<PhoneInput
value={values.phone_number}
handleChange={handleChange}
operatorsInfo={false}
/>
</div>
<TextAreaInput
value={values.message}
handleChange={handleChange}
height={145}
label="Чем бы Вы хотели с нами поделиться?"
name="message"
placeholder="Что угодно - от предложений до жалоб, мы всегда рады!"
/>
</div>
<Button
text="Отправить"
className="text-sm font-semibold py-4 px-6 bg-black text-white flex items-center justify-center w-full rounded-2xl whitespace-nowrap"
type="submit"
/>
</form>
</div>
</div>
</div>
)
}
export default ContactUs

View File

@@ -0,0 +1,33 @@
import React from 'react'
import { TextAreaProps } from '@/app/types'
const TextAreaInput = ({
value,
handleChange,
label,
name,
placeholder,
height,
}: TextAreaProps) => {
return (
<div>
<label
className="block mb-2 font-medium text-sm text-gray-500"
htmlFor={name}
>
{label}
</label>
<textarea
id={name}
placeholder={placeholder}
value={value}
onChange={handleChange}
style={{ minHeight: height ? `${height}px` : '160px' }}
className="w-full px-3 py-2 border border-gray-300 text-black rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-white"
autoComplete={name}
/>
</div>
)
}
export default TextAreaInput

View File

@@ -0,0 +1,41 @@
import { TelegramMessage } from '@/app/types'
export const sendMessage = async (data: TelegramMessage) => {
const API_URL = process.env.NEXT_PUBLIC_API_URL
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json',
}
try {
const response = await fetch(`${API_URL}/send-message/`, {
method: 'POST',
headers,
body: JSON.stringify({
source: data.source,
name: data.name,
phone_number: data.phone_number,
message: data.message,
}),
})
if (!response.ok) {
let errorMessage = `Failed to send form data: ${response.status} ${response.statusText}`
try {
const errorData = await response.text()
if (errorData) {
errorMessage += ` - ${errorData}`
}
} catch (e) {
console.error('Error parsing error response:', e)
}
throw new Error(errorMessage)
}
const text = await response.text()
return text ? JSON.parse(text) : null
} catch (error) {
console.error('Error sending form data:', error)
throw error
}
}

View File

@@ -0,0 +1,15 @@
import { SourceType } from '@/app/types'
const pathToSourceMap: Record<string, SourceType> = {
techSupport: 'contact-us', // берем в кавычки значение с дефисом
account: 'account',
}
export const getSourceFromPath = (pathname: string): SourceType => {
const cleanPath = pathname.replace(/^\//, '')
const source = pathToSourceMap[cleanPath] || 'contact-us' // дефолтное значение
// console.log('Final source:', source)
return source
}