[init] Initial commit

This commit is contained in:
ITQ
2024-03-31 21:02:53 +03:00
commit 04d4e83d1f
105 changed files with 8559 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
name: Django CI/CD
on: [push, pull_request]
jobs:
checking:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Copy env file
run: cp backend/template.env backend/.env
- name: Install production dependencies
run: pip install -r backend/requirements/prod.txt
- name: Check for pending migrations
run: cd backend/project && python manage.py makemigrations --check --dry-run
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install linting dependencies
run: pip install -r backend/requirements/dev.txt
- name: Lint with ruff
run: cd backend && ruff check .
testing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.x
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Copy env file
run: cp backend/template.env backend/.env
- name: Install prod dependencies
run: pip install -r backend/requirements/prod.txt
- name: Test production environment
run: cd backend/project && DJANGO_DEBUG=False python manage.py test
- name: Install dev dependencies
run: pip install -r backend/requirements/dev.txt
- name: Test development environment
run: cd backend/project && DJANGO_DEBUG=True python manage.py test
+1
View File
@@ -0,0 +1 @@
# SkillHub
+3
View File
@@ -0,0 +1,3 @@
# Folders
venv/
__pycache__/
+164
View File
@@ -0,0 +1,164 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
*.sqlite3
*.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Django stuff
cache
media
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements/prod.txt .
RUN pip install --no-cache-dir -r prod.txt
COPY . .
+37
View File
@@ -0,0 +1,37 @@
dump:
@cd project && python -Xutf8 manage.py dumpdata users --format json --indent 4 -o fixtures/users.json
load:
@cd project && python -Xutf8 manage.py loaddata fixtures/users.json
mig:
@cd project && python manage.py makemigrations
@cd project && python manage.py migrate
check: test
@ruff check
test:
@cd project && python manage.py test
run:
@cd project && python manage.py runserver
su:
@cd project && python manage.py createsuperuser
loc-m:
@cd project && django-admin makemessages -l ru -l en
loc-c:
@cd project && django-admin compilemessages
help:
@cd project && python manage.py help
fix:
ruff check --fix
sort-requirements requirements/prod.txt requirements/test.txt requirements/dev.txt
req:
@pip install -r requirements/dev.txt
+1
View File
@@ -0,0 +1 @@
# SkillHub Backend folder
View File
+7
View File
@@ -0,0 +1,7 @@
import os
import django.core.asgi
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = django.core.asgi.get_asgi_application()
+153
View File
@@ -0,0 +1,153 @@
import pathlib
import sys
import environs
BASE_DIR = pathlib.Path(__file__).resolve().parent.parent
env = environs.Env()
env.read_env(BASE_DIR.parent / ".env")
DEFAULT_HOSTS = ["127.0.0.1", "localhost"]
with env.prefixed("DJANGO_"):
SECRET_KEY = env("SECRET_KEY", "secret_key")
DEBUG = env.bool("DEBUG", True)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", DEFAULT_HOSTS)
INTERNAL_IPS = env.list("INTERNAL_IPS", ALLOWED_HOSTS)
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", [])
DB_NAME = env("DB_NAME", "db.sqlite3")
with env.prefixed("POSTGRES_"):
if not DEBUG:
POSTGRES_DB = env("DB", "postgres")
POSTGRES_USER = env("USER", "postgres")
POSTGRES_PASSWORD = env("PASSWORD", "postgres")
POSTGRES_HOST = env("HOST")
POSTGRES_PORT = env("PORT", "5432")
TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
MIGRATING = len(sys.argv) > 1 and (
"migrate" in sys.argv[1] or "makemigrations" in sys.argv[1]
)
def register_debug_toolbar():
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
INSTALLED_APPS = [
# django apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third party apps
"rest_framework",
"rest_framework_simplejwt",
"drf_yasg",
# project apps
"users.apps.UsersConfig",
"notifications.apps.NotificationsConfig",
]
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 = "config.urls"
TEMPLATES_DIR = BASE_DIR / "templates"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [TEMPLATES_DIR],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
if DEBUG:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / DB_NAME,
},
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": POSTGRES_DB,
"USER": POSTGRES_USER,
"PASSWORD": POSTGRES_PASSWORD,
"HOST": POSTGRES_HOST,
"PORT": POSTGRES_PORT,
},
}
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"
),
},
]
LANGUAGE_CODE = "ru-ru"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "users.User"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
if DEBUG and not (TESTING or MIGRATING):
register_debug_toolbar()
+68
View File
@@ -0,0 +1,68 @@
import django.conf
import django.contrib.admin
import django.urls
import rest_framework_simplejwt.views
import users.views
from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions, routers
from users.views import UserViewSet
router = routers.DefaultRouter()
router.register("users", UserViewSet)
schema_view = get_schema_view(
openapi.Info(title="SkillHub API", default_version="v1"),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path(
"swagger<format>/",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path(
"redoc/",
schema_view.with_ui("redoc", cache_timeout=0),
name="schema-redoc",
),
path("api/", include(router.urls)),
path("api/registration/", users.views.RegisterView.as_view()),
django.urls.path(
"api/token/",
rest_framework_simplejwt.views.TokenObtainPairView.as_view(),
name="token_obtain_pair",
),
django.urls.path(
"api/token/refresh/",
rest_framework_simplejwt.views.TokenRefreshView.as_view(),
name="token_refresh",
),
django.urls.path(
"api/token/verify/",
rest_framework_simplejwt.views.TokenVerifyView.as_view(),
name="token_verify",
),
django.urls.path("admin/", django.contrib.admin.site.urls),
]
if django.conf.settings.DEBUG and not (
django.conf.settings.TESTING or django.conf.settings.MIGRATING
):
import debug_toolbar
urlpatterns.append(
django.urls.path(
"__debug__/",
django.urls.include(debug_toolbar.urls),
),
)
+7
View File
@@ -0,0 +1,7 @@
import os
import django.core.wsgi
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = django.core.wsgi.get_wsgi_application()
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
msg = (
"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?"
)
raise ImportError(msg) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
+32
View File
@@ -0,0 +1,32 @@
from django.contrib import admin
from notifications import models
from notifications.forms import (
CreateNotificationAdminForm,
EditNotificationAdminForm,
)
class NotificationAdmin(admin.ModelAdmin):
form = EditNotificationAdminForm
add_form = CreateNotificationAdminForm
list_display = [
models.Notification.title.field.name,
models.Notification.user.field.name,
models.Notification.content.field.name,
models.Notification.read.field.name,
models.Notification.created_at.field.name,
]
def get_readonly_fields(self, request, obj=None): # noqa: ARG002
if obj:
return (
*self.readonly_fields,
models.Notification.read.field.name,
models.Notification.created_at.field.name,
)
return self.readonly_fields
admin.site.register(models.Notification, NotificationAdmin)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "notifications"
+23
View File
@@ -0,0 +1,23 @@
from django import forms
from notifications.models import Notification
class EditNotificationAdminForm(forms.ModelForm):
class Meta:
model = Notification
fields = (
model.user.field.name,
model.title.field.name,
model.content.field.name,
)
class CreateNotificationAdminForm(forms.ModelForm):
class Meta:
model = Notification
fields = (
model.user.field.name,
model.title.field.name,
model.content.field.name,
)
@@ -0,0 +1,56 @@
# Generated by Django 4.2.11 on 2024-03-31 15:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=150, verbose_name="заголовок")),
("content", models.TextField(null=True, verbose_name="содержание")),
(
"read",
models.BooleanField(default=False, verbose_name="дата создания"),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="дата создания"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
verbose_name="пользователь",
),
),
],
options={
"verbose_name": "уведомление",
"verbose_name_plural": "уведомления",
},
),
]
+44
View File
@@ -0,0 +1,44 @@
from django.conf import settings
from django.db import models
class NotificationManager(models.Manager):
def by_user(self, user_id):
return self.get_queryset().filter(
user__id=user_id,
)
class Notification(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notifications",
verbose_name="пользователь",
)
title = models.CharField(
max_length=150,
verbose_name="заголовок",
null=False,
)
content = models.TextField(
verbose_name="содержание",
null=False,
)
read = models.BooleanField(
"дата создания",
default=False,
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="дата создания",
)
objects = NotificationManager()
class Meta:
verbose_name = "уведомление"
verbose_name_plural = "уведомления"
def __str__(self):
return self.title
@@ -0,0 +1,9 @@
from rest_framework import serializers
from notifications.models import Notification
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = ["title", "content", "read", "created_at"]
+1
View File
@@ -0,0 +1 @@
# Create your tests here.
+14
View File
@@ -0,0 +1,14 @@
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from notifications.models import Notification
from notifications.serializers import NotificationSerializer
class UserNotificationsAPIView(generics.ListAPIView):
serializer_class = NotificationSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return Notification.objects.by_user(user.id)
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TeamsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "teams"
+96
View File
@@ -0,0 +1,96 @@
import users.models
from django.core import validators
from django.db import models
class Vacancy(models.Model):
name = models.CharField(
max_length=255,
verbose_name="название вакансии",
)
start_date = models.DateField(
verbose_name="дата начала диапазона возраста участников",
blank=True,
null=True,
)
end_date = models.DateField(
verbose_name="дата конец диапазона возраста участников",
blank=True,
null=True,
)
specialization = models.ForeignKey(
users.models.Specialization,
on_delete=models.CASCADE,
blank=True,
verbose_name="специализация",
null=True,
)
skills = models.ManyToManyField(
users.models.Skill,
blank=True,
verbose_name="Технологии",
)
def __str__(self):
return self.name
class Team(models.Model):
description = models.TextField(
verbose_name="описание команды",
)
name = models.CharField(
verbose_name="название команды",
max_length=255,
)
members = models.ManyToManyField(
users.models.User,
blank=True,
unique=True,
verbose_name="участники",
)
vacancies = models.ManyToManyField(
Vacancy,
blank=True,
unique=True,
verbose_name="вакансии",
)
avatar = models.ImageField(
upload_to="teams_avatars",
blank=True,
verbose_name="аватарка",
)
count_of_members = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxLengthValidator(5),
],
verbose_name="количество участников",
null=True,
)
country = models.CharField(
blank=True,
max_length=255,
verbose_name="страна",
)
city = models.CharField(
blank=True,
max_length=255,
verbose_name="город",
)
author = models.ForeignKey(
users.models.User,
on_delete=models.CASCADE,
)
def __str__(self):
return self.name
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import serializers
from teams.models import Team
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["id", "name", "description"]
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from .views import AddUserToTeam
urlpatterns = [
path(
"add_user_to_team/<int:team_id>/<int:user_id>/",
AddUserToTeam.as_view(),
name="add_user_to_team",
),
]
+27
View File
@@ -0,0 +1,27 @@
from backend.project.users.models import User
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from teams.models import Team
from .serializers import TeamSerializer
class AddUserToTeam(APIView):
def post(self, request, team_id, user_id): # noqa: ARG002
try:
team = Team.objects.get(id=team_id)
user = User.objects.get(id=user_id)
except Team.DoesNotExist:
return Response(
{"error": "Team not found"}, status=status.HTTP_404_NOT_FOUND
)
except User.DoesNotExist:
return Response(
{"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
team.members.add(user)
team_serializer = TeamSerializer(team)
return Response(team_serializer.data, status=status.HTTP_200_OK)
View File
+5
View File
@@ -0,0 +1,5 @@
from django.contrib import admin
from users.models import User
admin.site.register(User)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
@@ -0,0 +1,97 @@
# Generated by Django 5.0.3 on 2024-03-31 12:22
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Achievements',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Название достижения')),
('info', models.TextField(max_length=255, verbose_name='Информация про достижение')),
('file', models.FileField(upload_to='achievements', verbose_name='Файл достижения')),
],
),
migrations.CreateModel(
name='Skill',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('level', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)], verbose_name='уровень навыка')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Specialization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='электронная почта')),
('birthday', models.DateField(blank=True, help_text='Введите дату рождения', null=True, verbose_name='дата рождения')),
('avatar', models.ImageField(blank=True, upload_to='avatars', verbose_name='Аватарка')),
('country', models.CharField(blank=True, max_length=255, verbose_name='страна')),
('city', models.CharField(blank=True, max_length=255, verbose_name='город')),
('bio', models.TextField(blank=True, validators=[django.core.validators.MaxLengthValidator(512)], verbose_name='обо мне')),
('experience', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MinValueValidator(100)], verbose_name='опыт работы')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
('achievements', models.ManyToManyField(blank=True, to='users.achievements', verbose_name='достижения')),
('technologies', models.ManyToManyField(blank=True, to='users.skill', verbose_name='технологии')),
('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.specialization', verbose_name='специализация')),
('tag', models.ManyToManyField(blank=True, to='users.tag', verbose_name='теги')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
'swappable': 'AUTH_USER_MODEL',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2024-03-31 13:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='technologies',
new_name='skills',
),
]
+128
View File
@@ -0,0 +1,128 @@
from django.contrib.auth.models import AbstractUser
from django.core import validators
from django.db import models
class AbstractTag(models.Model):
name = models.CharField(
max_length=255,
unique=True,
)
class Meta:
abstract = True
def __str__(self):
return self.name
class Tag(AbstractTag):
pass
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxValueValidator(10),
],
verbose_name="уровень навыка",
)
class Specialization(AbstractTag):
pass
class Achievements(models.Model):
name = models.CharField(
max_length=255,
verbose_name="Название достижения",
)
info = models.TextField(
max_length=255,
verbose_name="Информация про достижение",
)
file = models.FileField(
upload_to="achievements",
verbose_name="Файл достижения",
)
def __str__(self):
return self.name
class User(AbstractUser):
email = models.EmailField("электронная почта", unique=True)
birthday = models.DateField(
verbose_name="дата рождения",
help_text="Введите дату рождения",
blank=True,
null=True,
)
avatar = models.ImageField(
upload_to="avatars",
blank=True,
verbose_name="Аватарка",
)
country = models.CharField(
blank=True,
max_length=255,
verbose_name="страна",
)
city = models.CharField(
blank=True,
max_length=255,
verbose_name="город",
)
bio = models.TextField(
blank=True,
validators=[
validators.MaxLengthValidator(
512,
),
],
verbose_name="обо мне",
)
skills = models.ManyToManyField(
Skill,
blank=True,
verbose_name="технологии",
)
tag = models.ManyToManyField(
Tag,
blank=True,
verbose_name="теги",
)
experience = models.IntegerField(
validators=[
validators.MinValueValidator(0),
validators.MinValueValidator(100),
],
verbose_name="опыт работы",
null=True,
)
achievements = models.ManyToManyField(
Achievements,
blank=True,
verbose_name="достижения",
)
specialization = models.ForeignKey(
Specialization,
on_delete=models.CASCADE,
blank=True,
verbose_name="специализация",
null=True,
)
+26
View File
@@ -0,0 +1,26 @@
import rest_framework.serializers
import users.models
class UserSerializer(rest_framework.serializers.ModelSerializer):
class Meta:
model = users.models.User
fields = (
"email",
"birthday",
"country",
"city",
"bio",
"avatar",
"password",
"first_name",
"last_name",
"tag",
"specialization",
"experience",
"achievements",
"username",
)
extra_kwargs = {"password": {"write_only": True}}
+47
View File
@@ -0,0 +1,47 @@
import rest_framework.generics
import rest_framework.permissions
import rest_framework.response
import rest_framework.viewsets
import users.models
import users.serializers
class UserViewSet(rest_framework.viewsets.ModelViewSet):
http_method_names = ("get",)
queryset = users.models.User.objects.all()
serializer_class = users.serializers.UserSerializer
permission_classes = [rest_framework.permissions.IsAuthenticated]
class RegisterView(rest_framework.generics.CreateAPIView):
http_method_names = ("post",)
serializer_class = users.serializers.UserSerializer
def post(self, request):
if users.models.User.objects.filter(
username=request.data.get("username"),
).exists():
return rest_framework.response.Response(
{
"username": [
"пользователь с таким именем уже существует.",
],
},
status=rest_framework.status.HTTP_409_CONFLICT,
)
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
serializer.save()
return rest_framework.response.Response(
serializer.data,
status=rest_framework.status.HTTP_201_CREATED,
)
return rest_framework.response.Response(
serializer.errors,
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
+14
View File
@@ -0,0 +1,14 @@
[tool.ruff]
line-length = 79
indent-width = 4
exclude = ["migrations", "venv", ".venv"]
[tool.ruff.lint]
select = ["ALL"]
ignore = ["D", "ANN", "EXE002", "RUF012", "RUF001", "COM812", "ISC001"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
+6
View File
@@ -0,0 +1,6 @@
black
sort-requirements
ruff==0.3.4
-r prod.txt
-r test.txt
+10
View File
@@ -0,0 +1,10 @@
django==4.2.11
environs==11.0.0
gunicorn==21.2.0
python-dotenv==1.0.1
psycopg2-binary==2.9.9
djangorestframework==3.15.1
djangorestframework-simplejwt==5.3.1
django-filter==24.2
Pillow==10.2.0
drf-yasg==1.21.7
+1
View File
@@ -0,0 +1 @@
django-debug-toolbar==4.3.0
+18
View File
@@ -0,0 +1,18 @@
# For django app
DJANGO_SECRET_KEY = your-secret-key
DJANGO_DEBUG = False
DJANGO_ALLOWED_HOSTS = 127.0.0.1,localhost
DJANGO_INTERNAL_IPS = 127.0.0.1,localhost
DJANGO_CSRF_TRUSTED_ORIGINS = http://127.0.0.1:8000,localhost
# For docker(remove if you want to keep defaults)
POSTGRES_PORT = <port_to_be_forwared> # default: 5432
POSTGRES_DB = <db_name> # default: postgres
POSTGRES_USER = <postgres_user> # default: postgres
POSTGRES_PASSWORD = <password> # default: postgres
PGADMIN_PORT = <port_to_be_forwared> # default: 5050
PGADMIN_EMAIL = <email> # default: admin@mail.com
PGADMIN_PASSWORD = <password> # default: admin
+86
View File
@@ -0,0 +1,86 @@
name: skillhub
services:
postgres:
image: postgres:16.2-alpine
container_name: postgres
healthcheck:
test: pg_isready -U postgres -h localhost
interval: 5s
timeout: 5s
retries: 10
environment:
POSTGRES_DB: ${POSTGRES_DB:-postgres}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
backend:
build: ./backend
container_name: backend
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
expose:
- 8000
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
POSTGRES_HOST: postgres
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-secret_key}
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-*}
DJANGO_INTERNAL_IPS: ${DJANGO_INTERNAL_IPS:-127.0.0.1}
command: ["sh", "-c", "cd project && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8080"]
ports:
- 8080:8080
frontend:
container_name: frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
restart: always
# nginx:
# container_name: nginx
# image: nginx:custom
# build: ./nginx
# restart: unless-stopped
# ports:
# - 80:80
# volumes:
# - media:/usr/src/app/media
# depends_on:
# - backend
pgadmin:
image: dpage/pgadmin4:8.4
container_name: pgadmin
depends_on:
postgres:
condition: service_healthy
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@mail.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
ports:
- "${PGADMIN_PORT:-5050}:80"
restart: always
volumes:
- pgadmin_data:/var/lib/pgadmin
volumes:
postgres_data:
redis_data:
pgadmin_data:
media:
external: true
+1
View File
@@ -0,0 +1 @@
Dockerfile
+32
View File
@@ -0,0 +1,32 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
}
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+19
View File
@@ -0,0 +1,19 @@
FROM node:lts-alpine3.19 AS builder
ENV NODE_ENV production
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine3.17-slim
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
+1
View File
@@ -0,0 +1 @@
# SkillHub Frontend folder
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "src/components/shared",
"utils": "/lib/utils"
}
}
+68
View File
@@ -0,0 +1,68 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
font-family: Arial, Helvetica, sans-serif;
}
body {
@apply bg-background text-foreground;
}
}
@font-face {
font-family: "Arial Black";
src: url(./fonts/Arial_Black.ttf) format("truetype");
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="globals.css">
<title>React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+9
View File
@@ -0,0 +1,9 @@
server {
listen 3000;
location / {
root /usr/share/nginx/html/;
include /etc/nginx/mime.types;
try_files $uri $uri/ /index.html;
}
}
+5174
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
{
"name": "skill-hub",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cn-decorator": "^2.1.0",
"i18next": "^23.10.1",
"lucide-react": "^0.363.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"react-router": "^6.22.3",
"react-router-dom": "^6.22.3",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4",
"vite": "^5.2.0",
"typescript": "^5.2.2",
"@vitejs/plugin-react-swc": "^3.5.0",
"less": "^4.2.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"autoprefixer": "^10.4.19"
},
"devDependencies": {
"@types/node": "^20.11.30",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.2.5"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

+49
View File
@@ -0,0 +1,49 @@
import TemplateWeb from "./components/app/Template/Landing/TemplateWeb";
import General from "./components/app/Template/General/General";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import Landing from "./components/pages/Landing/Landing";
import Main from "./components/pages/Main/Main";
import Teams from "./components/pages/Teams/Teams";
import MyTeams from "./components/pages/MyTeams/MyTeams";
const router = createBrowserRouter([
{
path: "*",
element: <TemplateWeb />,
children: [{
path: "",
element: <Landing />,
}]
},
{
path: "dash",
element: <General />,
children: [{
path: "main",
element: <Main />,
},
{
path: "teams",
element: <Teams />,
},
{
path: "my-teams",
element: <MyTeams />,
},
]
},
])
function App() {
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,6 @@
.page-maket{
display: flex;
}
.main-content{
width: 100%;
}
@@ -0,0 +1,24 @@
import { ThemeProvider } from "../../../theme-provider"
import NavigationBar from "../../../widgets/NavigationBar/NavigationBar"
import '../../../../i18n'
import less from "./General.module.less"
import Header from "../../../widgets/Header/Header"
import { Outlet } from "react-router"
function General() {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<Header />
<div className={less['page-maket']}>
<NavigationBar />
<div className={less["main-content"]}>
<Outlet/>
</div>
</div>
</ThemeProvider>
)
}
export default General
@@ -0,0 +1,3 @@
.page-maket{
display: flex;
}
@@ -0,0 +1,17 @@
import { Outlet } from "react-router"
import Header from "../../../widgets/Header/Header"
import less from "./TemplateWeb.module.less"
import { ThemeProvider } from "../../../theme-provider"
const TemplateWeb = () => {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<Header />
<div className={less['page-maket']}>
</div>
<Outlet />
</ThemeProvider>
)
}
export default TemplateWeb
@@ -0,0 +1,12 @@
.card-img{
border: 2px solid #cdcdcd;
border-radius: 8px;
width: 100%;
height: 100%;
background: #d9d9d9;
}
.card{
width: 800px;
padding: 20px;
height: 230px;
}
@@ -0,0 +1,22 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "../../shared/ui/card"
import less from "./TeamsCard.module.less";
const TeamsCard = () =>{
return(
<Card className={`${less["card"]} flex flex-row`}>
<div className={less["card-img"]}></div>
<div className="flex flex-col">
<CardHeader >
<CardTitle>Lorem ipsum</CardTitle>
<CardDescription>Lorem ipsum</CardDescription>
</CardHeader>
<CardContent>
<p>Lorem ipsum dolor sit amet consectetur. Lorem justo sit nunc commodo nam fames dui ac ullamcorper. Laoreet faucibus semper adipiscing lobortis.</p>
</CardContent>
</div>
</Card>
)
}
export default TeamsCard;
@@ -0,0 +1,23 @@
.search-bar{
height: 40px;
border-bottom-width: 1px;
display: flex;
justify-content:space-between;
align-items: center;
padding: 0px 32px;
width: 100%;
margin-bottom: 10px;
}
.title-content{
font-family: var(--third-family);
font-weight: 900;
font-size: 16px;
line-height: 100%;
color: #000;
}
.input-search{
border: 0;
--tw-shadow: none;
--tw-shadow-colored:none;
box-shadow: none;
}
@@ -0,0 +1,22 @@
import { useTranslation } from "react-i18next";
import less from "./SearchBar.module.less"
import { Input } from "../../shared/ui/input";
import { Search } from "lucide-react";
const SearchBar = ({ title = "" }: { title: string }) => {
// const { t } = useTranslation();
return (
<nav className={less["search-bar"]}>
<h3 className={less['title-content']}>{title}</h3>
<Search/>
<Input className={less["input-search"]} placeholder="Введите что-то для поиска"></Input>
</nav>
)
}
export default SearchBar;
+41
View File
@@ -0,0 +1,41 @@
import { Moon, Sun } from "lucide-react"
import { Button } from "./shared/ui/button"
import { useTheme } from "./theme-provider"
import { useTranslation } from "react-i18next"
import { MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarShortcut } from "./shared/ui/menubar"
import { ResetIcon } from "@radix-ui/react-icons"
export function ModeToggle() {
const { setTheme } = useTheme()
const { t } = useTranslation();
return (
<MenubarMenu>
<MenubarTrigger asChild>
<Button variant="ghost" size="sm">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</MenubarTrigger>
<MenubarContent align="end">
<MenubarItem onClick={() => setTheme("light")}>
{t("LightTheme")}<MenubarShortcut>L</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={() => setTheme("dark")}>
{t("DarkTheme")}<MenubarShortcut>D</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => setTheme("system")}>
{t("SystemTheme")}<MenubarShortcut><ResetIcon /></MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
)
}
@@ -0,0 +1,26 @@
.block{
width: 2000px;
height: 2000px;
position: fixed;
top: -310px;
left: -808px;
z-index: -1;
background: rgb(136,255,42);
background: radial-gradient(circle, rgba(136,255,42,1) 0%, rgba(255,255,255,0) 52%, rgba(255,255,255,0) 100%);
}
.landing{
font-family: "Arial Black";
margin-top: 200px;
margin-left: 20px;
font-size: 150px;
line-height: 100%;
text-transform: uppercase;
}
.info-block{
display: flex;
flex-direction: column;
align-items: center;
width: 500px;
margin: 0 auto;
}
@@ -0,0 +1,21 @@
import less from "./Landing.module.less"
import '../../../i18n'
import { Button } from "../../shared/ui/button"
import { Label } from "@radix-ui/react-menubar"
import { useTranslation } from "react-i18next";
function Landing() {
const { t } = useTranslation();
return (
<><div className={less.block}></div>
<h1 className={less.landing}>{t("landingLogo")}</h1>
<div className={less["info-block"]}>
<Label className="mt-10 mb-1">Lorem ipsum dolor sit amet consectetur. Tincidunt nunc duis interdum feugiat viverra tellus eu amet fermentum. Metus nulla lacinia egestas scelerisque porta urna et massa. Id ut vel aliquet lorem velit. Blandit interdum enim suspendisse non at sem nulla diam ullamcorper.</Label>
<Button variant="outline">{t("buttonGoTOReg")}</Button>
</div>
</>
)
}
export default Landing
@@ -0,0 +1,6 @@
.divv{
margin: 0;
}
.general-content{
margin-left: 20px;
}
@@ -0,0 +1,14 @@
import SearchBar from "../../features/SearchBar/SearchBar";
import TeamsCard from "../../entities/TeamsCard/TeamsCard";
import less from "./Main.module.less"
const Main = () => {
return (
<><div><SearchBar title="Вакансии" />
</div><div className={less["general-content"]}>
<TeamsCard />
</div></>
)
}
export default Main;
@@ -0,0 +1,9 @@
import less from "./MyTeams.module.less"
const MyTeams = () => {
return (
<p>My teams</p>
)
}
export default MyTeams;
@@ -0,0 +1,10 @@
import SearchBar from "../../features/SearchBar/SearchBar";
import less from "./Teams.moadule.less"
const Teams = () => {
return (
<div><SearchBar title="Команды"/></div>
)
}
export default Teams;
@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../../../lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "../../../../lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "../../../../lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
@@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "../../../../lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
+176
View File
@@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "../../../../lib/utils"
import { Label } from "src/components/shared/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "../../../../lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../../../lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
@@ -0,0 +1,240 @@
"use client"
import * as React from "react"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { cn } from "../../../../lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}
@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "../../../../lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "../../../../lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}
@@ -0,0 +1,15 @@
import { FormEvent } from "react";
export const submitLogin = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const formProps = Object.fromEntries(formData);
console.log(formProps)
}
export const submitRegister = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const formProps = Object.fromEntries(formData);
console.log(formProps)
}
@@ -0,0 +1,42 @@
import { DialogContent, DialogTitle, DialogDescription } from "../../shared/ui/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../shared/ui/tabs";
import { t } from "i18next";
import { Button } from "../../shared/ui/button";
import { DialogHeader } from "../../shared/ui/dialog";
import { Input } from "../../shared/ui/input";
import { submitLogin, submitRegister } from "./AuthAPI";
const AuthForm = () => {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>{t("entrance")}</DialogTitle>
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">{t("login")}</TabsTrigger>
<TabsTrigger value="password">{t("registration")}</TabsTrigger>
</TabsList>
<TabsContent value="account" >
<form className="flex flex-col gap-y-1" onSubmit={submitLogin}>
<Input type="email" name="email" placeholder="Email" />
<Input type="password" name="password" placeholder="Password" />
<Button className="mt-3">{t("buttonLoginInSystem")}</Button>
</form>
</TabsContent>
<TabsContent value="password">
<form className="flex flex-col gap-y-1 mb-2" onSubmit={submitRegister}>
<Input type="text" className="m-to-2" name="username" placeholder="Username" />
<Input type="email" name="email" placeholder="Email"/>
<Input type="password" name="password" placeholder="Password"/>
<Input type="password" name="repPassword" placeholder="Password"/>
<Button className="mt-3">{t("buttonRegInSystemStep1")}</Button>
</form>
</TabsContent>
</Tabs>
</DialogHeader>
</DialogContent>
)
}
export default AuthForm;
@@ -0,0 +1,25 @@
.header{
height: 60px;
border-bottom-width: 1px;
display: flex;
justify-content:space-between;
align-items: center;
padding: 0px 32px;
width: 100%;
}
@logo-size: 80px;
.logo{
width: @logo-size;
height: @logo-size;
}
@icon-size: 20px;
.icon{
width: @icon-size;
height: @icon-size;
}
.line-block{
display: flex;
gap: 5px;
}
@@ -0,0 +1,5 @@
import { ReactNode } from "react";
export interface HeaderProps {
children: ReactNode;
}
@@ -0,0 +1,68 @@
import { useTranslation } from "react-i18next";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "../../shared/ui/menubar"
import {
Dialog,
DialogTrigger,
} from "../../shared/ui/dialog"
import less from "./Header.module.less"
import { ResetIcon } from "@radix-ui/react-icons";
import { Separator } from "../../shared/ui/separator";
import { ModeToggle } from "../../mode-toggle";
import AuthForm from "./AuthForm";
const Header = () => {
const { t, i18n } = useTranslation();
const handleTrans = (code: string) => {
i18n.changeLanguage(code);
};
return (
<header className={less.header}>
<img className={less.logo} src='/logo.svg'></img>
<div className={less["line-block"]}>
<Menubar>
<MenubarMenu>
<MenubarTrigger >{t("flag")} {t("langCode").toUpperCase()}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={() => handleTrans("ru")}>
🇷🇺 RU<MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={() => handleTrans("en")}>
🇬🇧 EN <MenubarShortcut>E</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={() => handleTrans("zh")}>
🇨🇳 ZH <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => handleTrans(navigator.language)}>System language<MenubarShortcut><ResetIcon /></MenubarShortcut></MenubarItem>
</MenubarContent>
</MenubarMenu>
<ModeToggle />
</Menubar>
<Separator orientation="vertical" />
<Dialog>
<DialogTrigger className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">{t("entrance")}
</DialogTrigger>
<AuthForm/>
</Dialog>
</div>
</header>
)
}
export default Header;
@@ -0,0 +1,21 @@
.nav-block{
display: flex;
flex-direction: column;
align-items: center;
border-right-width: 1px;
padding: 10px;
height: 100vh;
}
.button{
width: 180px;
text-align: left;
display: flex;
gap: 5px;
justify-content: space-between;
}
@icon-size: 18px;
.icon{
width: @icon-size;
height: @icon-size;
}
@@ -0,0 +1,15 @@
import { Button } from "../../shared/ui/button";
import less from "./NavigationBar.module.less"
import { useTranslation } from 'react-i18next';
import {Columns3, ColumnsIcon, HomeIcon, } from "lucide-react";
import { Link } from "react-router-dom";
const NavigationBar = () =>{
const { t } = useTranslation();
return( <nav className={less["nav-block"]}>
<Link to={"main"}><Button className={less.button} variant='ghost' size='default'><div className="flex gap-1"><HomeIcon className={less.icon}/>{t('home')}</div></Button></Link>
<Link to={"teams"}><Button className={less.button} variant='ghost' size='default'><div className="flex gap-1"><Columns3 className={less.icon}/>{t('teams')}</div> </Button></Link>
<Link to={"my-teams"}><Button className={less.button} variant='ghost' size='default'><div className="flex gap-1"><ColumnsIcon className={less.icon}/>{t('myTeams')}</div></Button></Link>
</nav>)
}
export default NavigationBar;
+84
View File
@@ -0,0 +1,84 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'ru',
interpolation: {
escapeValue: false,
},
resources: {
ru: {
translation: {
flag: "🇷🇺",
langCode: "ru",
home: "Главная",
teams: "Команды",
myTeams: "Мои команды",
something: "Что-то",
LightTheme: "Светлая",
DarkTheme: "Темная",
SystemTheme: "Тема устройства",
entrance: "Вход",
login: "Авторизация",
registration: "Регистрация",
loginHeader: "Введите адрес электронной почты и пароль, чтобы начать.",
regHeader: "Введите юзернейм, почту и пароль, чтобы зарегистрироваться.",
buttonLoginInSystem: "Войти в систему",
buttonRegInSystemStep1: "Продолжить",
buttonGoTOReg: "Приступить к регистрации!",
landingLogo: "Убьем надежду!",
}
},
en: {
translation: {
flag: "🇬🇧",
langCode: "en",
home: "Home",
myTeams: "My Teams",
Interests: "Interests",
something: "Something",
LightTheme: "Light",
DarkTheme: "Dark",
SystemTheme: "System Theme",
entrance: "Sign in",
login: "Log in",
registration: "Sign up",
loginHeader: " Enter your email address and password to get started.",
buttonLoginInSystem: "log in",
buttonRegInSystemStep1: "Continue",
buttonGoTOReg: "Proceed with registration!",
landingLogo: "Let's kill hope!",
}
},
zh: {
translation: {
flag: "🇨🇳",
langCode: "zn",
home: "家",
teams: "团队",
myTeams: "我的命令",
something: "某物",
LightTheme: "光",
DarkTheme: "黑暗",
SystemTheme: "系统主题",
entrance: "登入您的帐户",
login: "登录",
registration: "登记注册",
loginHeader: "请输入您的电子邮件地址和密码,开始更改.",
buttonLoginInSystem: "登录系统",
buttonRegInSystemStep1: "继续",
buttonGoTOReg: "继续登记!",
landingLogo: "让我们杀希望",
}
},
}
});
export default i18n;
View File
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

Some files were not shown because too many files have changed in this diff Show More