initiate drf app
92
.gitignore
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env*.local
|
||||
.env.production
|
||||
*.env
|
||||
.env.*
|
||||
|
||||
# Debugging and logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Next.js build output
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Production build
|
||||
/build
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
|
||||
# Dependency management
|
||||
.pnp/
|
||||
.pnp.js
|
||||
yarn.lock
|
||||
|
||||
# Local configuration
|
||||
.vscode/
|
||||
.vercel/
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# TypeScript build info
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.pem
|
||||
|
||||
# Byte-compiled Python files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Виртуальные окружения
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
bin/
|
||||
local/
|
||||
.include/
|
||||
*.egg
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
|
||||
# Секретные ключи и конфигурации
|
||||
*.env
|
||||
|
||||
# Системные файлы
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Базы данных SQLite
|
||||
db.sqlite3
|
||||
database
|
||||
|
||||
# Static and media files
|
||||
staticfiles/
|
||||
media/
|
||||
|
||||
# Миграции
|
||||
*/migrations/*.pyc
|
||||
*/migrations/*.py~
|
||||
|
||||
# Компиляция файлов
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Docker файлы
|
||||
*.pid
|
||||
*.dockerignore
|
||||
docker-compose.override.yml
|
||||
|
||||
.DS_Store
|
||||
97
README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Trip with Benefits
|
||||
|
||||
Это репозиторий сайта tripwb. Этот проект включает в себя фронтенд на Next.js и бэкенд на Django.py с базой данных PostgreSQL.
|
||||
|
||||
## Описание
|
||||
|
||||
Сайт приложения tripwb является аггрегатором для поиска и перевозки посылок
|
||||
|
||||
## Технологии
|
||||
|
||||
### Фронтенд
|
||||
|
||||
- **Next.js** - библиотека для создания пользовательских интерфейсов.
|
||||
|
||||
### Бэкенд
|
||||
|
||||
- **Django.py** - веб-фреймворк для Python.
|
||||
- **PostgreSQL** - реляционная база данных для хранения данных.
|
||||
|
||||
## Установка
|
||||
|
||||
### Предварительные требования
|
||||
|
||||
Для запуска проекта вам потребуются:
|
||||
|
||||
- Node.js (рекомендуется версия 20.x или выше)
|
||||
- PostgreSQL (рекомендуется версия 12.x или выше)
|
||||
|
||||
### Шаги для установки
|
||||
|
||||
1. **Клонирование репозитория:**
|
||||
|
||||
```sh
|
||||
git clone https://gitea.a3-global.com/sysadminix/tripwithbonus.git
|
||||
cd tripwb
|
||||
```
|
||||
|
||||
2. **Установка зависимостей для фронтенда и бэкенда:**
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
cd backend
|
||||
pipenv shell
|
||||
pipenv install
|
||||
```
|
||||
|
||||
3. **Настройка базы данных:**
|
||||
|
||||
Создайте базу данных PostgreSQL и выполните миграции:
|
||||
|
||||
```sh
|
||||
createdb tripwbDB
|
||||
# Выполните миграции, если они имеются. В проекте откройте директорию backend
|
||||
cd backend
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
4. **Настройка переменных окружения:**
|
||||
|
||||
Создайте файл `.env` в корневой директории и добавьте необходимые переменные окружения:
|
||||
|
||||
```env
|
||||
# telegram data
|
||||
BOT_TOKEN
|
||||
CHAT_ID
|
||||
|
||||
# database connection
|
||||
|
||||
DB_USER
|
||||
DB_HOST
|
||||
DB_NAME
|
||||
DB_PASSWORD
|
||||
DB_PORT = 5432
|
||||
```
|
||||
|
||||
5. **Локальная разработка:**
|
||||
|
||||
Откройте два терминала или используйте вкладки в одном терминале.
|
||||
|
||||
В первом терминале запустите бэкенд:
|
||||
|
||||
```
|
||||
cd backend
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Во втором терминале запустите фронтенд:
|
||||
|
||||
```
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Теперь проект будет доступен по адресу `http://localhost:3000`.
|
||||
15
backend/Pipfile
Normal file
@@ -0,0 +1,15 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
djangorestframework = "*"
|
||||
python-dotenv = "*"
|
||||
psycopg2 = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
81
backend/Pipfile.lock
generated
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b373803326406c6ad8118054bb515b95be11d1fba2e9252d0914839e5c6ed309"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.12"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47",
|
||||
"sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.8.1"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284",
|
||||
"sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==5.2.1"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
"sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361",
|
||||
"sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.16.0"
|
||||
},
|
||||
"psycopg2": {
|
||||
"hashes": [
|
||||
"sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4",
|
||||
"sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11",
|
||||
"sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2",
|
||||
"sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e",
|
||||
"sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716",
|
||||
"sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067",
|
||||
"sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442",
|
||||
"sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2",
|
||||
"sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b",
|
||||
"sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.9.10"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5",
|
||||
"sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272",
|
||||
"sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.5.3"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
0
backend/api/__init__.py
Normal file
3
backend/api/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
0
backend/api/main/__init__.py
Normal file
9
backend/api/main/serializers.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from mainpage.models import FAQ
|
||||
|
||||
class FAQMainSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FAQ
|
||||
fields = "__all__"
|
||||
|
||||
19
backend/api/main/views.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from api.utils.decorators import handle_exceptions
|
||||
|
||||
from api.main.serializers import FAQMainSerializer
|
||||
from mainpage.models import FAQ
|
||||
|
||||
class FAQView(APIView):
|
||||
@handle_exceptions
|
||||
def get(self, request):
|
||||
|
||||
faqs = FAQ.objects.all()
|
||||
|
||||
data = {
|
||||
'faqs': FAQMainSerializer(faqs, many=True).data
|
||||
}
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
0
backend/api/migrations/__init__.py
Normal file
3
backend/api/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
backend/api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
7
backend/api/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from api.main.views import FAQView
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
||||
]
|
||||
56
backend/api/utils/decorators.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from functools import wraps
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
from django.http import Http404
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
def handle_exceptions(func):
|
||||
"""
|
||||
Обработчик ошибок для API endpoints
|
||||
Обрабатывает различные типы исключений и возвращает соответствующие HTTP статусы
|
||||
Текущие обрабоки:
|
||||
- Ошибки валидации - HTTP400
|
||||
- Объект не найден - HTTP404
|
||||
- Ошибки доступа - HTTP403
|
||||
- Обработки DRF исключений - вернется статус код от DRF
|
||||
- Необработанные исключения - HTTP500
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ValidationError as e:
|
||||
# ошибки валидации
|
||||
return Response(
|
||||
{"error": e.messages if hasattr(e, 'messages') else str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Http404 as e:
|
||||
# объект не найден
|
||||
return Response(
|
||||
{"error": str(e) or "Запрашиваемый ресурс не найден"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except PermissionDenied as e:
|
||||
# ошибки доступа
|
||||
return Response(
|
||||
{"error": str(e) or "У вас нет прав для выполнения этого действия"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
except APIException as e:
|
||||
# обработка DRF исключений
|
||||
return Response(
|
||||
{"error": str(e)},
|
||||
status=e.status_code
|
||||
)
|
||||
except Exception as e:
|
||||
# необработанные исключения
|
||||
return Response(
|
||||
{
|
||||
"error": "Произошла внутренняя ошибка сервера",
|
||||
"detail": str(e)
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
return wrapper
|
||||
0
backend/base/__init__.py
Normal file
16
backend/base/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for base project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'base.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
114
backend/base/settings.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(dotenv_path=BASE_DIR / './.env')
|
||||
|
||||
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
DEBUG = os.environ.get("DEBUG_MODE")
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'api.apps.ApiConfig',
|
||||
'routes.apps.RoutesConfig',
|
||||
'mainpage.apps.MainpageConfig',
|
||||
'rest_framework',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'base.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'base.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.environ.get("DB_NAME"),
|
||||
"USER": os.environ.get("DB_USER"),
|
||||
"PASSWORD": os.environ.get("DB_PASSWORD"),
|
||||
"HOST": os.environ.get("DB_HOST"),
|
||||
"PORT": os.environ.get("DB_PORT"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
23
backend/base/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
URL configuration for base project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('api.urls')),
|
||||
]
|
||||
16
backend/base/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for base project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'base.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
0
backend/mainpage/__init__.py
Normal file
4
backend/mainpage/admin.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from .models import FAQ
|
||||
|
||||
admin.site.register(FAQ)
|
||||
6
backend/mainpage/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MainpageConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mainpage'
|
||||
26
backend/mainpage/migrations/0001_initial.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.1 on 2025-05-15 14:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FAQ',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=250)),
|
||||
('content', models.CharField(max_length=800)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'FAQ',
|
||||
'verbose_name_plural': 'FAQs',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/mainpage/migrations/__init__.py
Normal file
14
backend/mainpage/models.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.db import models
|
||||
|
||||
class FAQ (models.Model):
|
||||
title = models.CharField(max_length=250)
|
||||
content = models.CharField(max_length=800)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'FAQ'
|
||||
verbose_name_plural = 'FAQs'
|
||||
ordering = ['id']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
3
backend/mainpage/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
22
backend/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'base.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
backend/routes/__init__.py
Normal file
3
backend/routes/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/routes/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RoutesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'routes'
|
||||
24
backend/routes/constants/routeChoices.py
Normal file
@@ -0,0 +1,24 @@
|
||||
type_transport_choices = [
|
||||
("road", "Авто"),
|
||||
("avia", "Авиа"),
|
||||
('both', "Любой"),
|
||||
]
|
||||
|
||||
transfer_location_choices = [
|
||||
("airport", "В аэропорту"),
|
||||
("city", "По городу"),
|
||||
("other", "По договоренности")
|
||||
]
|
||||
|
||||
cargo_type_choices = [
|
||||
("letter", "Письмо или Документы"),
|
||||
("package", "Посылка (до 30кг)"),
|
||||
("passenger", "Попутчик"),
|
||||
("parcel", "Бандероль (до 5кг)"),
|
||||
("cargo", "Груз (свыше 30 кг)"),
|
||||
]
|
||||
|
||||
owner_type_choices = [
|
||||
("customer", "Заказчик"),
|
||||
("mover", "Перевозчик")
|
||||
]
|
||||
0
backend/routes/migrations/__init__.py
Normal file
3
backend/routes/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
backend/routes/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
backend/routes/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
127
docker-compose.yml
Normal file
@@ -0,0 +1,127 @@
|
||||
name: tripwb
|
||||
|
||||
networks:
|
||||
tripwb:
|
||||
name: tripwb
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
container_name: tripwb-caddy-server
|
||||
depends_on:
|
||||
tripwb-backend-app:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- caddy-config:/config
|
||||
- caddy-data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
networks:
|
||||
- tripwb
|
||||
|
||||
tripwb-frontend-app:
|
||||
build:
|
||||
context: frontend/
|
||||
# image: tripwb-frontend
|
||||
container_name: tripwb-frontend-app
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
||||
BACKEND_URL: ${BACKEND_URL}
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
tripwb-backend-app:
|
||||
condition: service_healthy
|
||||
caddy:
|
||||
condition: service_started
|
||||
# ports:
|
||||
# - 9000:3000
|
||||
networks:
|
||||
- tripwb
|
||||
tripwb-backend-app:
|
||||
build:
|
||||
context: backend/
|
||||
# image:tripwb-backend
|
||||
container_name: tripwb-backend-app
|
||||
environment:
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
DEBUG_MODE: ${DEBUG_MODE}
|
||||
API_KEY: ${API_KEY}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_NAME: ${DB_NAME}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_PORT: ${DB_PORT}
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-s', '-o', '-f', 'http://tripwb-backend-app:8000']
|
||||
# interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- tripwb-backend-app-uploads:/root/tripwb/uploads
|
||||
# ports:
|
||||
# - 8000:8000
|
||||
networks:
|
||||
- tripwb
|
||||
|
||||
# pgadmin-app:
|
||||
# image: dpage/pgadmin4
|
||||
# container_name: pgadmin-app
|
||||
# environment:
|
||||
# DB_PORT: ${DB_PORT}
|
||||
# DB_HOST: ${DB_HOST}
|
||||
# DB_NAME: ${DB_NAME}
|
||||
# DB_PASSWORD: ${DB_PASSWORD}
|
||||
# DB_USER: ${DB_USER}
|
||||
# BOT_TOKEN: ${BOT_TOKEN}
|
||||
# CHAT_ID: ${BOT_TOKEN}
|
||||
# JWT_SECRET: ${BOT_TOKEN}
|
||||
# NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
||||
# PGADMIN_DEFAULT_EMAIL: timofey.syr17@gmail.com
|
||||
# PGADMIN_DEFAULT_PASSWORD: passWoRd
|
||||
# depends_on:
|
||||
# postgres:
|
||||
# condition: service_healthy
|
||||
# ports:
|
||||
# - 81:80
|
||||
# networks:
|
||||
# - tripwb
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
restart: always
|
||||
container_name: tripwb-db
|
||||
env_file: backend/.env
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "sh -c 'pg_isready -U ${DB_USER} -d ${DB_NAME}'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- tripwb
|
||||
|
||||
volumes:
|
||||
pg-data:
|
||||
caddy-config:
|
||||
caddy-data:
|
||||
tripwb-backend-app-uploads:
|
||||
41
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
308
frontend/app/(urls)/search/components/SearchCard.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import React from 'react'
|
||||
import Image, { StaticImageData } from 'next/image'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { SearchCardProps } from '@/app/types/index'
|
||||
|
||||
const SearchCard = ({
|
||||
id,
|
||||
username,
|
||||
userImg,
|
||||
start_point,
|
||||
country_from,
|
||||
country_from_icon,
|
||||
country_from_code,
|
||||
end_point,
|
||||
country_to,
|
||||
country_to_icon,
|
||||
country_to_code,
|
||||
cargo_type,
|
||||
user_request,
|
||||
user_comment,
|
||||
moving_type,
|
||||
estimated_date,
|
||||
day_out,
|
||||
day_in,
|
||||
}: SearchCardProps) => {
|
||||
const getUserRequestStyles = () => {
|
||||
if (user_request === 'Нужен перевозчик') {
|
||||
return 'text-[#065bff]'
|
||||
}
|
||||
return 'text-[#45c226]'
|
||||
}
|
||||
|
||||
const setMovingTypeIcon = () => {
|
||||
if (moving_type === 'Авиатранспорт') {
|
||||
return '/images/airplane.png'
|
||||
}
|
||||
return '/images/car.png'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* десктоп */}
|
||||
<div className="hidden sm:block">
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 w-full my-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<Image
|
||||
src={userImg}
|
||||
alt={username}
|
||||
width={52}
|
||||
height={52}
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-base font-semibold">{username}</div>
|
||||
<div className="text-gray-500">|</div>
|
||||
<div
|
||||
className={`text-base font-semibold ${getUserRequestStyles()}`}
|
||||
>
|
||||
{user_request}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
Тип посылки:{' '}
|
||||
<span className="text-orange font-semibold ml-1">
|
||||
{cargo_type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
text="Откликнуться"
|
||||
className="bg-orange hover:bg-orange/80 text-white px-10 py-3 text-base font-semibold transition-colors cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#f8f8f8] rounded-lg p-5">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-gray-600">{user_comment}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm flex justify-end pt-2">
|
||||
Объявление № {id}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<div className="flex flex-col">
|
||||
{user_request === 'Нужен перевозчик' ? (
|
||||
<span className="text-gray-500">Забрать из:</span>
|
||||
) : (
|
||||
<span className="text-gray-500">Выезжаю из:</span>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={country_from_icon}
|
||||
width={26}
|
||||
height={13}
|
||||
alt={country_from_code}
|
||||
/>
|
||||
<span className="text-gray-400 pr-2 pl-1">
|
||||
{country_from_code}
|
||||
</span>
|
||||
<span className="text-base font-semibold">
|
||||
{start_point} / {country_from}
|
||||
</span>
|
||||
</div>
|
||||
{user_request === 'Могу перевезти' && (
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<span className="text-sm font-normal">Отправление:</span>{' '}
|
||||
<span className="text-sm font-semibold">
|
||||
{day_out?.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-base font-semibold">{moving_type}</span>
|
||||
<Image
|
||||
src={setMovingTypeIcon()}
|
||||
width={15}
|
||||
height={15}
|
||||
alt="route vector"
|
||||
className="w-[15px] h-[15px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-[500px] mx-auto my-1">
|
||||
<Image
|
||||
src="/images/vector.svg"
|
||||
width={500}
|
||||
height={6}
|
||||
alt="route vector"
|
||||
className="absolute"
|
||||
/>
|
||||
<div className="w-5 h-5 rounded-full bg-white border-3 border-[#065bff] relative z-10" />
|
||||
<div className="w-5 h-5 rounded-full bg-white border-3 border-[#45c226] relative z-10" />
|
||||
</div>
|
||||
|
||||
{user_request === 'Нужен перевозчик' && (
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-sm font-normal">Дата доставки:</span>{' '}
|
||||
<span className="text-sm font-semibold">
|
||||
{estimated_date.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col -mb-[14px]">
|
||||
{user_request === 'Нужен перевозчик' ? (
|
||||
<div className="text-base text-gray-500">Доставить в:</div>
|
||||
) : (
|
||||
<div className="text-base text-gray-500">Прибываю в:</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={country_to_icon}
|
||||
width={26}
|
||||
height={13}
|
||||
alt={country_to_code}
|
||||
/>
|
||||
<span className="text-gray-400 pr-2 pl-1">
|
||||
{country_to_code}
|
||||
</span>
|
||||
<span className="text-base font-semibold">
|
||||
{end_point} / {country_to}
|
||||
</span>
|
||||
</div>
|
||||
{user_request === 'Могу перевезти' && (
|
||||
<div className="text-sm text-gray-500">
|
||||
<span className="text-sm font-normal">Прибытие:</span>{' '}
|
||||
<span className="text-sm font-semibold">
|
||||
{day_in?.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* мобилка */}
|
||||
<div className="block sm:hidden">
|
||||
<div className="bg-white rounded-xl shadow-lg p-4 w-full my-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={`text-sm font-semibold ${getUserRequestStyles()}`}>
|
||||
{user_request}
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
Тип посылки: <span className="text-orange">{cargo_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between mt-5 mb-2 gap-3">
|
||||
<div className="min-w-[64px] w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src={userImg}
|
||||
alt={username}
|
||||
width={52}
|
||||
height={52}
|
||||
className="rounded-full object-cover aspect-square w-[52px] h-[52px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-[#f8f8f8] rounded-lg text-sm font-normal p-4 flex-1">
|
||||
{user_comment}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex justify-end">
|
||||
Объявление № {id}
|
||||
</div>
|
||||
|
||||
{user_request === 'Нужен перевозчик' ? (
|
||||
<span className="text-gray-500 pl-7 text-sm">Забрать из:</span>
|
||||
) : (
|
||||
<span className="text-gray-500 pl-7 text-sm">Выезжаю из:</span>
|
||||
)}
|
||||
<div className="flex flex-row items-stretch mt-4 mb-2 gap-4">
|
||||
<div className="flex flex-col items-center h-[150px] relative">
|
||||
<div className="h-full w-[10px] flex items-center justify-center relative">
|
||||
<Image
|
||||
src="/images/vectormob.png"
|
||||
width={6}
|
||||
height={100}
|
||||
alt="route vector"
|
||||
className="absolute h-[150px] w-auto"
|
||||
/>
|
||||
<div className="absolute -top-[10px] w-4 h-4 rounded-full bg-white border-3 border-[#065bff] z-10" />
|
||||
<div className="absolute -bottom-[10px] w-4 h-4 rounded-full bg-white border-3 border-[#45c226] z-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div className="flex items-center -mt-[14px]">
|
||||
<Image
|
||||
src={country_from_icon}
|
||||
width={26}
|
||||
height={13}
|
||||
alt={country_from_code}
|
||||
/>
|
||||
<span className="text-gray-400 text-sm pr-2 pl-1">
|
||||
{country_from_code}
|
||||
</span>
|
||||
<span className="text-base font-semibold">
|
||||
{start_point} / {country_from}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col ">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-base">{moving_type}</span>
|
||||
<Image
|
||||
src={setMovingTypeIcon()}
|
||||
width={15}
|
||||
height={15}
|
||||
alt="route vector"
|
||||
className="w-[15px] h-[15px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[165px] h-[2px] bg-gray-200 my-2" />
|
||||
<div className="text-sm">
|
||||
Дата доставки: {estimated_date.toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col -mb-[14px]">
|
||||
{user_request === 'Нужен перевозчик' ? (
|
||||
<div className="text-sm text-gray-500">Доставить в:</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">Прибываю в:</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={country_to_icon}
|
||||
width={26}
|
||||
height={13}
|
||||
alt={country_to_code}
|
||||
/>
|
||||
<span className="text-gray-400 pr-2 pl-1">
|
||||
{country_to_code}
|
||||
</span>
|
||||
<span className="text-base font-semibold">
|
||||
{end_point} / {country_to}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user_request === 'Могу перевезти' && (
|
||||
<div className="text-sm text-gray-500 mt-3 ml-7">
|
||||
<span className="text-sm font-normal">Прибытие:</span>{' '}
|
||||
<span className="text-sm font-semibold">
|
||||
{day_in?.toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchCard
|
||||
7
frontend/app/(urls)/search/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
const SearchPage = () => {
|
||||
return <div>SearchPage</div>
|
||||
}
|
||||
|
||||
export default SearchPage
|
||||
BIN
frontend/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
27
frontend/app/globals.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
:root {
|
||||
--background: #eeebeb;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-orange: #ff613a;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--background: #eeebeb;
|
||||
--foreground: #171717;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
37
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Geist, Geist_Mono } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Отправка посылок в любую точку мира | TripWB',
|
||||
description:
|
||||
'Международная отправка посылок ✓ Отправка посылки в любую точку планеты ✓ Приемлемая цена отправки посылки ✓ Доставка в кратчайшие сроки ➡️ Обращайтесь к нам',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<body className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-grow">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
245
frontend/app/page.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import AddressSelector from '@/components/AddressSelector'
|
||||
import SearchCard from '@/app/(urls)/search/components/SearchCard'
|
||||
import FAQ from '@/components/FAQ'
|
||||
import { data } from '@/app/staticData'
|
||||
import { routes } from '@/app/staticData'
|
||||
import Button from '@/components/ui/Button'
|
||||
import News from '@/components/News'
|
||||
import { getFAQs } from '@/lib/fetchFAQ'
|
||||
|
||||
export default async function Home() {
|
||||
const faqs = await getFAQs()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-[93%] mx-auto">
|
||||
{/* main */}
|
||||
<div className="flex items-center justify-center space-x-16">
|
||||
<div>
|
||||
<Image
|
||||
src="/images/box1.png"
|
||||
alt="main"
|
||||
width={220}
|
||||
height={220}
|
||||
className="hidden sm:block"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center space-y-5">
|
||||
<h1 className="text-3xl sm:text-4xl text-center font-bold">
|
||||
<div className="pb-1">
|
||||
Сервис по <span className="text-orange">поиску</span>{' '}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-orange">перевозчиков</span> посылок
|
||||
</div>
|
||||
</h1>
|
||||
<p className="text-base sm:text-lg text-center pb-3">
|
||||
Доставка посылок с попутчиками: от документов до крупногабаритных
|
||||
грузов
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/images/box2.png"
|
||||
alt="main"
|
||||
width={220}
|
||||
height={220}
|
||||
className="hidden sm:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* форма на серч */}
|
||||
<AddressSelector />
|
||||
|
||||
<div className="text-lg font-normal text-[#272424] underline decoration-orange underline-offset-4 mb-20 hover:text-orange transition-colors cursor-pointer">
|
||||
Я могу взять с собой посылку
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center mb-8">
|
||||
<h2 className="text-4xl text-center font-bold">Все объявления</h2>
|
||||
<div className="text-base my-3">
|
||||
На нашем сайте размещено уже{' '}
|
||||
<span className="text-orange">{routes}</span> объявлений по отправке и
|
||||
перевозке посылок
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* первые пять серч карточек -- бекенд??? */}
|
||||
<div className="w-full max-w-[1250px] mx-auto">
|
||||
<div className="grid grid-cols-1 gap-4 w-full">
|
||||
{data.map((card) => (
|
||||
<SearchCard key={card.id} {...card} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center py-4">
|
||||
<Button
|
||||
text="Разместить объявление"
|
||||
className=" bg-orange text-white text-xl font-semibold px-6 py-3 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* how it works */}
|
||||
<div className="w-full max-w-[1250px] mx-auto my-20">
|
||||
<h2 className="text-4xl text-center font-bold">Как это работает</h2>
|
||||
<div className="flex flex-col items-center justify-center text-base font-medium text-center py-4 mb-4">
|
||||
<span>
|
||||
TWB - это сервис, созданный для того, чтобы отправитель и перевозчик
|
||||
нашли друг-друга!
|
||||
</span>
|
||||
<span>
|
||||
Наш сервис предлагает вам прямые контакты, а не является посредником
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center sm:items-start sm:space-x-4 md:space-x-8 lg:space-x-20 space-y-8 sm:space-y-0">
|
||||
<div className="flex flex-col items-center w-full sm:w-1/3 px-4 sm:px-2">
|
||||
<div className="flex flex-col items-center h-[260px] sm:h-[300px]">
|
||||
<Image
|
||||
src="/images/laptop.png"
|
||||
alt="laptop"
|
||||
width={230}
|
||||
height={230}
|
||||
className="mb-6 w-[180px] h-[180px] sm:w-[230px] sm:h-[230px]"
|
||||
/>
|
||||
<div className="text-xl sm:text-2xl font-semibold text-center">
|
||||
Найдите перевозчика
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm sm:text-base text-gray-600 text-center px-2 sm:px-4 md:px-7">
|
||||
В форме поиска укажите откуда и куда Вам нужно доставить посылку,
|
||||
нажмите кнопку "найти перевозчика". Если по вашему запросу ничего
|
||||
не найдено - Вы можете сами разместить объявление и тогда
|
||||
перевозчики Вас найдут.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center w-full sm:w-1/3 px-4 sm:px-2">
|
||||
<div className="flex flex-col items-center h-[260px] sm:h-[300px]">
|
||||
<Image
|
||||
src="/images/phone.png"
|
||||
alt="phone"
|
||||
width={230}
|
||||
height={230}
|
||||
className="mb-6 w-[180px] h-[180px] sm:w-[230px] sm:h-[230px]"
|
||||
/>
|
||||
<div className="text-xl sm:text-2xl font-semibold text-center">
|
||||
Свяжитесь с перевозчиком
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm sm:text-base text-gray-600 text-center px-2 sm:px-4 md:px-7">
|
||||
Нажмите на кнопку «ОТКЛИКНУТЬСЯ», свяжитесь и договоритесь о месте
|
||||
встречи и условиях перевозки
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center w-full sm:w-1/3 px-4 sm:px-2">
|
||||
<div className="flex flex-col items-center h-[260px] sm:h-[300px]">
|
||||
<Image
|
||||
src="/images/package.png"
|
||||
alt="package"
|
||||
width={230}
|
||||
height={230}
|
||||
className="mb-6 w-[180px] h-[180px] sm:w-[230px] sm:h-[230px]"
|
||||
/>
|
||||
<div className="text-xl sm:text-2xl font-semibold text-center">
|
||||
Передайте посылку
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm sm:text-base text-gray-600 text-center px-2 sm:px-4 md:px-7">
|
||||
Встречайтесь, знакомьтесь и передавайте посылку
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center pt-8 sm:pt-10">
|
||||
<Button
|
||||
text="Отправить посылку"
|
||||
className="bg-orange hover:bg-orange/80 text-white text-base sm:text-lg px-8 sm:px-12 py-2.5 sm:py-3 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* преимущества */}
|
||||
<div className="w-full max-w-[1250px] mx-auto my-10 px-4 sm:px-6">
|
||||
<h2 className="text-3xl sm:text-4xl text-center font-bold mb-12">
|
||||
Преимущества сервиса
|
||||
</h2>
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-6 lg:gap-4">
|
||||
<div className="flex flex-col w-full lg:w-1/4 space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Прямой контакт
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Общаешься напрямую с перевозчиком, никаких посредников
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Своя цена
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Стоимость перевозки самостоятельно обговариваете с перевозчиком.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Нет доп. расходов
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Никаких комиссий, переплат и дополнительных расходов за
|
||||
отправку.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-auto px-4 sm:px-0">
|
||||
<Image
|
||||
src="/images/advantage.svg"
|
||||
alt="advantages"
|
||||
width={648}
|
||||
height={403}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full lg:w-1/4 space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Уведомления
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Можешь самостоятельно найти перевозчиков или разместить
|
||||
объявление на сайте.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Удобный поиск
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Как только по твоему объявлению найдется перевозчик мы сообщим
|
||||
на E-mail.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-xl text-center sm:text-left font-semibold">
|
||||
Экономия времени
|
||||
</div>
|
||||
<span className="text-base text-center sm:text-left text-gray-600">
|
||||
Не нужно искать группы, чаты, и кидать "клич", а просто
|
||||
достаточно разместить объявление на сайте.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* FAQ */}
|
||||
<FAQ faqs={faqs} />
|
||||
|
||||
{/* новости */}
|
||||
<News />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
frontend/app/staticData/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import avatar from '../../public/images/avatar.png'
|
||||
import belarusIcon from '../../public/images/belarus.png'
|
||||
import russiaIcon from '../../public/images/russia.png'
|
||||
|
||||
const userImg = avatar
|
||||
const blIcon = belarusIcon
|
||||
const ruIcon = russiaIcon
|
||||
|
||||
export const routes = 12845
|
||||
|
||||
export const data = [
|
||||
{
|
||||
id: 1123,
|
||||
username: 'John Doe',
|
||||
userImg: userImg,
|
||||
start_point: 'Минск',
|
||||
country_from: 'Беларусь',
|
||||
end_point: 'Москва',
|
||||
country_to: 'Россия',
|
||||
cargo_type: 'Документы',
|
||||
user_request: 'Нужен перевозчик',
|
||||
user_comment: 'Нужно перевезти документы из Минска в Москву',
|
||||
country_from_icon: blIcon,
|
||||
country_to_icon: ruIcon,
|
||||
country_from_code: 'BY',
|
||||
country_to_code: 'RU',
|
||||
moving_type: 'Авиатранспорт',
|
||||
estimated_date: new Date(2025, 4, 15),
|
||||
},
|
||||
{
|
||||
id: 2423,
|
||||
username: 'John Doe',
|
||||
userImg: userImg,
|
||||
start_point: 'Минск',
|
||||
country_from: 'Беларусь',
|
||||
end_point: 'Москва',
|
||||
country_to: 'Россия',
|
||||
cargo_type: 'Документы',
|
||||
user_request: 'Могу перевезти',
|
||||
user_comment: 'Нужно перевезти документы из Минска в Москву',
|
||||
moving_type: 'Автоперевозка',
|
||||
estimated_date: new Date(2025, 5, 18),
|
||||
country_from_icon: blIcon,
|
||||
country_to_icon: ruIcon,
|
||||
country_from_code: 'BY',
|
||||
country_to_code: 'RU',
|
||||
day_out: new Date(2025, 5, 21),
|
||||
day_in: new Date(2025, 5, 25),
|
||||
},
|
||||
]
|
||||
|
||||
export const news = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Высокий уровень вовлечения представителей целевой аудитории',
|
||||
description:
|
||||
'Значимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса для',
|
||||
image: '/images/news.svg',
|
||||
slug: 'vysoki-uroven-vlezheniya-predstaviteley-tselyovoi-auditorii',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Высокий уровень вовлечения представителей целевой аудитории',
|
||||
description:
|
||||
'Значимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса для',
|
||||
image: '/images/news.svg',
|
||||
slug: 'vysoki-uroven-vlezheniya-predstaviteley-tselyovoi-auditorii',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Высокий уровень вовлечения представителей целевой аудитории',
|
||||
description:
|
||||
'Значимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса дляЗначимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса дляЗначимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса дляЗначимость этих проблем настолько очевидна, что экономическая повестка сегодняшнего дня не оставляет шанса для',
|
||||
image: '/images/news.svg',
|
||||
slug: 'vysoki-uroven-vlezheniya-predstaviteley-tselyovoi-auditorii',
|
||||
},
|
||||
]
|
||||
56
frontend/app/types/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import Image, { StaticImageData } from 'next/image'
|
||||
|
||||
export interface TextInputProps {
|
||||
value: string
|
||||
handleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
label?: string
|
||||
placeholder?: string
|
||||
name: string
|
||||
type?: 'text' | 'email' | 'password'
|
||||
className?: string
|
||||
maxLength?: number
|
||||
tooltip?: string | React.ReactNode
|
||||
}
|
||||
|
||||
export interface ButtonProps {
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
text?: string
|
||||
type?: 'button'
|
||||
}
|
||||
|
||||
export interface SearchCardProps {
|
||||
id: number
|
||||
username: string
|
||||
userImg: string | StaticImageData
|
||||
start_point: string
|
||||
country_from: string
|
||||
country_from_icon: string | StaticImageData
|
||||
country_from_code: string
|
||||
end_point: string
|
||||
country_to: string
|
||||
country_to_icon: string | StaticImageData
|
||||
country_to_code: string
|
||||
cargo_type: string
|
||||
user_request: string
|
||||
moving_type: string
|
||||
estimated_date: Date
|
||||
user_comment: string
|
||||
day_out?: Date
|
||||
day_in?: Date
|
||||
}
|
||||
|
||||
export interface AccordionProps {
|
||||
title: string
|
||||
content: string | React.ReactNode
|
||||
}
|
||||
|
||||
export interface FAQ {
|
||||
id: number
|
||||
title: string
|
||||
content: string | React.ReactNode
|
||||
}
|
||||
|
||||
export interface FAQProps {
|
||||
faqs: FAQ[]
|
||||
}
|
||||
45
frontend/components/AddressSelector.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import TextInput from './ui/TextInput'
|
||||
import Button from './ui/Button'
|
||||
|
||||
export default function AddressSelector() {
|
||||
const [fromAddress, setFromAddress] = useState('')
|
||||
const [toAddress, setToAddress] = useState('')
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-4 sm:p-6 w-full my-2 sm:my-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:gap-3">
|
||||
<div className="w-full sm:flex-[3] min-w-0 sm:px-1">
|
||||
<TextInput
|
||||
placeholder="Минск, Беларусь"
|
||||
tooltip="Укажите пункт (Город/Страна), откуда необходимо забрать посылку."
|
||||
label="Забрать посылку из"
|
||||
value={fromAddress}
|
||||
handleChange={(e) => setFromAddress(e.target.value)}
|
||||
name="fromAddress"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:flex-[3] min-w-0 sm:px-1">
|
||||
<TextInput
|
||||
placeholder="Москва, Россия"
|
||||
label="Доставить посылку в"
|
||||
tooltip="Укажите пункт (Город/Страна), куда необходимо доставить посылку."
|
||||
value={toAddress}
|
||||
handleChange={(e) => setToAddress(e.target.value)}
|
||||
name="toAddress"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
text="Найти перевозчика"
|
||||
className="w-full sm:w-auto sm:flex-1 whitespace-nowrap bg-orange hover:bg-orange/80 text-white p-4"
|
||||
/>
|
||||
<Button
|
||||
text="Найти посылку"
|
||||
className="w-full sm:w-auto sm:flex-1 whitespace-nowrap bg-gray-100 hover:bg-gray-200 text-gray-800 p-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
frontend/components/EmailHandler.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const EmailHandler = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
name="email"
|
||||
placeholder="Введите ваш e-mail"
|
||||
className="w-full px-4 py-3 rounded-md bg-white text-black placeholder:text-gray-400"
|
||||
/>
|
||||
<button className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center justify-center cursor-pointer">
|
||||
<Image
|
||||
src="/images/subscribe.png"
|
||||
alt="submit"
|
||||
width={50}
|
||||
height={50}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailHandler
|
||||
20
frontend/components/FAQ.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import Accordion from './ui/Accordion'
|
||||
import { FAQProps } from '@/app/types'
|
||||
|
||||
const FAQ: React.FC<FAQProps> = ({ faqs }) => {
|
||||
return (
|
||||
<div className="w-full max-w-[1250px] mx-auto">
|
||||
<div className="pl-4 py-5 sticky">
|
||||
<h2 className="text-3xl font-bold text-center py-4">
|
||||
Часто задаваемые вопросы:
|
||||
</h2>
|
||||
{faqs.map((faq) => (
|
||||
<Accordion key={faq.id} title={faq.title} content={faq.content} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FAQ
|
||||
255
frontend/components/Footer.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
FaInstagram,
|
||||
FaTelegram,
|
||||
FaVk,
|
||||
FaFacebook,
|
||||
FaYoutube,
|
||||
FaTiktok,
|
||||
} from 'react-icons/fa'
|
||||
import EmailHandler from './EmailHandler'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-[#272424]">
|
||||
<div className="flex flex-col md:flex-row px-6 py-8 w-[93%] mx-auto gap-8 md:gap-16">
|
||||
{/* левый таб */}
|
||||
<div className="flex flex-col space-y-5 w-full md:w-1/4 items-center md:items-start text-center md:text-left">
|
||||
<Image
|
||||
src="/images/footerLogo.png"
|
||||
alt="logo"
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<span className="text-sm text-gray-300">
|
||||
Подпишись и будь в курсе всех событий, а также получай подарки и
|
||||
бонусы от Trip With Bonus
|
||||
</span>
|
||||
<EmailHandler />
|
||||
<div className="flex gap-4 justify-center md:justify-start">
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaInstagram
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://telegram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaTelegram
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://vk.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaVk
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaFacebook
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://tiktok.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaTiktok
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://youtube.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaYoutube
|
||||
size={20}
|
||||
className="text-white hover:text-orange transition-colors"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ссылки */}
|
||||
<div className="flex flex-col md:flex-row justify-between w-full md:w-2/4 space-y-8 md:space-y-0 items-center md:items-start text-center md:text-left">
|
||||
{/* информация */}
|
||||
<div className="flex flex-col space-y-4 items-center md:items-start">
|
||||
<div className="text-white text-xl font-medium md:pb-5">
|
||||
Информация
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Перевезти посылку
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Отправить посылку
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Для отправителя
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Для перевозчика
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* О Trip With Bonus */}
|
||||
<div className="flex flex-col space-y-4 items-center md:items-start">
|
||||
<div className="text-white text-xl font-medium md:pb-5">
|
||||
О Trip With Bonus
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Новости
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Партнерам
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Реклама
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 items-center md:items-start">
|
||||
<div className="text-white text-xl font-medium md:pb-5">
|
||||
Кооперация
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Реклама
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Поддержка
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-orange transition-colors"
|
||||
>
|
||||
Контакты
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4 w-full md:w-1/4 items-center md:items-start text-center md:text-left">
|
||||
<div className="text-white text-xl font-medium md:pb-5">
|
||||
Свяжитесь с нами:
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<a
|
||||
href="tel:+375291234567"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity text-gray-300 hover:text-orange"
|
||||
>
|
||||
+ 7 (777) 777-77-77{' '}
|
||||
</a>
|
||||
<a
|
||||
href="mailto:sales@tripwb.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity text-gray-300 hover:text-orange"
|
||||
>
|
||||
sales@tripwb.com
|
||||
</a>
|
||||
<a
|
||||
href="mailto:support@tripwb.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity text-gray-300 hover:text-orange"
|
||||
>
|
||||
support@tripwb.com
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-2 mt-7 justify-center md:justify-start">
|
||||
<Link
|
||||
href="/register"
|
||||
className="bg-orange hover:bg-orange/80 text-white px-4 py-2 rounded-2xl text-base font-medium"
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-white hover:text-orange transition-colors py-2"
|
||||
>
|
||||
Войти
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Нижняя часть футера */}
|
||||
<div>
|
||||
<div className="flex flex-col justify-between md:flex-row px-6 py-4 md:py-8 w-[95%] mx-auto text-center md:text-left">
|
||||
<div>Copyright © {new Date().getFullYear()}. Все права защищены.</div>
|
||||
<div className="flex flex-col md:flex-row py-4 md:py-0 md:space-x-5 items-center md:items-start">
|
||||
<Link href="/" className="hover:text-orange">
|
||||
Публичная оферта
|
||||
</Link>
|
||||
<Link href="/" className="hover:text-orange">
|
||||
Политика конфиденциальности
|
||||
</Link>
|
||||
<Link href="/" className="hover:text-orange">
|
||||
Правила пользования сервисом
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
66
frontend/components/Header.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import Burger from './ui/Burger'
|
||||
import LangSwitcher from './LangSwitcher'
|
||||
import Button from './ui/Button'
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="flex justify-between items-center px-6 py-8 w-[93%] mx-auto">
|
||||
<div className="flex items-center justify-center space-x-10">
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="logo"
|
||||
width={50}
|
||||
height={50}
|
||||
priority
|
||||
/>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<Burger />
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-base font-medium px-4 hover:text-orange transition-colors hidden md:block"
|
||||
>
|
||||
Могу взять посылку
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-10">
|
||||
<LangSwitcher />
|
||||
<Button
|
||||
text="Разместить объявление"
|
||||
className="hidden md:block bg-orange hover:bg-orange/80 px-4 py-3 text-white"
|
||||
/>
|
||||
|
||||
<div className="hidden md:block text-base font-medium">
|
||||
<Link
|
||||
href="/register"
|
||||
className="hover:text-orange transition-colors"
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
<span> / </span>
|
||||
<Link href="/login" className="hover:text-orange transition-colors">
|
||||
Войти
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 md:hidden">
|
||||
<Link href="/login">
|
||||
<Image
|
||||
src="/images/userlogo.png"
|
||||
alt="user"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Link>
|
||||
<Burger />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
31
frontend/components/LangSwitcher.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const LangSwitcher = () => {
|
||||
const [selectedLang, setSelectedLang] = useState('ru')
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setSelectedLang('ru')}
|
||||
className={`${
|
||||
selectedLang === 'ru' ? 'text-orange' : 'text-gray-500'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
RU
|
||||
</button>
|
||||
<span className="text-gray-500">/</span>
|
||||
<button
|
||||
onClick={() => setSelectedLang('en')}
|
||||
className={`${
|
||||
selectedLang === 'en' ? 'text-orange' : 'text-gray-500'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LangSwitcher
|
||||
44
frontend/components/News.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { news } from '@/app/staticData'
|
||||
import ShowMore from './ui/ShowMore'
|
||||
|
||||
interface NewsItem {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
image: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export default function News() {
|
||||
return (
|
||||
<div className="w-full max-w-[1250px] mx-auto px-4 sm:px-6 mb-20">
|
||||
<h2 className="text-3xl sm:text-4xl text-center font-bold mb-10">
|
||||
Последние новости
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{news.map((item) => (
|
||||
<Link href={`/news/${item.slug}`} key={item.id}>
|
||||
<div className="flex flex-col bg-white rounded-2xl shadow-md overflow-hidden hover:shadow-2xl transition-shadow duration-500 p-6">
|
||||
<div className="relative h-[200px]">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<h3 className="text-base font-semibold mb-3">{item.title}</h3>
|
||||
<ShowMore text={item.description} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
75
frontend/components/ui/Accordion.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { IoIosAdd, IoIosClose } from 'react-icons/io'
|
||||
import { AccordionProps } from '@/app/types'
|
||||
|
||||
const Accordion: React.FC<AccordionProps> = ({ title, content }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [contentHeight, setContentHeight] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setContentHeight(contentRef.current.scrollHeight)
|
||||
}
|
||||
}, [content])
|
||||
|
||||
const renderContent = () => {
|
||||
if (typeof content === 'string') {
|
||||
return (
|
||||
<div
|
||||
className="text-md font-medium [&>ol]:list-decimal [&>ol]:pl-4 [&_ul]:list-disc [&_ul]:py-2 [&_ul]:pl-4 [&_li]:mb-2 [&_p]:mt-1 [&_a]:text-orange [&_a]:hover:underline"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className="text-md font-medium">{content}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative bg-white rounded-2xl my-4">
|
||||
<button
|
||||
className="flex justify-between items-center py-4 px-6 hover:bg-gray-50 rounded-2xl space-x-9 w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<h3 className="text-xl font-bold text-left">{title}</h3>
|
||||
<div className="w-6 h-6 flex items-center justify-center rounded-full border border-orange">
|
||||
<div
|
||||
className={`transition-all duration-500 ${
|
||||
isOpen ? 'rotate-180' : 'rotate-0'
|
||||
}`}
|
||||
>
|
||||
{isOpen ? (
|
||||
<IoIosClose className="w-5 h-5 text-orange" />
|
||||
) : (
|
||||
<IoIosAdd className="w-5 h-5 text-orange" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={
|
||||
{ '--accordion-height': `${contentHeight}px` } as React.CSSProperties
|
||||
}
|
||||
className={`
|
||||
grid
|
||||
transition-all duration-500 ease-[cubic-bezier(0.4,0,0.2,1)]
|
||||
${
|
||||
isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className=" rounded-xl px-6 mt-2">
|
||||
<div className="py-4">{renderContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Accordion
|
||||
82
frontend/components/ui/Burger.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Button from './Button'
|
||||
const Burger = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="relative z-50 w-8 h-8 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div className="relative w-8 h-8">
|
||||
<span
|
||||
className={`absolute h-0.5 bg-orange transition-all duration-300 ease-in-out ${
|
||||
isOpen ? 'w-8 rotate-45 top-4' : 'w-8 top-2'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`absolute h-0.5 bg-orange transition-all duration-300 ease-in-out ${
|
||||
isOpen ? 'w-0 opacity-0 top-4' : 'w-8 top-4'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`absolute h-0.5 bg-orange transition-all duration-300 ease-in-out ${
|
||||
isOpen ? 'w-8 -rotate-45 top-4' : 'w-8 top-6'
|
||||
}`}
|
||||
></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* меню */}
|
||||
<div
|
||||
className={`fixed z-50 left-0 w-full bg-[#eeebeb] shadow-lg transition-all duration-300 ease-in-out origin-top ${
|
||||
isOpen
|
||||
? 'top-[80px] h-[calc(100vh-80px)] opacity-100 scale-y-100'
|
||||
: 'top-[80px] h-0 opacity-0 scale-y-0'
|
||||
} ${isOpen ? 'pointer-events-auto' : 'pointer-events-none'}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col items-center pt-12 space-y-8 transition-all duration-300 ${
|
||||
isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl hover:text-orange transition-colors duration-200"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Ссылка 1
|
||||
</Link>
|
||||
<Link
|
||||
href="/search"
|
||||
className="text-xl hover:text-orange transition-colors duration-200"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Ссылка 2
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-xl hover:text-orange transition-colors duration-200"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Ссылка 3
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl hover:text-orange transition-colors duration-200"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Могу взять посылку
|
||||
</Link>
|
||||
<Button text="Разместить объявление" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Burger
|
||||
16
frontend/components/ui/Button.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { ButtonProps } from '@/app/types/index'
|
||||
|
||||
const Button = ({ onClick, className, text, type }: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`text-base font-medium rounded-2xl cursor-pointer ${className}`}
|
||||
type={type}
|
||||
>
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
48
frontend/components/ui/ShowMore.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface ShowMoreProps {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const ShowMore = ({ text }: ShowMoreProps) => {
|
||||
const [isExpandedMobile, setIsExpandedMobile] = useState(false)
|
||||
const [isExpandedDesktop, setIsExpandedDesktop] = useState(false)
|
||||
|
||||
if (!text) return null
|
||||
|
||||
const maxLength = {
|
||||
mobile: 100,
|
||||
desktop: 100,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-5">
|
||||
{/* мобила */}
|
||||
<div
|
||||
onClick={() => setIsExpandedMobile(!isExpandedMobile)}
|
||||
className={`lg:hidden text-justify relative cursor-pointer overflow-hidden
|
||||
${isExpandedMobile ? 'max-h-[2000px]' : 'max-h-[300px]'}`}
|
||||
>
|
||||
{isExpandedMobile ? text : text.slice(0, maxLength.mobile)}
|
||||
{!isExpandedMobile && text.length > maxLength.mobile && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* десктоп */}
|
||||
<div
|
||||
onClick={() => setIsExpandedDesktop(!isExpandedDesktop)}
|
||||
className={`hidden lg:block text-justify relative cursor-pointer overflow-hidden
|
||||
${isExpandedDesktop ? 'max-h-[2000px]' : 'max-h-[300px]'}`}
|
||||
>
|
||||
{isExpandedDesktop ? text : text.slice(0, maxLength.desktop)}
|
||||
{!isExpandedDesktop && text.length > maxLength.desktop && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShowMore
|
||||
41
frontend/components/ui/TextInput.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { TextInputProps } from '@/app/types'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
const TextInput = ({
|
||||
value,
|
||||
handleChange,
|
||||
label,
|
||||
placeholder,
|
||||
name,
|
||||
type = 'text',
|
||||
className = '',
|
||||
maxLength,
|
||||
tooltip,
|
||||
}: TextInputProps) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<div className="flex items-center gap-2 my-2">
|
||||
<label className="font-medium text-sm text-gray-500" htmlFor={name}>
|
||||
{label}
|
||||
</label>
|
||||
{tooltip && <Tooltip content={tooltip} />}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full p-4 border border-gray-300 text-black rounded-xl focus:outline-none focus:ring-2 focus:ring-mainblocks focus:bg-white"
|
||||
autoComplete={name}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextInput
|
||||
34
frontend/components/ui/Tooltip.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { CiCircleInfo } from 'react-icons/ci'
|
||||
|
||||
interface TooltipProps {
|
||||
content: string | React.ReactNode
|
||||
}
|
||||
|
||||
const Tooltip = ({ content }: TooltipProps) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center overflow-visible">
|
||||
<button
|
||||
type="button"
|
||||
className="text-orange hover:text-orange/80 focus:outline-none"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={() => setShowTooltip(!showTooltip)}
|
||||
>
|
||||
<CiCircleInfo className="w-4 h-4" />
|
||||
</button>
|
||||
{showTooltip && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-4 py-2 bg-white rounded-xl text-center shadow-lg text-sm text-gray-700 w-max max-w-xs z-10">
|
||||
{content}
|
||||
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-2 h-2 bg-white transform rotate-45"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
16
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
18
frontend/lib/fetchFAQ.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FAQ, FAQProps } from '@/app/types'
|
||||
|
||||
export async function getFAQs(): Promise<FAQ[]> {
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
|
||||
const response = await fetch(`${API_URL}/faq/`, {
|
||||
next: {
|
||||
revalidate: 259200, // три дня
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch FAQs')
|
||||
}
|
||||
|
||||
const data: FAQProps = await response.json()
|
||||
return data.faqs
|
||||
}
|
||||
17
frontend/next.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const API_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000/api/v1'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'via.placeholder.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
6722
frontend/package-lock.json
generated
Normal file
28
frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20.17.47",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
9
frontend/public/images/advantage.svg
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
BIN
frontend/public/images/airplane.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/images/avatar.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
frontend/public/images/belarus.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/images/box1.png
Normal file
|
After Width: | Height: | Size: 529 KiB |
BIN
frontend/public/images/box2.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
frontend/public/images/car.png
Normal file
|
After Width: | Height: | Size: 890 B |
BIN
frontend/public/images/footerLogo.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
frontend/public/images/laptop.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
frontend/public/images/leftArrow.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/public/images/logo.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
9
frontend/public/images/news.svg
Normal file
|
After Width: | Height: | Size: 13 MiB |
BIN
frontend/public/images/package.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/public/images/phone.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/public/images/russia.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
frontend/public/images/subscribe.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
frontend/public/images/userlogo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
58
frontend/public/images/vector.svg
Normal file
@@ -0,0 +1,58 @@
|
||||
<svg width="521" height="4" viewBox="0 0 521 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 2.02181C0 0.92928 3.11013 0.0436119 6.94667 0.0436119H514.053C517.89 0.0436119 521 0.92928 521 2.02181C521 3.11433 517.89 4 514.053 4H6.94666C3.11012 4 0 3.11433 0 2.02181Z" fill="url(#paint0_linear_6203_10870)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.11626 3.90369C6.89024 3.83345 6.89033 3.71958 7.11645 3.64936L12.4973 1.9785L7.11898 0.306974C6.89297 0.23673 6.89305 0.122864 7.11918 0.0526475C7.3453 -0.0175688 7.71183 -0.0175463 7.93785 0.0526978L13.7254 1.85141C13.9514 1.92166 13.9513 2.03552 13.7252 2.10574L7.93493 3.90374C7.70881 3.97396 7.34228 3.97393 7.11626 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5393 3.90369C17.3132 3.83345 17.3133 3.71958 17.5394 3.64936L22.9203 1.9785L17.542 0.306974C17.316 0.23673 17.3161 0.122864 17.5422 0.0526475C17.7683 -0.0175688 18.1348 -0.0175463 18.3608 0.0526978L24.1484 1.85141C24.3744 1.92166 24.3743 2.03552 24.1482 2.10574L18.3579 3.90374C18.1318 3.97396 17.7653 3.97393 17.5393 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.9623 3.90369C27.7362 3.83345 27.7363 3.71958 27.9624 3.64936L33.3433 1.9785L27.965 0.306974C27.739 0.23673 27.739 0.122864 27.9652 0.0526475C28.1913 -0.0175688 28.5578 -0.0175463 28.7838 0.0526978L34.5714 1.85141C34.7974 1.92166 34.7973 2.03552 34.5712 2.10574L28.7809 3.90374C28.5548 3.97396 28.1883 3.97393 27.9623 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.3853 3.90369C38.1592 3.83345 38.1593 3.71958 38.3854 3.64936L43.7663 1.9785L38.388 0.306974C38.162 0.23673 38.162 0.122864 38.3882 0.0526475C38.6143 -0.0175688 38.9808 -0.0175463 39.2068 0.0526978L44.9944 1.85141C45.2204 1.92166 45.2203 2.03552 44.9942 2.10574L39.2039 3.90374C38.9778 3.97396 38.6113 3.97393 38.3853 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.8082 3.90369C48.5822 3.83345 48.5823 3.71958 48.8084 3.64936L54.1893 1.9785L48.811 0.306974C48.585 0.23673 48.585 0.122864 48.8112 0.0526475C49.0373 -0.0175688 49.4038 -0.0175463 49.6298 0.0526978L55.4174 1.85141C55.6434 1.92166 55.6433 2.03552 55.4172 2.10574L49.6269 3.90374C49.4008 3.97396 49.0343 3.97393 48.8082 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.2312 3.90369C59.0052 3.83345 59.0053 3.71958 59.2314 3.64936L64.6123 1.9785L59.234 0.306974C59.0079 0.23673 59.008 0.122864 59.2342 0.0526475C59.4603 -0.0175688 59.8268 -0.0175463 60.0528 0.0526978L65.8404 1.85141C66.0664 1.92166 66.0663 2.03552 65.8402 2.10574L60.0499 3.90374C59.8238 3.97396 59.4573 3.97393 59.2312 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.6542 3.90369C69.4282 3.83345 69.4283 3.71958 69.6544 3.64936L75.0353 1.9785L69.657 0.306974C69.431 0.23673 69.431 0.122864 69.6572 0.0526475C69.8833 -0.0175688 70.2498 -0.0175463 70.4758 0.0526978L76.2634 1.85141C76.4894 1.92166 76.4893 2.03552 76.2632 2.10574L70.4729 3.90374C70.2468 3.97396 69.8803 3.97393 69.6542 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.0772 3.90369C79.8512 3.83345 79.8513 3.71958 80.0774 3.64936L85.4583 1.9785L80.08 0.306974C79.854 0.23673 79.854 0.122864 80.0802 0.0526475C80.3063 -0.0175688 80.6728 -0.0175463 80.8988 0.0526978L86.6864 1.85141C86.9124 1.92166 86.9123 2.03552 86.6862 2.10574L80.8959 3.90374C80.6698 3.97396 80.3033 3.97393 80.0772 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.5002 3.90369C90.2742 3.83345 90.2743 3.71958 90.5004 3.64936L95.8812 1.9785L90.503 0.306974C90.2769 0.23673 90.277 0.122864 90.5032 0.0526475C90.7293 -0.0175688 91.0958 -0.0175463 91.3218 0.0526978L97.1094 1.85141C97.3354 1.92166 97.3353 2.03552 97.1092 2.10574L91.3189 3.90374C91.0928 3.97396 90.7262 3.97393 90.5002 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.923 3.90369C100.697 3.83345 100.697 3.71958 100.923 3.64936L106.304 1.9785L100.926 0.306974C100.7 0.23673 100.7 0.122864 100.926 0.0526475C101.152 -0.0175688 101.519 -0.0175463 101.745 0.0526978L107.532 1.85141C107.758 1.92166 107.758 2.03552 107.532 2.10574L101.742 3.90374C101.516 3.97396 101.149 3.97393 100.923 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M111.346 3.90369C111.12 3.83345 111.12 3.71958 111.346 3.64936L116.727 1.9785L111.349 0.306974C111.123 0.23673 111.123 0.122864 111.349 0.0526475C111.575 -0.0175688 111.942 -0.0175463 112.168 0.0526978L117.955 1.85141C118.181 1.92166 118.181 2.03552 117.955 2.10574L112.165 3.90374C111.939 3.97396 111.572 3.97393 111.346 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M121.769 3.90369C121.543 3.83345 121.543 3.71958 121.769 3.64936L127.15 1.9785L121.772 0.306974C121.546 0.23673 121.546 0.122864 121.772 0.0526475C121.998 -0.0175688 122.365 -0.0175463 122.591 0.0526978L128.378 1.85141C128.604 1.92166 128.604 2.03552 128.378 2.10574L122.588 3.90374C122.362 3.97396 121.995 3.97393 121.769 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.192 3.90369C131.966 3.83345 131.966 3.71958 132.192 3.64936L137.573 1.9785L132.195 0.306974C131.969 0.23673 131.969 0.122864 132.195 0.0526475C132.421 -0.0175688 132.788 -0.0175463 133.014 0.0526978L138.801 1.85141C139.027 1.92166 139.027 2.03552 138.801 2.10574L133.011 3.90374C132.785 3.97396 132.418 3.97393 132.192 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M142.615 3.90369C142.389 3.83345 142.389 3.71958 142.615 3.64936L147.996 1.9785L142.618 0.306974C142.392 0.23673 142.392 0.122864 142.618 0.0526475C142.844 -0.0175688 143.211 -0.0175463 143.437 0.0526978L149.224 1.85141C149.45 1.92166 149.45 2.03552 149.224 2.10574L143.434 3.90374C143.208 3.97396 142.841 3.97393 142.615 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M153.038 3.90369C152.812 3.83345 152.812 3.71958 153.038 3.64936L158.419 1.9785L153.041 0.306974C152.815 0.23673 152.815 0.122864 153.041 0.0526475C153.267 -0.0175688 153.634 -0.0175463 153.86 0.0526978L159.647 1.85141C159.873 1.92166 159.873 2.03552 159.647 2.10574L153.857 3.90374C153.631 3.97396 153.264 3.97393 153.038 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M163.461 3.90369C163.235 3.83345 163.235 3.71958 163.461 3.64936L168.842 1.9785L163.464 0.306974C163.238 0.23673 163.238 0.122864 163.464 0.0526475C163.69 -0.0175688 164.057 -0.0175463 164.283 0.0526978L170.07 1.85141C170.296 1.92166 170.296 2.03552 170.07 2.10574L164.28 3.90374C164.054 3.97396 163.687 3.97393 163.461 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M173.884 3.90369C173.658 3.83345 173.658 3.71958 173.884 3.64936L179.265 1.9785L173.887 0.306974C173.661 0.23673 173.661 0.122864 173.887 0.0526475C174.113 -0.0175688 174.48 -0.0175463 174.706 0.0526978L180.493 1.85141C180.719 1.92166 180.719 2.03552 180.493 2.10574L174.703 3.90374C174.477 3.97396 174.11 3.97393 173.884 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.307 3.90369C184.081 3.83345 184.081 3.71958 184.307 3.64936L189.688 1.9785L184.31 0.306974C184.084 0.23673 184.084 0.122864 184.31 0.0526475C184.536 -0.0175688 184.903 -0.0175463 185.129 0.0526978L190.916 1.85141C191.142 1.92166 191.142 2.03552 190.916 2.10574L185.126 3.90374C184.9 3.97396 184.533 3.97393 184.307 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M194.73 3.90369C194.504 3.83345 194.504 3.71958 194.73 3.64936L200.111 1.9785L194.733 0.306974C194.507 0.23673 194.507 0.122864 194.733 0.0526475C194.959 -0.0175688 195.326 -0.0175463 195.552 0.0526978L201.339 1.85141C201.565 1.92166 201.565 2.03552 201.339 2.10574L195.549 3.90374C195.323 3.97396 194.956 3.97393 194.73 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M205.153 3.90369C204.927 3.83345 204.927 3.71958 205.153 3.64936L210.534 1.9785L205.156 0.306974C204.93 0.23673 204.93 0.122864 205.156 0.0526475C205.382 -0.0175688 205.749 -0.0175463 205.975 0.0526978L211.762 1.85141C211.988 1.92166 211.988 2.03552 211.762 2.10574L205.972 3.90374C205.746 3.97396 205.379 3.97393 205.153 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M215.576 3.90369C215.35 3.83345 215.35 3.71958 215.576 3.64936L220.957 1.9785L215.579 0.306974C215.353 0.23673 215.353 0.122864 215.579 0.0526475C215.805 -0.0175688 216.172 -0.0175463 216.398 0.0526978L222.185 1.85141C222.411 1.92166 222.411 2.03552 222.185 2.10574L216.395 3.90374C216.169 3.97396 215.802 3.97393 215.576 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M225.999 3.90369C225.773 3.83345 225.773 3.71958 225.999 3.64936L231.38 1.9785L226.002 0.306974C225.776 0.23673 225.776 0.122864 226.002 0.0526475C226.228 -0.0175688 226.595 -0.0175463 226.821 0.0526978L232.608 1.85141C232.834 1.92166 232.834 2.03552 232.608 2.10574L226.818 3.90374C226.592 3.97396 226.225 3.97393 225.999 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.422 3.90369C236.196 3.83345 236.196 3.71958 236.422 3.64936L241.803 1.9785L236.425 0.306974C236.199 0.23673 236.199 0.122864 236.425 0.0526475C236.651 -0.0175688 237.018 -0.0175463 237.244 0.0526978L243.031 1.85141C243.257 1.92166 243.257 2.03552 243.031 2.10574L237.241 3.90374C237.015 3.97396 236.648 3.97393 236.422 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M246.845 3.90369C246.619 3.83345 246.619 3.71958 246.845 3.64936L252.226 1.9785L246.848 0.306974C246.622 0.23673 246.622 0.122864 246.848 0.0526475C247.074 -0.0175688 247.441 -0.0175463 247.667 0.0526978L253.454 1.85141C253.68 1.92166 253.68 2.03552 253.454 2.10574L247.664 3.90374C247.438 3.97396 247.071 3.97393 246.845 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M257.268 3.90369C257.042 3.83345 257.042 3.71958 257.268 3.64936L262.649 1.9785L257.271 0.306974C257.045 0.23673 257.045 0.122864 257.271 0.0526475C257.497 -0.0175688 257.864 -0.0175463 258.09 0.0526978L263.877 1.85141C264.103 1.92166 264.103 2.03552 263.877 2.10574L258.087 3.90374C257.861 3.97396 257.494 3.97393 257.268 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M267.691 3.90369C267.465 3.83345 267.465 3.71958 267.691 3.64936L273.072 1.9785L267.694 0.306974C267.468 0.23673 267.468 0.122864 267.694 0.0526475C267.92 -0.0175688 268.287 -0.0175463 268.513 0.0526978L274.3 1.85141C274.526 1.92166 274.526 2.03552 274.3 2.10574L268.51 3.90374C268.284 3.97396 267.917 3.97393 267.691 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M278.114 3.90369C277.888 3.83345 277.888 3.71958 278.114 3.64936L283.495 1.9785L278.117 0.306974C277.891 0.23673 277.891 0.122864 278.117 0.0526475C278.343 -0.0175688 278.71 -0.0175463 278.936 0.0526978L284.723 1.85141C284.949 1.92166 284.949 2.03552 284.723 2.10574L278.933 3.90374C278.707 3.97396 278.34 3.97393 278.114 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.537 3.90369C288.311 3.83345 288.311 3.71958 288.537 3.64936L293.918 1.9785L288.54 0.306974C288.314 0.23673 288.314 0.122864 288.54 0.0526475C288.766 -0.0175688 289.133 -0.0175463 289.359 0.0526978L295.146 1.85141C295.372 1.92166 295.372 2.03552 295.146 2.10574L289.356 3.90374C289.13 3.97396 288.763 3.97393 288.537 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M298.96 3.90369C298.734 3.83345 298.734 3.71958 298.96 3.64936L304.341 1.9785L298.963 0.306974C298.737 0.23673 298.737 0.122864 298.963 0.0526475C299.189 -0.0175688 299.556 -0.0175463 299.782 0.0526978L305.569 1.85141C305.795 1.92166 305.795 2.03552 305.569 2.10574L299.779 3.90374C299.553 3.97396 299.186 3.97393 298.96 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M309.383 3.90369C309.157 3.83345 309.157 3.71958 309.383 3.64936L314.764 1.9785L309.386 0.306974C309.16 0.23673 309.16 0.122864 309.386 0.0526475C309.612 -0.0175688 309.979 -0.0175463 310.205 0.0526978L315.992 1.85141C316.218 1.92166 316.218 2.03552 315.992 2.10574L310.202 3.90374C309.976 3.97396 309.609 3.97393 309.383 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M319.806 3.90369C319.58 3.83345 319.58 3.71958 319.806 3.64936L325.187 1.9785L319.809 0.306974C319.583 0.23673 319.583 0.122864 319.809 0.0526475C320.035 -0.0175688 320.402 -0.0175463 320.628 0.0526978L326.415 1.85141C326.641 1.92166 326.641 2.03552 326.415 2.10574L320.625 3.90374C320.399 3.97396 320.032 3.97393 319.806 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M330.229 3.90369C330.003 3.83345 330.003 3.71958 330.229 3.64936L335.61 1.9785L330.232 0.306974C330.006 0.23673 330.006 0.122864 330.232 0.0526475C330.458 -0.0175688 330.825 -0.0175463 331.051 0.0526978L336.838 1.85141C337.064 1.92166 337.064 2.03552 336.838 2.10574L331.048 3.90374C330.822 3.97396 330.455 3.97393 330.229 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M340.652 3.90369C340.426 3.83345 340.426 3.71958 340.652 3.64936L346.033 1.9785L340.655 0.306974C340.429 0.23673 340.429 0.122864 340.655 0.0526475C340.881 -0.0175688 341.248 -0.0175463 341.474 0.0526978L347.261 1.85141C347.487 1.92166 347.487 2.03552 347.261 2.10574L341.471 3.90374C341.245 3.97396 340.878 3.97393 340.652 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M351.075 3.90369C350.849 3.83345 350.849 3.71958 351.075 3.64936L356.456 1.9785L351.078 0.306974C350.852 0.23673 350.852 0.122864 351.078 0.0526475C351.304 -0.0175688 351.671 -0.0175463 351.897 0.0526978L357.684 1.85141C357.91 1.92166 357.91 2.03552 357.684 2.10574L351.894 3.90374C351.668 3.97396 351.301 3.97393 351.075 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M361.498 3.90369C361.272 3.83345 361.272 3.71958 361.498 3.64936L366.879 1.9785L361.501 0.306974C361.275 0.23673 361.275 0.122864 361.501 0.0526475C361.727 -0.0175688 362.094 -0.0175463 362.32 0.0526978L368.107 1.85141C368.333 1.92166 368.333 2.03552 368.107 2.10574L362.317 3.90374C362.091 3.97396 361.724 3.97393 361.498 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M371.921 3.90369C371.695 3.83345 371.695 3.71958 371.921 3.64936L377.302 1.9785L371.924 0.306974C371.698 0.23673 371.698 0.122864 371.924 0.0526475C372.15 -0.0175688 372.517 -0.0175463 372.743 0.0526978L378.53 1.85141C378.756 1.92166 378.756 2.03552 378.53 2.10574L372.74 3.90374C372.514 3.97396 372.147 3.97393 371.921 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M382.344 3.90369C382.118 3.83345 382.118 3.71958 382.344 3.64936L387.725 1.9785L382.347 0.306974C382.121 0.23673 382.121 0.122864 382.347 0.0526475C382.573 -0.0175688 382.94 -0.0175463 383.166 0.0526978L388.953 1.85141C389.179 1.92166 389.179 2.03552 388.953 2.10574L383.163 3.90374C382.937 3.97396 382.57 3.97393 382.344 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M392.767 3.90369C392.541 3.83345 392.541 3.71958 392.767 3.64936L398.148 1.9785L392.77 0.306974C392.544 0.23673 392.544 0.122864 392.77 0.0526475C392.996 -0.0175688 393.363 -0.0175463 393.589 0.0526978L399.376 1.85141C399.602 1.92166 399.602 2.03552 399.376 2.10574L393.586 3.90374C393.36 3.97396 392.993 3.97393 392.767 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M403.19 3.90369C402.964 3.83345 402.964 3.71958 403.19 3.64936L408.571 1.9785L403.193 0.306974C402.967 0.23673 402.967 0.122864 403.193 0.0526475C403.419 -0.0175688 403.786 -0.0175463 404.012 0.0526978L409.799 1.85141C410.025 1.92166 410.025 2.03552 409.799 2.10574L404.009 3.90374C403.783 3.97396 403.416 3.97393 403.19 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M413.613 3.90369C413.387 3.83345 413.387 3.71958 413.613 3.64936L418.994 1.9785L413.616 0.306974C413.39 0.23673 413.39 0.122864 413.616 0.0526475C413.842 -0.0175688 414.209 -0.0175463 414.435 0.0526978L420.222 1.85141C420.448 1.92166 420.448 2.03552 420.222 2.10574L414.432 3.90374C414.206 3.97396 413.839 3.97393 413.613 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M424.036 3.90369C423.81 3.83345 423.81 3.71958 424.036 3.64936L429.417 1.9785L424.039 0.306974C423.813 0.23673 423.813 0.122864 424.039 0.0526475C424.265 -0.0175688 424.632 -0.0175463 424.858 0.0526978L430.645 1.85141C430.871 1.92166 430.871 2.03552 430.645 2.10574L424.855 3.90374C424.629 3.97396 424.262 3.97393 424.036 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M434.459 3.90369C434.233 3.83345 434.233 3.71958 434.459 3.64936L439.84 1.9785L434.462 0.306974C434.236 0.23673 434.236 0.122864 434.462 0.0526475C434.688 -0.0175688 435.055 -0.0175463 435.281 0.0526978L441.068 1.85141C441.294 1.92166 441.294 2.03552 441.068 2.10574L435.278 3.90374C435.052 3.97396 434.685 3.97393 434.459 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M444.882 3.90369C444.656 3.83345 444.656 3.71958 444.882 3.64936L450.263 1.9785L444.885 0.306974C444.659 0.23673 444.659 0.122864 444.885 0.0526475C445.111 -0.0175688 445.478 -0.0175463 445.704 0.0526978L451.491 1.85141C451.717 1.92166 451.717 2.03552 451.491 2.10574L445.701 3.90374C445.475 3.97396 445.108 3.97393 444.882 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M455.305 3.90369C455.079 3.83345 455.079 3.71958 455.305 3.64936L460.686 1.9785L455.308 0.306974C455.082 0.23673 455.082 0.122864 455.308 0.0526475C455.534 -0.0175688 455.901 -0.0175463 456.127 0.0526978L461.914 1.85141C462.14 1.92166 462.14 2.03552 461.914 2.10574L456.124 3.90374C455.898 3.97396 455.531 3.97393 455.305 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M465.728 3.90369C465.502 3.83345 465.502 3.71958 465.728 3.64936L471.109 1.9785L465.731 0.306974C465.505 0.23673 465.505 0.122864 465.731 0.0526475C465.957 -0.0175688 466.324 -0.0175463 466.55 0.0526978L472.337 1.85141C472.563 1.92166 472.563 2.03552 472.337 2.10574L466.547 3.90374C466.321 3.97396 465.954 3.97393 465.728 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M476.151 3.90369C475.925 3.83345 475.925 3.71958 476.151 3.64936L481.532 1.9785L476.154 0.306974C475.928 0.23673 475.928 0.122864 476.154 0.0526475C476.38 -0.0175688 476.747 -0.0175463 476.973 0.0526978L482.76 1.85141C482.986 1.92166 482.986 2.03552 482.76 2.10574L476.97 3.90374C476.744 3.97396 476.377 3.97393 476.151 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M486.574 3.90369C486.348 3.83345 486.348 3.71958 486.574 3.64936L491.955 1.9785L486.577 0.306974C486.351 0.23673 486.351 0.122864 486.577 0.0526475C486.803 -0.0175688 487.17 -0.0175463 487.396 0.0526978L493.183 1.85141C493.409 1.92166 493.409 2.03552 493.183 2.10574L487.393 3.90374C487.167 3.97396 486.8 3.97393 486.574 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M496.997 3.90369C496.771 3.83345 496.771 3.71958 496.997 3.64936L502.378 1.9785L497 0.306974C496.774 0.23673 496.774 0.122864 497 0.0526475C497.226 -0.0175688 497.593 -0.0175463 497.819 0.0526978L503.606 1.85141C503.832 1.92166 503.832 2.03552 503.606 2.10574L497.816 3.90374C497.59 3.97396 497.223 3.97393 496.997 3.90369Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M507.42 3.90369C507.194 3.83345 507.194 3.71958 507.42 3.64936L512.801 1.9785L507.423 0.306974C507.197 0.23673 507.197 0.122864 507.423 0.0526475C507.649 -0.0175688 508.016 -0.0175463 508.242 0.0526978L514.029 1.85141C514.255 1.92166 514.255 2.03552 514.029 2.10574L508.239 3.90374C508.013 3.97396 507.646 3.97393 507.42 3.90369Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6203_10870" x1="0" y1="2" x2="521" y2="2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#065BFF"/>
|
||||
<stop offset="1" stop-color="#45C226"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/public/images/vectormob.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
27
frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||