[init] Initial commit
This commit is contained in:
Executable
+52
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# Folders
|
||||
venv/
|
||||
__pycache__/
|
||||
Executable
+164
@@ -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
|
||||
@@ -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 . .
|
||||
Executable
+37
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# SkillHub Backend folder
|
||||
Executable
Executable
+7
@@ -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()
|
||||
Executable
+153
@@ -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()
|
||||
Executable
+68
@@ -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),
|
||||
),
|
||||
)
|
||||
Executable
+7
@@ -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()
|
||||
Executable
+21
@@ -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()
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotificationsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "notifications"
|
||||
@@ -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": "уведомления",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
# Create your tests here.
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TeamsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "teams"
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from users.models import User
|
||||
|
||||
admin.site.register(User)
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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}}
|
||||
@@ -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,
|
||||
)
|
||||
Executable
+14
@@ -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"
|
||||
@@ -0,0 +1,6 @@
|
||||
black
|
||||
sort-requirements
|
||||
ruff==0.3.4
|
||||
|
||||
-r prod.txt
|
||||
-r test.txt
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
django-debug-toolbar==4.3.0
|
||||
Executable
+18
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Dockerfile
|
||||
@@ -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": {
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# SkillHub Frontend folder
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
server {
|
||||
listen 3000;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html/;
|
||||
include /etc/nginx/mime.types;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Generated
+5174
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user