init
85
.gitignore
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
backend/__pycache__/
|
||||||
|
backend/**/*.pyc
|
||||||
|
backend/db.sqlite3
|
||||||
|
backend/media/
|
||||||
|
backend/static/
|
||||||
|
backend/*.log
|
||||||
|
backend/.env
|
||||||
|
backend/.venv/
|
||||||
|
backend/env/
|
||||||
|
backend/.mypy_cache/
|
||||||
|
backend/.pytest_cache/
|
||||||
|
backend/.coverage
|
||||||
|
backend/htmlcov/
|
||||||
|
backend/.vscode/
|
||||||
|
backend/.DS_Store
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.env
|
||||||
|
frontend/.DS_Store
|
||||||
|
frontend/.vscode/
|
||||||
|
frontend/*.log
|
||||||
|
frontend/.eslintcache
|
||||||
|
frontend/.parcel-cache/
|
||||||
|
frontend/.turbo/
|
||||||
|
frontend/.next/
|
||||||
|
frontend/.yarn/
|
||||||
|
frontend/.pnp*
|
||||||
|
frontend/yarn-error.log
|
||||||
|
frontend/.npm/
|
||||||
|
frontend/.cache/
|
||||||
|
frontend/.idea/
|
||||||
|
frontend/.swc/
|
||||||
|
frontend/.coverage
|
||||||
|
frontend/coverage/
|
||||||
|
frontend/.env.local
|
||||||
|
frontend/.env.development.local
|
||||||
|
frontend/.env.test.local
|
||||||
|
frontend/.env.production.local
|
||||||
|
frontend/.DS_Store
|
||||||
|
frontend/.vscode/
|
||||||
|
frontend/.history/
|
||||||
|
frontend/.sass-cache/
|
||||||
|
frontend/.tmp/
|
||||||
|
frontend/.log/
|
||||||
|
frontend/.vercel/
|
||||||
|
frontend/.firebase/
|
||||||
|
frontend/.netlify/
|
||||||
|
frontend/.expo/
|
||||||
|
frontend/.expo-shared/
|
||||||
|
frontend/.nx/
|
||||||
|
frontend/.storybook/
|
||||||
|
frontend/.husky/
|
||||||
|
frontend/.lintstagedrc*
|
||||||
|
frontend/.prettier*
|
||||||
|
frontend/.stylelint*
|
||||||
|
frontend/.editorconfig
|
||||||
|
frontend/.gitattributes
|
||||||
|
frontend/.gitmodules
|
||||||
|
frontend/.npmrc
|
||||||
|
frontend/.yarnrc
|
||||||
|
frontend/.yarnrc.yml
|
||||||
|
frontend/.pnpmfile.cjs
|
||||||
|
frontend/.pnpmfile.js
|
||||||
|
frontend/.pnpm-debug.log
|
||||||
|
frontend/.lock
|
||||||
|
frontend/.env.*.local
|
||||||
|
frontend/.env.*.prod
|
||||||
|
frontend/.env.*.test
|
||||||
|
frontend/.env.*.dev
|
||||||
|
frontend/.env.*.staging
|
||||||
|
frontend/.env.*.production
|
||||||
|
frontend/.env.*.development
|
||||||
|
frontend/.env.*.example
|
||||||
|
frontend/.env.*.sample
|
||||||
|
frontend/.env.*.template
|
||||||
|
frontend/.env.*.backup
|
||||||
|
frontend/.env.*.bak
|
||||||
|
frontend/.env.*.old
|
||||||
|
frontend/.env.*.new
|
||||||
|
frontend/.env.*.dist
|
||||||
|
frontend/.env.*.orig
|
||||||
|
.env
|
||||||
|
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
3
backend/vontor_cz/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ["celery_app"]
|
||||||
25
backend/vontor_cz/asgi.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for vontor_cz project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
#import myapp.routing # your app's routing
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings')
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
"http": get_asgi_application(),
|
||||||
|
"websocket": AuthMiddlewareStack(
|
||||||
|
URLRouter(
|
||||||
|
#myapp.routing.websocket_urlpatterns
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
8
backend/vontor_cz/celery.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
||||||
|
|
||||||
|
app = Celery("backend")
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
61
backend/vontor_cz/models.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class ActiveManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(is_deleted=False)
|
||||||
|
|
||||||
|
class AllManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset()
|
||||||
|
|
||||||
|
# How to use custom object Managers: add these fields to your model, to override objects behaviour and all_objects behaviour
|
||||||
|
# objects = ActiveManager()
|
||||||
|
# all_objects = AllManager()
|
||||||
|
|
||||||
|
|
||||||
|
class SoftDeleteModel(models.Model):
|
||||||
|
is_deleted = models.BooleanField(default=False)
|
||||||
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
self.is_deleted = True
|
||||||
|
self.deleted_at = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
objects = ActiveManager()
|
||||||
|
all_objects = AllManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
# Soft delete self
|
||||||
|
self.is_deleted = True
|
||||||
|
self.deleted_at = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def hard_delete(self, using=None, keep_parents=False):
|
||||||
|
super().delete(using=using, keep_parents=keep_parents)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SiteSettings model for managing site-wide settings
|
||||||
|
"""class SiteSettings(models.Model):
|
||||||
|
bank = models.CharField(max_length=100, blank=True)
|
||||||
|
support_email = models.EmailField(blank=True)
|
||||||
|
logo = models.ImageField(upload_to='settings/', blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Site Settings"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Site Settings"
|
||||||
|
verbose_name_plural = "Site Settings"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_solo(cls):
|
||||||
|
obj, created = cls.objects.get_or_create(id=1)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
"""
|
||||||
895
backend/vontor_cz/settings.py
Normal file
@@ -0,0 +1,895 @@
|
|||||||
|
"""
|
||||||
|
Django settings for vontor_cz project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.1.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.core.management.utils import get_random_secret_key
|
||||||
|
from django.db import OperationalError, connections
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv() # Pouze načte proměnné lokálně, pokud nejsou dostupné
|
||||||
|
|
||||||
|
#---------------- ENV VARIABLES USECASE--------------
|
||||||
|
# v jiné app si to importneš skrz: from django.conf import settings
|
||||||
|
# a použiješ takto: settings.FRONTEND_URL
|
||||||
|
|
||||||
|
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
||||||
|
FRONTEND_URL_DEV = os.getenv("FRONTEND_URL_DEV", "http://localhost:5173")
|
||||||
|
print(f"FRONTEND_URL: {FRONTEND_URL}\nFRONTEND_URL_DEV: {FRONTEND_URL_DEV}\n")
|
||||||
|
|
||||||
|
#-------------------------BASE ⚙️------------------------
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Pavel
|
||||||
|
# from django.conf.locale.en import formats as en_formats
|
||||||
|
|
||||||
|
DATETIME_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d", # '2025-07-25'
|
||||||
|
"%Y-%m-%d %H:%M", # '2025-07-25 14:30'
|
||||||
|
"%Y-%m-%d %H:%M:%S", # '2025-07-25 14:30:59'
|
||||||
|
"%Y-%m-%dT%H:%M", # '2025-07-25T14:30'
|
||||||
|
"%Y-%m-%dT%H:%M:%S", # '2025-07-25T14:30:59'
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'cs'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Prague'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
if os.getenv("DEBUG", "") == "True":
|
||||||
|
DEBUG = True
|
||||||
|
else:
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
print(f"\nDEBUG state: {str(DEBUG)}\nDEBUG .env raw: {os.getenv('DEBUG', '')}\n")
|
||||||
|
|
||||||
|
#-----------------------BASE END⚙️--------------------------
|
||||||
|
|
||||||
|
#--------------- URLS 🌐 -------------------
|
||||||
|
|
||||||
|
ASGI_APPLICATION = 'vontor_cz.asgi.application' #daphne
|
||||||
|
ROOT_URLCONF = 'vontor_cz.urls'
|
||||||
|
LOGIN_URL = '/admin' #nastavení Login adresy
|
||||||
|
|
||||||
|
#-----------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#----------------------------------- LOGS -------------------------------------------
|
||||||
|
#slouží pro tisknutí do konzole v dockeru skrz: logger.debug("content")
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"formatters": {
|
||||||
|
"verbose": {
|
||||||
|
"format": "{levelname} {asctime} {name}: {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG" if DEBUG else "INFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Vytvoř si logger podle názvu souboru (modulu)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug("Ladicí zpráva – vidíš jen když je DEBUG = True")
|
||||||
|
logger.info("Informace – např. že uživatel klikl na tlačítko")
|
||||||
|
logger.warning("Varování – něco nečekaného, ale ne kritického")
|
||||||
|
logger.error("Chyba – něco se pokazilo, ale aplikace jede dál")
|
||||||
|
logger.critical("Kritická chyba – selhání systému, třeba pád služby")
|
||||||
|
"""
|
||||||
|
|
||||||
|
#---------------------------------- END LOGS ---------------------------------------
|
||||||
|
|
||||||
|
#-------------------------------------SECURITY 🔐------------------------------------
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
SECRET_KEY = 'pernament'
|
||||||
|
else:
|
||||||
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", get_random_secret_key())
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_COOKIE_AGE = 86400 # one day
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
#'vontor_cz.backend.EmailOrUsernameModelBackend', #custom backend z authentication aplikace
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
]
|
||||||
|
|
||||||
|
#--------------------------------END SECURITY 🔐-------------------------------------
|
||||||
|
|
||||||
|
#-------------------------------------CORS + HOSTs 🌐🔐------------------------------------
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
'https://domena.cz',
|
||||||
|
"https://www.domena.cz",
|
||||||
|
"http://localhost:3000", #react docker
|
||||||
|
"http://localhost:5173" #react dev
|
||||||
|
]
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:3000",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"https://www.domena.cz",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = False # Tohle musí být false, když používáš credentials
|
||||||
|
|
||||||
|
print("CORS_ALLOWED_ORIGINS =", CORS_ALLOWED_ORIGINS)
|
||||||
|
print("CSRF_TRUSTED_ORIGINS =", CSRF_TRUSTED_ORIGINS)
|
||||||
|
print("ALLOWED_HOSTS =", ALLOWED_HOSTS)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#--------------------------------END CORS + HOSTs 🌐🔐---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#--------------------------------------SSL 🧾------------------------------------
|
||||||
|
|
||||||
|
if os.getenv("SSL", "") == "True":
|
||||||
|
USE_SSL = True
|
||||||
|
else:
|
||||||
|
USE_SSL = False
|
||||||
|
|
||||||
|
|
||||||
|
if USE_SSL is True:
|
||||||
|
print("SSL turned on!")
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
else:
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
CSRF_COOKIE_SECURE = False
|
||||||
|
SECURE_SSL_REDIRECT = False
|
||||||
|
SECURE_BROWSER_XSS_FILTER = False
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = False
|
||||||
|
USE_X_FORWARDED_HOST = False
|
||||||
|
|
||||||
|
print(f"\nUsing SSL: {USE_SSL}\n")
|
||||||
|
|
||||||
|
#--------------------------------END-SSL 🧾---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------REST FRAMEWORK 🛠️------------------------------------
|
||||||
|
|
||||||
|
# ⬇️ Základní lifetime konfigurace
|
||||||
|
ACCESS_TOKEN_LIFETIME = timedelta(minutes=15)
|
||||||
|
REFRESH_TOKEN_LIFETIME = timedelta(days=1)
|
||||||
|
|
||||||
|
# ⬇️ Nastavení SIMPLE_JWT podle režimu
|
||||||
|
if DEBUG:
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": ACCESS_TOKEN_LIFETIME,
|
||||||
|
"REFRESH_TOKEN_LIFETIME": REFRESH_TOKEN_LIFETIME,
|
||||||
|
|
||||||
|
"AUTH_COOKIE": "access_token",
|
||||||
|
"AUTH_COOKIE_SECURE": False, # není HTTPS
|
||||||
|
"AUTH_COOKIE_HTTP_ONLY": True,
|
||||||
|
"AUTH_COOKIE_PATH": "/",
|
||||||
|
"AUTH_COOKIE_SAMESITE": "Lax", # není cross-site
|
||||||
|
|
||||||
|
"ROTATE_REFRESH_TOKENS": True,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": True,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": ACCESS_TOKEN_LIFETIME,
|
||||||
|
"REFRESH_TOKEN_LIFETIME": REFRESH_TOKEN_LIFETIME,
|
||||||
|
|
||||||
|
"AUTH_COOKIE": "access_token",
|
||||||
|
"AUTH_COOKIE_SECURE": True, # HTTPS only
|
||||||
|
"AUTH_COOKIE_HTTP_ONLY": True,
|
||||||
|
"AUTH_COOKIE_PATH": "/",
|
||||||
|
"AUTH_COOKIE_SAMESITE": "None", # potřebné pro cross-origin
|
||||||
|
|
||||||
|
"ROTATE_REFRESH_TOKENS": True,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
"DATETIME_FORMAT": "%Y-%m-%d %H:%M", # Pavel
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'account.tokens.CookieJWTAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.AllowAny',
|
||||||
|
),
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
|
|
||||||
|
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
||||||
|
}
|
||||||
|
|
||||||
|
#--------------------------------END REST FRAMEWORK 🛠️-------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------APPS 📦------------------------------------
|
||||||
|
MY_CREATED_APPS = [
|
||||||
|
'account',
|
||||||
|
]
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'daphne', #asgi bude fungovat lokálně (musí být na začátku)
|
||||||
|
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
'corsheaders', #cors
|
||||||
|
|
||||||
|
'django_celery_beat', #slouží k plánování úkolů pro Celery
|
||||||
|
|
||||||
|
|
||||||
|
#'chat.apps.GlobalChatCheck', #tohle se spusti při každé django inicializaci (migration, createmigration, runserver)
|
||||||
|
|
||||||
|
#'authentication',
|
||||||
|
|
||||||
|
'storages',# Adds support for external storage services like Amazon S3 via django-storages
|
||||||
|
'django_filters',
|
||||||
|
|
||||||
|
'channels' ,# django channels
|
||||||
|
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework_api_key',
|
||||||
|
|
||||||
|
'drf_spectacular', #rest framework, grafické zobrazení
|
||||||
|
|
||||||
|
#Nastavení stránky
|
||||||
|
#'constance',
|
||||||
|
#'constance.backends.database',
|
||||||
|
|
||||||
|
'django.contrib.sitemaps',
|
||||||
|
|
||||||
|
'tinymce',
|
||||||
|
|
||||||
|
|
||||||
|
#kvůli bugum je lepší to dát na poslední místo v INSTALLED_APPS
|
||||||
|
'django_cleanup.apps.CleanupConfig', #app která maže nepoužité soubory(media) z databáze na S3
|
||||||
|
]
|
||||||
|
|
||||||
|
#skládaní dohromady INSTALLED_APPS
|
||||||
|
INSTALLED_APPS = INSTALLED_APPS[:-1] + MY_CREATED_APPS + INSTALLED_APPS[-1:]
|
||||||
|
|
||||||
|
# -------------------------------------END APPS 📦------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------MIDDLEWARE 🧩------------------------------------
|
||||||
|
# Middleware is a framework of hooks into Django's request/response processing.
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
# Middleware that allows your backend to accept requests from other domains (CORS)
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
|
||||||
|
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
|
#CUSTOM
|
||||||
|
#'tools.middleware.CustomMaxUploadSizeMiddleware',
|
||||||
|
|
||||||
|
|
||||||
|
'whitenoise.middleware.WhiteNoiseMiddleware',# díky tomu funguje načítaní static files
|
||||||
|
]
|
||||||
|
|
||||||
|
#--------------------------------END MIDDLEWARE 🧩---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------CACHE + CHANNELS(ws) 📡🗄️------------------------------------
|
||||||
|
|
||||||
|
# Caching settings for Redis (using Docker's internal network name for Redis)
|
||||||
|
if DEBUG is False:
|
||||||
|
#PRODUCTION
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
'LOCATION': 'redis://redis:6379/0', # Using the service name `redis` from Docker Compose
|
||||||
|
'OPTIONS': {
|
||||||
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
|
'PASSWORD': os.getenv('REDIS_PASSWORD'), # Make sure to set REDIS_PASSWORD in your environment
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSockets Channel Layers (using Redis in production)
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||||
|
'CONFIG': {
|
||||||
|
'hosts': [('redis', 6379)], # Use `redis` service in Docker Compose
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
#DEVELOPMENT
|
||||||
|
# Use in-memory channel layer for development (when DEBUG is True)
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels.layers.InMemoryChannelLayer',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use in-memory cache for development (when DEBUG is True)
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#--------------------------------END CACHE + CHANNELS(ws) 📡🗄️---------------------------------
|
||||||
|
|
||||||
|
#-------------------------------------CELERY 📅------------------------------------
|
||||||
|
|
||||||
|
# CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||||
|
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL")
|
||||||
|
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
# test connection
|
||||||
|
r = redis.Redis(host='localhost', port=6379, db=0)
|
||||||
|
r.ping()
|
||||||
|
except Exception:
|
||||||
|
CELERY_BROKER_URL = 'memory://'
|
||||||
|
|
||||||
|
CELERY_ACCEPT_CONTENT = os.getenv("CELERY_ACCEPT_CONTENT")
|
||||||
|
CELERY_TASK_SERIALIZER = os.getenv("CELERY_TASK_SERIALIZER")
|
||||||
|
CELERY_TIMEZONE = os.getenv("CELERY_TIMEZONE")
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULER = os.getenv("CELERY_BEAT_SCHEDULER")
|
||||||
|
# if DEBUG:
|
||||||
|
# CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||||
|
# try:
|
||||||
|
# import redis
|
||||||
|
# # test connection
|
||||||
|
# r = redis.Redis(host='localhost', port=6379, db=0)
|
||||||
|
# r.ping()
|
||||||
|
# except Exception:
|
||||||
|
# CELERY_BROKER_URL = 'memory://'
|
||||||
|
|
||||||
|
# CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
# CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
# CELERY_TIMEZONE = 'Europe/Prague'
|
||||||
|
|
||||||
|
# CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||||
|
|
||||||
|
# from celery.schedules import crontab
|
||||||
|
|
||||||
|
# CELERY_BEAT_SCHEDULE = {
|
||||||
|
# 'hard_delete_soft_deleted_monthly': {
|
||||||
|
# 'task': 'vontor_cz.tasks.hard_delete_soft_deleted_records',
|
||||||
|
# 'schedule': crontab(minute=0, hour=0, day_of_month=1), # každý první den v měsíci o půlnoci
|
||||||
|
# },
|
||||||
|
# 'delete_old_reservations_monthly': {
|
||||||
|
# 'task': 'account.tasks.delete_old_reservations',
|
||||||
|
# 'schedule': crontab(minute=0, hour=1, day_of_month=1), # každý první den v měsíci v 1:00 ráno
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
# else:
|
||||||
|
# # Nebo nastav dummy broker, aby se úlohy neodesílaly
|
||||||
|
# CELERY_BROKER_URL = 'memory://' # broker v paměti, pro testování bez Redis
|
||||||
|
|
||||||
|
#-------------------------------------END CELERY 📅------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------DATABASE 💾------------------------------------
|
||||||
|
|
||||||
|
# Nastavuje výchozí typ primárního klíče pro modely.
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# říka že se úkladá do databáze, místo do cookie
|
||||||
|
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
|
||||||
|
|
||||||
|
USE_PRODUCTION_DB = os.getenv("USE_PRODUCTION_DB", "False") == "True"
|
||||||
|
|
||||||
|
if USE_PRODUCTION_DB is False:
|
||||||
|
# DEVELOPMENT
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3', # Database engine
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3', # Path to the SQLite database file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
#PRODUCTION
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': os.getenv('DATABASE_ENGINE'),
|
||||||
|
'NAME': os.getenv('DATABASE_NAME'),
|
||||||
|
'USER': os.getenv('DATABASE_USER'),
|
||||||
|
'PASSWORD': os.getenv('DATABASE_PASSWORD'),
|
||||||
|
'HOST': os.getenv('DATABASE_HOST', "localhost"),
|
||||||
|
'PORT': os.getenv('DATABASE_PORT'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'account.CustomUser' #class CustomUser(AbstractUser) best practice to use AbstractUser
|
||||||
|
|
||||||
|
#--------------------------------END DATABASE 💾---------------------------------
|
||||||
|
|
||||||
|
#--------------------------------------PAGE SETTINGS -------------------------------------
|
||||||
|
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
|
||||||
|
|
||||||
|
# Configuration for Constance(variables)
|
||||||
|
CONSTANCE_CONFIG = {
|
||||||
|
'BITCOIN_WALLET': ('', 'Public BTC wallet address'),
|
||||||
|
'SUPPORT_EMAIL': ('admin@example.com', 'Support email'),
|
||||||
|
}
|
||||||
|
|
||||||
|
#--------------------------------------EMAIL 📧--------------------------------------
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
# DEVELOPMENT
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # Use console backend for development
|
||||||
|
# EMAILY SE BUDOU POSÍLAT DO KONZOLE!!!
|
||||||
|
else:
|
||||||
|
# PRODUCTION
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
|
EMAIL_HOST = os.getenv("EMAIL_HOST_DEV")
|
||||||
|
EMAIL_PORT = int(os.getenv("EMAIL_PORT_DEV", 465))
|
||||||
|
EMAIL_USE_TLS = True # ❌ Keep this OFF when using SSL
|
||||||
|
EMAIL_USE_SSL = False # ✅ Must be True for port 465
|
||||||
|
EMAIL_HOST_USER = os.getenv("EMAIL_USER_DEV")
|
||||||
|
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_USER_PASSWORD_DEV")
|
||||||
|
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
||||||
|
EMAIL_TIMEOUT = 10
|
||||||
|
|
||||||
|
print("---------EMAIL----------\nEMAIL_HOST =", os.getenv("EMAIL_HOST_DEV"))
|
||||||
|
print("EMAIL_PORT =", os.getenv("EMAIL_PORT_DEV"))
|
||||||
|
print("EMAIL_USER =", os.getenv("EMAIL_USER_DEV"))
|
||||||
|
print("EMAIL_USER_PASSWORD =", os.getenv("EMAIL_USER_PASSWORD_DEV"), "\n------------------------")
|
||||||
|
|
||||||
|
#----------------------------------EMAIL END 📧-------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------TEMPLATES 🗂️------------------------------------
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
"DIRS": [BASE_DIR / 'templates'],
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
#--------------------------------END TEMPLATES 🗂️---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------MEDIA + STATIC 🖼️, AWS ☁️------------------------------------
|
||||||
|
|
||||||
|
# nastavení složky pro globalstaticfiles (static složky django hledá samo)
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / 'globalstaticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if os.getenv("USE_AWS", "") == "True":
|
||||||
|
USE_AWS = True
|
||||||
|
else:
|
||||||
|
USE_AWS = False
|
||||||
|
|
||||||
|
print(f"\n-------------- USE_AWS: {USE_AWS} --------------")
|
||||||
|
|
||||||
|
if USE_AWS is False:
|
||||||
|
# DEVELOPMENT
|
||||||
|
|
||||||
|
|
||||||
|
# Development: Use local file system storage for static files
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Media and Static URL for local dev
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# Local folder for collected static files
|
||||||
|
STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
|
||||||
|
|
||||||
|
elif USE_AWS:
|
||||||
|
# PRODUCTION
|
||||||
|
|
||||||
|
AWS_LOCATION = "static"
|
||||||
|
|
||||||
|
# Production: Use S3 storage
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND" : "storages.backends.s3boto3.S3StaticStorage",
|
||||||
|
},
|
||||||
|
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND" : "storages.backends.s3boto3.S3StaticStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Media and Static URL for AWS S3
|
||||||
|
MEDIA_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/media/'
|
||||||
|
STATIC_URL = f'https://{os.getenv("AWS_STORAGE_BUCKET_NAME")}.s3.amazonaws.com/static/'
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS.append(STATIC_URL)
|
||||||
|
|
||||||
|
# Static files should be collected to a local directory and then uploaded to S3
|
||||||
|
STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
||||||
|
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||||
|
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
|
||||||
|
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME', 'us-east-1') # Default to 'us-east-1' if not set
|
||||||
|
AWS_S3_SIGNATURE_VERSION = 's3v4' # Use AWS Signature Version 4
|
||||||
|
AWS_S3_USE_SSL = True
|
||||||
|
AWS_S3_FILE_OVERWRITE = True
|
||||||
|
AWS_DEFAULT_ACL = None # Set to None to avoid setting a default ACL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(f"Static url: {STATIC_URL}\nStatic storage: {STORAGES}\n----------------------------")
|
||||||
|
|
||||||
|
#--------------------------------END: MEDIA + STATIC 🖼️, AWS ☁️---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------TINY MCE ✍️------------------------------------
|
||||||
|
|
||||||
|
TINYMCE_JS_URL = 'https://cdn.tiny.cloud/1/no-api-key/tinymce/7/tinymce.min.js'
|
||||||
|
|
||||||
|
TINYMCE_DEFAULT_CONFIG = {
|
||||||
|
"height": "320px",
|
||||||
|
"width": "960px",
|
||||||
|
"menubar": "file edit view insert format tools table help",
|
||||||
|
"plugins": "advlist autolink lists link image charmap print preview anchor searchreplace visualblocks code "
|
||||||
|
"fullscreen insertdatetime media table paste code help wordcount spellchecker",
|
||||||
|
"toolbar": "undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft "
|
||||||
|
"aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor "
|
||||||
|
"backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | "
|
||||||
|
"fullscreen preview save print | insertfile image media pageembed template link anchor codesample | "
|
||||||
|
"a11ycheck ltr rtl | showcomments addcomment code",
|
||||||
|
"custom_undo_redo_levels": 10,
|
||||||
|
}
|
||||||
|
TINYMCE_SPELLCHECKER = True
|
||||||
|
TINYMCE_COMPRESSOR = True
|
||||||
|
|
||||||
|
#--------------------------------END-TINY-MCE-SECTION ✍️---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------DRF SPECTACULAR 📊------------------------------------
|
||||||
|
|
||||||
|
SPECTACULAR_DEFAULTS: Dict[str, Any] = {
|
||||||
|
# A regex specifying the common denominator for all operation paths. If
|
||||||
|
# SCHEMA_PATH_PREFIX is set to None, drf-spectacular will attempt to estimate
|
||||||
|
# a common prefix. Use '' to disable.
|
||||||
|
# Mainly used for tag extraction, where paths like '/api/v1/albums' with
|
||||||
|
# a SCHEMA_PATH_PREFIX regex '/api/v[0-9]' would yield the tag 'albums'.
|
||||||
|
'SCHEMA_PATH_PREFIX': None,
|
||||||
|
|
||||||
|
# Remove matching SCHEMA_PATH_PREFIX from operation path. Usually used in
|
||||||
|
# conjunction with appended prefixes in SERVERS.
|
||||||
|
'SCHEMA_PATH_PREFIX_TRIM': False,
|
||||||
|
|
||||||
|
# Insert a manual path prefix to the operation path, e.g. '/service/backend'.
|
||||||
|
# Use this for example to align paths when the API is mounted as a sub-resource
|
||||||
|
# behind a proxy and Django is not aware of that. Alternatively, prefixes can
|
||||||
|
# also specified via SERVERS, but this makes the operation path more explicit.
|
||||||
|
'SCHEMA_PATH_PREFIX_INSERT': '',
|
||||||
|
|
||||||
|
# Coercion of {pk} to {id} is controlled by SCHEMA_COERCE_PATH_PK. Additionally,
|
||||||
|
# some libraries (e.g. drf-nested-routers) use "_pk" suffixed path variables.
|
||||||
|
# This setting globally coerces path variables like "{user_pk}" to "{user_id}".
|
||||||
|
'SCHEMA_COERCE_PATH_PK_SUFFIX': False,
|
||||||
|
|
||||||
|
# Schema generation parameters to influence how components are constructed.
|
||||||
|
# Some schema features might not translate well to your target.
|
||||||
|
# Demultiplexing/modifying components might help alleviate those issues.
|
||||||
|
'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator',
|
||||||
|
|
||||||
|
# Create separate components for PATCH endpoints (without required list)
|
||||||
|
'COMPONENT_SPLIT_PATCH': True,
|
||||||
|
|
||||||
|
# Split components into request and response parts where appropriate
|
||||||
|
# This setting is highly recommended to achieve the most accurate API
|
||||||
|
# description, however it comes at the cost of having more components.
|
||||||
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
|
|
||||||
|
# Aid client generator targets that have trouble with read-only properties.
|
||||||
|
'COMPONENT_NO_READ_ONLY_REQUIRED': False,
|
||||||
|
|
||||||
|
# Adds "minLength: 1" to fields that do not allow blank strings. Deactivated
|
||||||
|
# by default because serializers do not strictly enforce this on responses and
|
||||||
|
# so "minLength: 1" may not always accurately describe API behavior.
|
||||||
|
# Gets implicitly enabled by COMPONENT_SPLIT_REQUEST, because this can be
|
||||||
|
# accurately modeled when request and response components are separated.
|
||||||
|
'ENFORCE_NON_BLANK_FIELDS': False,
|
||||||
|
|
||||||
|
# This version string will end up the in schema header. The default OpenAPI
|
||||||
|
# version is 3.0.3, which is heavily tested. We now also support 3.1.0,
|
||||||
|
# which contains the same features and a few mandatory, but minor changes.
|
||||||
|
'OAS_VERSION': '3.0.3',
|
||||||
|
|
||||||
|
# Configuration for serving a schema subset with SpectacularAPIView
|
||||||
|
'SERVE_URLCONF': None,
|
||||||
|
|
||||||
|
# complete public schema or a subset based on the requesting user
|
||||||
|
'SERVE_PUBLIC': True,
|
||||||
|
|
||||||
|
# include schema endpoint into schema
|
||||||
|
'SERVE_INCLUDE_SCHEMA': True,
|
||||||
|
|
||||||
|
# list of authentication/permission classes for spectacular's views.
|
||||||
|
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], #account.permissions.AdminOnly
|
||||||
|
|
||||||
|
# None will default to DRF's AUTHENTICATION_CLASSES
|
||||||
|
'SERVE_AUTHENTICATION': None,
|
||||||
|
|
||||||
|
# Dictionary of general configuration to pass to the SwaggerUI({ ... })
|
||||||
|
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||||||
|
# The settings are serialized with json.dumps(). If you need customized JS, use a
|
||||||
|
# string instead. The string must then contain valid JS and is passed unchanged.
|
||||||
|
'SWAGGER_UI_SETTINGS': {
|
||||||
|
'deepLinking': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
# Initialize SwaggerUI with additional OAuth2 configuration.
|
||||||
|
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/
|
||||||
|
'SWAGGER_UI_OAUTH2_CONFIG': {},
|
||||||
|
|
||||||
|
# Dictionary of general configuration to pass to the Redoc.init({ ... })
|
||||||
|
# https://redocly.com/docs/redoc/config/#functional-settings
|
||||||
|
# The settings are serialized with json.dumps(). If you need customized JS, use a
|
||||||
|
# string instead. The string must then contain valid JS and is passed unchanged.
|
||||||
|
'REDOC_UI_SETTINGS': {},
|
||||||
|
|
||||||
|
# CDNs for swagger and redoc. You can change the version or even host your
|
||||||
|
# own depending on your requirements. For self-hosting, have a look at
|
||||||
|
# the sidecar option in the README.
|
||||||
|
'SWAGGER_UI_DIST': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest',
|
||||||
|
'SWAGGER_UI_FAVICON_HREF': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/favicon-32x32.png',
|
||||||
|
'REDOC_DIST': 'https://cdn.jsdelivr.net/npm/redoc@latest',
|
||||||
|
|
||||||
|
# Append OpenAPI objects to path and components in addition to the generated objects
|
||||||
|
'APPEND_PATHS': {},
|
||||||
|
'APPEND_COMPONENTS': {},
|
||||||
|
|
||||||
|
|
||||||
|
# Postprocessing functions that run at the end of schema generation.
|
||||||
|
# must satisfy interface result = hook(generator, request, public, result)
|
||||||
|
'POSTPROCESSING_HOOKS': [
|
||||||
|
'drf_spectacular.hooks.postprocess_schema_enums'
|
||||||
|
],
|
||||||
|
|
||||||
|
# Preprocessing functions that run before schema generation.
|
||||||
|
# must satisfy interface result = hook(endpoints=result) where result
|
||||||
|
# is a list of Tuples (path, path_regex, method, callback).
|
||||||
|
# Example: 'drf_spectacular.hooks.preprocess_exclude_path_format'
|
||||||
|
'PREPROCESSING_HOOKS': [],
|
||||||
|
|
||||||
|
# Determines how operations should be sorted. If you intend to do sorting with a
|
||||||
|
# PREPROCESSING_HOOKS, be sure to disable this setting. If configured, the sorting
|
||||||
|
# is applied after the PREPROCESSING_HOOKS. Accepts either
|
||||||
|
# True (drf-spectacular's alpha-sorter), False, or a callable for sort's key arg.
|
||||||
|
'SORT_OPERATIONS': True,
|
||||||
|
|
||||||
|
# enum name overrides. dict with keys "YourEnum" and their choice values "field.choices"
|
||||||
|
# e.g. {'SomeEnum': ['A', 'B'], 'OtherEnum': 'import.path.to.choices'}
|
||||||
|
'ENUM_NAME_OVERRIDES': {},
|
||||||
|
|
||||||
|
# Adds "blank" and "null" enum choices where appropriate. disable on client generation issues
|
||||||
|
'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': True,
|
||||||
|
|
||||||
|
# Add/Append a list of (``choice value`` - choice name) to the enum description string.
|
||||||
|
'ENUM_GENERATE_CHOICE_DESCRIPTION': True,
|
||||||
|
|
||||||
|
# Optional suffix for generated enum.
|
||||||
|
# e.g. {'ENUM_SUFFIX': "Type"} would produce an enum name 'StatusType'.
|
||||||
|
'ENUM_SUFFIX': 'Enum',
|
||||||
|
|
||||||
|
# function that returns a list of all classes that should be excluded from doc string extraction
|
||||||
|
'GET_LIB_DOC_EXCLUDES': 'drf_spectacular.plumbing.get_lib_doc_excludes',
|
||||||
|
|
||||||
|
# Function that returns a mocked request for view processing. For CLI usage
|
||||||
|
# original_request will be None.
|
||||||
|
# interface: request = build_mock_request(method, path, view, original_request, **kwargs)
|
||||||
|
'GET_MOCK_REQUEST': 'drf_spectacular.plumbing.build_mock_request',
|
||||||
|
|
||||||
|
# Camelize names like "operationId" and path parameter names
|
||||||
|
# Camelization of the operation schema itself requires the addition of
|
||||||
|
# 'drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields'
|
||||||
|
# to POSTPROCESSING_HOOKS. Please note that the hook depends on
|
||||||
|
# ``djangorestframework_camel_case``, while CAMELIZE_NAMES itself does not.
|
||||||
|
'CAMELIZE_NAMES': False,
|
||||||
|
|
||||||
|
# Changes the location of the action/method on the generated OperationId. For example,
|
||||||
|
# "POST": "group_person_list", "group_person_create"
|
||||||
|
# "PRE": "list_group_person", "create_group_person"
|
||||||
|
'OPERATION_ID_METHOD_POSITION': 'POST',
|
||||||
|
|
||||||
|
# Determines if and how free-form 'additionalProperties' should be emitted in the schema. Some
|
||||||
|
# code generator targets are sensitive to this. None disables generic 'additionalProperties'.
|
||||||
|
# allowed values are 'dict', 'bool', None
|
||||||
|
'GENERIC_ADDITIONAL_PROPERTIES': 'dict',
|
||||||
|
|
||||||
|
# Path converter schema overrides (e.g. <int:foo>). Can be used to either modify default
|
||||||
|
# behavior or provide a schema for custom converters registered with register_converter(...).
|
||||||
|
# Takes converter labels as keys and either basic python types, OpenApiType, or raw schemas
|
||||||
|
# as values. Example: {'aint': OpenApiTypes.INT, 'bint': str, 'cint': {'type': ...}}
|
||||||
|
'PATH_CONVERTER_OVERRIDES': {},
|
||||||
|
|
||||||
|
# Determines whether operation parameters should be sorted alphanumerically or just in
|
||||||
|
# the order they arrived. Accepts either True, False, or a callable for sort's key arg.
|
||||||
|
'SORT_OPERATION_PARAMETERS': True,
|
||||||
|
|
||||||
|
# @extend_schema allows to specify status codes besides 200. This functionality is usually used
|
||||||
|
# to describe error responses, which rarely make use of list mechanics. Therefore, we suppress
|
||||||
|
# listing (pagination and filtering) on non-2XX status codes by default. Toggle this to enable
|
||||||
|
# list responses with ListSerializers/many=True irrespective of the status code.
|
||||||
|
'ENABLE_LIST_MECHANICS_ON_NON_2XX': False,
|
||||||
|
|
||||||
|
# This setting allows you to deviate from the default manager by accessing a different model
|
||||||
|
# property. We use "objects" by default for compatibility reasons. Using "_default_manager"
|
||||||
|
# will likely fix most issues, though you are free to choose any name.
|
||||||
|
"DEFAULT_QUERY_MANAGER": 'objects',
|
||||||
|
|
||||||
|
# Controls which authentication methods are exposed in the schema. If not None, will hide
|
||||||
|
# authentication classes that are not contained in the whitelist. Use full import paths
|
||||||
|
# like ['rest_framework.authentication.TokenAuthentication', ...].
|
||||||
|
# Empty list ([]) will hide all authentication methods. The default None will show all.
|
||||||
|
'AUTHENTICATION_WHITELIST': None,
|
||||||
|
# Controls which parsers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST.
|
||||||
|
# List of allowed parsers or None to allow all.
|
||||||
|
'PARSER_WHITELIST': None,
|
||||||
|
# Controls which renderers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST.
|
||||||
|
# rest_framework.renderers.BrowsableAPIRenderer is ignored by default if whitelist is None
|
||||||
|
'RENDERER_WHITELIST': None,
|
||||||
|
|
||||||
|
# Option for turning off error and warn messages
|
||||||
|
'DISABLE_ERRORS_AND_WARNINGS': False,
|
||||||
|
|
||||||
|
# Runs exemplary schema generation and emits warnings as part of "./manage.py check --deploy"
|
||||||
|
'ENABLE_DJANGO_DEPLOY_CHECK': True,
|
||||||
|
|
||||||
|
# General schema metadata. Refer to spec for valid inputs
|
||||||
|
# https://spec.openapis.org/oas/v3.0.3#openapi-object
|
||||||
|
'TITLE': 'e-Tržnice API',
|
||||||
|
'DESCRIPTION': 'This is the API documentation for e-Tržnice.',
|
||||||
|
'TOS': None,
|
||||||
|
# Optional: MAY contain "name", "url", "email"
|
||||||
|
'CONTACT': {},
|
||||||
|
# Optional: MUST contain "name", MAY contain URL
|
||||||
|
|
||||||
|
'LICENSE': {},
|
||||||
|
# Statically set schema version. May also be an empty string. When used together with
|
||||||
|
# view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests.
|
||||||
|
# Set VERSION to None if only the request version should be rendered.
|
||||||
|
'VERSION': '1.0.0',
|
||||||
|
# Optional list of servers.
|
||||||
|
# Each entry MUST contain "url", MAY contain "description", "variables"
|
||||||
|
# e.g. [{'url': 'https://example.com/v1', 'description': 'Text'}, ...]
|
||||||
|
'SERVERS': [],
|
||||||
|
# Tags defined in the global scope
|
||||||
|
'TAGS': [],
|
||||||
|
# Optional: List of OpenAPI 3.1 webhooks. Each entry should be an import path to an
|
||||||
|
# OpenApiWebhook instance.
|
||||||
|
'WEBHOOKS': [],
|
||||||
|
# Optional: MUST contain 'url', may contain "description"
|
||||||
|
'EXTERNAL_DOCS': {},
|
||||||
|
|
||||||
|
# Arbitrary specification extensions attached to the schema's info object.
|
||||||
|
# https://swagger.io/specification/#specification-extensions
|
||||||
|
'EXTENSIONS_INFO': {},
|
||||||
|
|
||||||
|
# Arbitrary specification extensions attached to the schema's root object.
|
||||||
|
# https://swagger.io/specification/#specification-extensions
|
||||||
|
'EXTENSIONS_ROOT': {},
|
||||||
|
|
||||||
|
# Oauth2 related settings. used for example by django-oauth2-toolkit.
|
||||||
|
# https://spec.openapis.org/oas/v3.0.3#oauth-flows-object
|
||||||
|
'OAUTH2_FLOWS': [],
|
||||||
|
'OAUTH2_AUTHORIZATION_URL': None,
|
||||||
|
'OAUTH2_TOKEN_URL': None,
|
||||||
|
'OAUTH2_REFRESH_URL': None,
|
||||||
|
'OAUTH2_SCOPES': None,
|
||||||
|
}
|
||||||
39
backend/vontor_cz/urls.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for vontor_cz project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from drf_spectacular.views import (
|
||||||
|
SpectacularAPIView,
|
||||||
|
SpectacularSwaggerView,
|
||||||
|
SpectacularRedocView,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
#rest framework, map of api
|
||||||
|
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||||
|
path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||||
|
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||||
|
|
||||||
|
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/account/', include('account.urls')),
|
||||||
|
|
||||||
|
path('api/stripe/', include('thirdparty.stripe.urls')),
|
||||||
|
path('api/trading212/', include('thirdparty.trading212.urls')),
|
||||||
|
|
||||||
|
]
|
||||||
16
backend/vontor_cz/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for vontor_cz project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vontor_cz.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
44
docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
build: ./backend
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
command: daphne -b 0.0.0.0 -p 8000 backend.asgi:application
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
env_file:
|
||||||
|
- ./frontend/.env
|
||||||
|
build: ./frontend
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
command: npm run dev
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
db:
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
image: postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: mydb
|
||||||
|
POSTGRES_USER: myuser
|
||||||
|
POSTGRES_PASSWORD: mypassword
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
11
frontend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
113
frontend/REACT.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Vontor CZ
|
||||||
|
|
||||||
|
Welcome to **Vontor CZ**!
|
||||||
|
|
||||||
|
## frontend Folder Overview
|
||||||
|
|
||||||
|
The `frontend` folder contains all code and assets for the client-side React application. Its structure typically includes:
|
||||||
|
|
||||||
|
- **src/**
|
||||||
|
Tady se ukladají věci, které uživateli se nepředavají přímo spíš takové stavební kostky.
|
||||||
|
- **api/**
|
||||||
|
TypeScript/JS soubory které se starají o API a o JWT tokeny.
|
||||||
|
Čistě pracují s backendem
|
||||||
|
- **context/**
|
||||||
|
Kontext si načte data které mu předáš a můžeš si je předávat mezi komponenty rychleji.
|
||||||
|
- **hooks/**
|
||||||
|
Pracuje s API a formátují to do výstupu který potřebujeme.
|
||||||
|
|
||||||
|
- **components/**
|
||||||
|
Konktrétní komponenty které se vykreslují na stránce.
|
||||||
|
|
||||||
|
Už využívají už hooky a contexty pro vykreslení informaci bez složite logiky (ať je komponenta hezky čistá a né moc obsáhla).
|
||||||
|
|
||||||
|
- **features/**
|
||||||
|
Nejsou to celé stránky, ale hotové komponenty.
|
||||||
|
|
||||||
|
Obsahuje všechny komponenty plus její hooky, API, state a utils potřebné pro jednu konkrétní funkcionalitu aplikace. (použijí se jenom jednou)
|
||||||
|
|
||||||
|
Features zajišťují modularitu a přehlednost aplikace.
|
||||||
|
|
||||||
|
Příklad: komponenta košík, která zahrnuje API volání, state management a UI komponenty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **layouts/**
|
||||||
|
Tohle je jenom komponenta, která načte další komponenty, ale používá se jako layout kde jsou například navigace footer a ostatní se opakující prvky stránky, ale hlavní obsah ještě není součastí! Ten se načte skrz <outlet/> v pages.
|
||||||
|
- **pages/**
|
||||||
|
Tady se jde do finále tady se vkládají samostatné komponenty a tvoří se už hlavní obsah stránky a vkládají se komponenty a tvoří se logika mezi ně.
|
||||||
|
- **routes/**
|
||||||
|
tady se ukládají routy které například zabraňuji načtení stránky nepříhlášeným uživatelům nebo jenom pro ty s určitou roli/oprávněním... tyhle route komponenty se pak využívají v
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **assets/**
|
||||||
|
Obrázky, fonty, a ostatní statické soubory.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
- **App.tsx**
|
||||||
|
Root komponenta pro aplikaci, kde se nastavují routy pro jednotlivé stránky.
|
||||||
|
Pozor nemyslím ty routy ze složky routes/ ... to jsou jenom obaly pro konktrétní routy pro jednotlivé stránky.
|
||||||
|
- **main.tsx**
|
||||||
|
Vstupní bod pro načítaní aplikace (načíta se první komponenta App.jsx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **utils/**
|
||||||
|
Sběrné místo pro pomocné funkce, které nejsou přímo komponenty nebo hooky, ale jsou znovupoužitelné napříč aplikací.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **index.css**
|
||||||
|
globální styly
|
||||||
|
- **App.css**
|
||||||
|
obsahuje stylovaní layoutu, navigace, footer, error okna atd. takové věci které zůstavjí vždy stejně
|
||||||
|
|
||||||
|
- **public/**
|
||||||
|
Složka public obsahuje statické soubory dostupné přímo přes URL (např. index.html, favicon, obrázky), které React přímo nereenderuje ani neoptimalizuje.
|
||||||
|
|
||||||
|
- **package.json**
|
||||||
|
něco jak requirements.txt v pythonu
|
||||||
|
|
||||||
|
- **vite.config.js / vite.config.ts**
|
||||||
|
Vite konfigurace pro building a serving frontend aplikace.
|
||||||
|
|
||||||
|
- **Dockerfile**
|
||||||
|
Konfigurace pro Docker
|
||||||
|
|
||||||
|
## Getting Started :3
|
||||||
|
|
||||||
|
1. **Instalace balíčku (bere z package.json):**
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start dev server:**
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Build pro produkci(finále):**
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Preview production build:**
|
||||||
|
```sh
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a New React Project with Vite
|
||||||
|
|
||||||
|
If you want to start a new project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create vite@latest
|
||||||
|
# Choose 'react' or 'react-ts' for TypeScript
|
||||||
|
cd your-project
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
23
frontend/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3452
frontend/package-lock.json
generated
Normal file
32
frontend/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-router": "^5.1.20",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@types/axios": "^0.9.36",
|
||||||
|
"@types/react": "^19.1.10",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"eslint": "^9.33.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.39.1",
|
||||||
|
"vite": "^7.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/public/PUBLIC.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
# Anything you import in JS/CSS → src/assets/
|
||||||
|
|
||||||
|
# Anything you reference directly in HTML → public/ something like: <img src="/images/foo.png">
|
||||||
0
frontend/public/favicon.ico
Normal file
42
frontend/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
10
frontend/src/App.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
/* */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
202
frontend/src/api/axios.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_URL: string = `${import.meta.env.VITE_BACKEND_URL}/api`;
|
||||||
|
|
||||||
|
// Axios instance, můžeme používat místo globálního axios
|
||||||
|
const axios_instance = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
withCredentials: true, // potřebné pro cookies
|
||||||
|
});
|
||||||
|
axios_instance.defaults.xsrfCookieName = "csrftoken";
|
||||||
|
axios_instance.defaults.xsrfHeaderName = "X-CSRFToken";
|
||||||
|
|
||||||
|
export default axios_instance;
|
||||||
|
|
||||||
|
// 🔐 Axios response interceptor: automatická obnova při 401
|
||||||
|
axios_instance.interceptors.request.use((config) => {
|
||||||
|
const getCookie = (name: string): string | null => {
|
||||||
|
let cookieValue: string | null = null;
|
||||||
|
if (document.cookie && document.cookie !== "") {
|
||||||
|
const cookies = document.cookie.split(";");
|
||||||
|
for (let cookie of cookies) {
|
||||||
|
cookie = cookie.trim();
|
||||||
|
if (cookie.startsWith(name + "=")) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrfToken = getCookie("csrftoken");
|
||||||
|
if (csrfToken && config.method && ["post", "put", "patch", "delete"].includes(config.method)) {
|
||||||
|
if (!config.headers) config.headers = {};
|
||||||
|
config.headers["X-CSRFToken"] = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Přidej globální response interceptor pro redirect na login při 401 s detail hláškou
|
||||||
|
axios_instance.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status === 401 &&
|
||||||
|
error.response.data &&
|
||||||
|
error.response.data.detail === "Nebyly zadány přihlašovací údaje."
|
||||||
|
) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔄 Obnova access tokenu pomocí refresh cookie
|
||||||
|
export const refreshAccessToken = async (): Promise<{ access: string; refresh: string } | null> => {
|
||||||
|
try {
|
||||||
|
const res = await axios_instance.post(`/account/token/refresh/`);
|
||||||
|
return res.data as { access: string; refresh: string };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Token refresh failed", err);
|
||||||
|
await logout();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Přihlášení
|
||||||
|
export const login = async (username: string, password: string): Promise<any> => {
|
||||||
|
await logout();
|
||||||
|
try {
|
||||||
|
const response = await axios_instance.post(`/account/token/`, { username, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response) {
|
||||||
|
// Server responded with a status code outside 2xx
|
||||||
|
console.log('Login error status:', err.response.status);
|
||||||
|
} else if (err.request) {
|
||||||
|
// Request was made but no response received
|
||||||
|
console.log('Login network error:', err.request);
|
||||||
|
} else {
|
||||||
|
// Something else happened
|
||||||
|
console.log('Login setup error:', err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ❌ Odhlášení s CSRF tokenem
|
||||||
|
export const logout = async (): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const getCookie = (name: string): string | null => {
|
||||||
|
let cookieValue: string | null = null;
|
||||||
|
if (document.cookie && document.cookie !== "") {
|
||||||
|
const cookies = document.cookie.split(";");
|
||||||
|
for (let cookie of cookies) {
|
||||||
|
cookie = cookie.trim();
|
||||||
|
if (cookie.startsWith(name + "=")) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrfToken = getCookie("csrftoken");
|
||||||
|
|
||||||
|
const response = await axios_instance.post(
|
||||||
|
"/account/logout/",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": csrfToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response.data);
|
||||||
|
return response.data; // např. { detail: "Logout successful" }
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Logout failed", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 📡 Obecný request pro API
|
||||||
|
*
|
||||||
|
* @param method - HTTP metoda (např. "get", "post", "put", "patch", "delete")
|
||||||
|
* @param endpoint - API endpoint (např. "/api/service-tickets/")
|
||||||
|
* @param data - data pro POST/PUT/DELETE requesty
|
||||||
|
* @param config - další konfigurace pro axios request
|
||||||
|
* @returns Promise<any> - vrací data z odpovědi
|
||||||
|
*/
|
||||||
|
export const apiRequest = async (
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
data: Record<string, any> = {},
|
||||||
|
config: Record<string, any> = {}
|
||||||
|
): Promise<any> => {
|
||||||
|
const url = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios_instance({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: ["post", "put", "patch"].includes(method.toLowerCase()) ? data : undefined,
|
||||||
|
params: ["get", "delete"].includes(method.toLowerCase()) ? data : undefined,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response) {
|
||||||
|
// Server odpověděl s kódem mimo rozsah 2xx
|
||||||
|
console.error("API Error:", {
|
||||||
|
status: err.response.status,
|
||||||
|
data: err.response.data,
|
||||||
|
headers: err.response.headers,
|
||||||
|
});
|
||||||
|
} else if (err.request) {
|
||||||
|
// Request byl odeslán, ale nedošla odpověď
|
||||||
|
console.error("No response received:", err.request);
|
||||||
|
} else {
|
||||||
|
// Něco jiného se pokazilo při sestavování requestu
|
||||||
|
console.error("Request setup error:", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 👤 Funkce pro získání aktuálně přihlášeného uživatele
|
||||||
|
export async function getCurrentUser(): Promise<any> {
|
||||||
|
const response = await axios_instance.get(`${API_URL}/account/user/me/`);
|
||||||
|
return response.data; // vrací data uživatele
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔒 ✔️ Jednoduchá funkce, která kontroluje přihlášení - můžeš to upravit dle potřeby
|
||||||
|
export async function isAuthenticated(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
return user != null;
|
||||||
|
} catch (err) {
|
||||||
|
return false; // pokud padne 401, není přihlášen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { axios_instance, API_URL };
|
||||||
26
frontend/src/api/external.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a general external API call using axios.
|
||||||
|
*
|
||||||
|
* @param url - The full URL of the external API endpoint.
|
||||||
|
* @param method - HTTP method (GET, POST, PUT, PATCH, DELETE, etc.).
|
||||||
|
* @param data - Request body data (for POST, PUT, PATCH). Optional.
|
||||||
|
* @param config - Additional Axios request config (headers, params, etc.). Optional.
|
||||||
|
* @returns Promise resolving to AxiosResponse<any>.
|
||||||
|
*
|
||||||
|
* @example externalApiCall("https://api.example.com/data", "post", { foo: "bar" }, { headers: { Authorization: "Bearer token" } })
|
||||||
|
*/
|
||||||
|
export async function externalApiCall(
|
||||||
|
url: string,
|
||||||
|
method: AxiosRequestConfig["method"],
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<AxiosResponse<any>> {
|
||||||
|
return axios({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
41
frontend/src/api/get_chocies.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { apiRequest } from "./axios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads enum values from an OpenAPI schema for a given path, method, and field (e.g., category).
|
||||||
|
*
|
||||||
|
* @param path - API path, e.g., "/api/service-tickets/"
|
||||||
|
* @param method - HTTP method
|
||||||
|
* @param field - field name in parameters or request
|
||||||
|
* @param schemaUrl - URL of the JSON schema, default "/api/schema/?format=json"
|
||||||
|
* @returns Promise<Array<{ value: string; label: string }>>
|
||||||
|
*/
|
||||||
|
export async function fetchEnumFromSchemaJson(
|
||||||
|
path: string,
|
||||||
|
method: "get" | "post" | "patch" | "put",
|
||||||
|
field: string,
|
||||||
|
schemaUrl: string = "/schema/?format=json"
|
||||||
|
): Promise<Array<{ value: string; label: string }>> {
|
||||||
|
try {
|
||||||
|
const schema = await apiRequest("get", schemaUrl);
|
||||||
|
|
||||||
|
const methodDef = schema.paths?.[path]?.[method];
|
||||||
|
if (!methodDef) {
|
||||||
|
throw new Error(`Method ${method.toUpperCase()} for ${path} not found in schema.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in "parameters" (e.g., GET query parameters)
|
||||||
|
const param = methodDef.parameters?.find((p: any) => p.name === field);
|
||||||
|
|
||||||
|
if (param?.schema?.enum) {
|
||||||
|
return param.schema.enum.map((val: string) => ({
|
||||||
|
value: val,
|
||||||
|
label: val,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Field '${field}' does not contain enum`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading enum values:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/src/assets/fonts/windows-98/ms_sans_serif.woff
Normal file
BIN
frontend/src/assets/fonts/windows-98/ms_sans_serif.woff2
Normal file
BIN
frontend/src/assets/fonts/windows-98/ms_sans_serif_bold.woff
Normal file
BIN
frontend/src/assets/fonts/windows-98/ms_sans_serif_bold.woff2
Normal file
BIN
frontend/src/assets/img/cursor/Sata.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
frontend/src/assets/img/cursor/omagad.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
frontend/src/assets/img/cursor/pointing(inactive).png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src/assets/img/cursor/pointing.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src/assets/img/cursor/pointing2.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/src/assets/img/errors/403.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
frontend/src/assets/img/errors/404.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
frontend/src/assets/img/errors/500.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
frontend/src/assets/img/errors/error_icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/src/assets/img/job.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
24
frontend/src/assets/img/svg/default-background.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<svg viewBox="0 0 640 360" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- sky -->
|
||||||
|
<rect x="0" y="0" width="640" height="360" fill="#A9A9A9"/>
|
||||||
|
<!-- hill -->
|
||||||
|
<path d="M 0 240 Q 320 60 640 240 L 640 360 L 0 360 Z" fill="#696969"/>
|
||||||
|
<!-- trees -->
|
||||||
|
<g fill="#696969" stroke="#696969">
|
||||||
|
<circle cx="100" cy="192" r="26"/>
|
||||||
|
<circle cx="112" cy="176" r="29"/>
|
||||||
|
<circle cx="126" cy="208" r="22"/>
|
||||||
|
<circle cx="496" cy="184" r="32"/>
|
||||||
|
<circle cx="528" cy="168" r="26"/>
|
||||||
|
<circle cx="560" cy="208" r="29"/>
|
||||||
|
</g>
|
||||||
|
<!-- clouds -->
|
||||||
|
<g fill="#696969" stroke="#696969">
|
||||||
|
<circle cx="90" cy="60" r="25"/>
|
||||||
|
<circle cx="130" cy="80" r="30"/>
|
||||||
|
<circle cx="170" cy="50" r="35"/>
|
||||||
|
<circle cx="300" cy="40" r="28"/>
|
||||||
|
<circle cx="340" cy="70" r="32"/>
|
||||||
|
<circle cx="380" cy="60" r="25"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 823 B |
10
frontend/src/assets/img/svg/default-chat.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
|
||||||
|
|
||||||
|
<g class="layer">
|
||||||
|
<title>Layer 1</title>
|
||||||
|
<rect fill="#e0e0e0" height="100" id="svg_3" stroke="#000000" stroke-width="0" width="100" x="0" y="0"/>
|
||||||
|
<path d="m15,23.23l0,0c0,-4.55 3.6,-8.23 8.04,-8.23l3.65,0l0,0l17.54,0l32.88,0c2.13,0 4.18,0.87 5.68,2.41c1.51,1.54 2.35,3.64 2.35,5.82l0,20.57l0,0l0,12.34l0,0c0,4.55 -3.6,8.23 -8.04,8.23l-32.88,0l-22.91,20.93l5.37,-20.93l-3.65,0c-4.44,0 -8.04,-3.68 -8.04,-8.23l0,0l0,-12.34l0,0z" fill="#696969" id="svg_1" stroke="#000000" stroke-width="0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 705 B |
10
frontend/src/assets/img/svg/default-pfp.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="100%" height="100%" fill="#e0e0e0"/>
|
||||||
|
|
||||||
|
<!-- Head -->
|
||||||
|
<circle cx="100" cy="70" r="40" fill="#bdbdbd"/>
|
||||||
|
|
||||||
|
<!-- Shoulders with rounded top edges -->
|
||||||
|
<rect x="40" y="100" width="120" height="80" rx="20" ry="20" fill="#9e9e9e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
17
frontend/src/assets/img/svg/menu-symbol.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 24.75 24.75"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#000000" d="M0,3.875c0-1.104,0.896-2,2-2h20.75c1.104,0,2,0.896,2,2s-0.896,2-2,2H2C0.896,5.875,0,4.979,0,3.875z M22.75,10.375H2
|
||||||
|
c-1.104,0-2,0.896-2,2c0,1.104,0.896,2,2,2h20.75c1.104,0,2-0.896,2-2C24.75,11.271,23.855,10.375,22.75,10.375z M22.75,18.875H2
|
||||||
|
c-1.104,0-2,0.896-2,2s0.896,2,2,2h20.75c1.104,0,2-0.896,2-2S23.855,18.875,22.75,18.875z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 722 B |
BIN
frontend/src/assets/img/test.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
11
frontend/src/assets/robots.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Sitemap: https://vontor.com/sitemap.xml
|
||||||
|
Disallow: /admin/
|
||||||
|
Allow: /
|
||||||
|
Allow: /social/public/
|
||||||
|
Allow: /social/post/
|
||||||
|
Allow: /social/community/
|
||||||
|
Allow: /social/main/
|
||||||
|
Allow: /social/profile/
|
||||||
|
Allow: /social/login/
|
||||||
|
Allow: /social/register/
|
||||||
|
Crawl-delay: 10
|
||||||
28
frontend/src/components/Footer/footer.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<footer id="contacts">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>vontor.cz</h1>
|
||||||
|
</div>
|
||||||
|
<address>
|
||||||
|
Written by <b>David Bruno Vontor</b><br>
|
||||||
|
<p>Tel.: <a href="tel:+420 605 512 624"><u>+420 605 512 624</u></a></p>
|
||||||
|
<p>E-mail: <a href="mailto:brunovontor@gmail.com"><u>brunovontor@gmail.com</u></a></p>
|
||||||
|
<p>IČO: <a href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"><u>21613109</u></a></p>
|
||||||
|
</address>
|
||||||
|
<div class="contacts">
|
||||||
|
<a href="https://github.com/Brunobrno">
|
||||||
|
<i class="fa fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/brunovontor/">
|
||||||
|
<i class="fa fa-instagram"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/BVontor">
|
||||||
|
<i class="fa-brands fa-x-twitter"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://steamcommunity.com/id/Brunobrno/">
|
||||||
|
<i class="fa-brands fa-steam"></i>
|
||||||
|
</a>
|
||||||
|
<a href="www.youtube.com/@brunovontor">
|
||||||
|
<i class="fa-brands fa-youtube"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
0
frontend/src/components/Footer/footer.tsx
Normal file
99
frontend/src/components/Forms/ContactMe/ContactMeForm.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*TODO: Implement the contact form functionality*/
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="contact-me">
|
||||||
|
<div class="opening">
|
||||||
|
<i class="fa-solid fa-arrow-pointer" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<form method="post" id="contactme-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ contactme_form }}
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="cover"></div>
|
||||||
|
<div class="triangle"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="{% static 'home/js/global/contact-me.js' %}"></script>
|
||||||
|
|
||||||
|
|
||||||
|
contact-me.js:
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$("#contactme-form").submit(function (event) {
|
||||||
|
event.preventDefault(); // Prevent normal form submission
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/submit-contactme/", // URL of the Django view
|
||||||
|
type: "POST",
|
||||||
|
data: $(this).serialize(), // Serialize form data
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
close_contact();
|
||||||
|
|
||||||
|
$("#contactme-form .success-form-alert").fadeIn();
|
||||||
|
$("#contactme-form")[0].reset(); // Clear the form
|
||||||
|
|
||||||
|
alert("Zpráva odeslaná!")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (response) {
|
||||||
|
alert("Zpráva nebyla odeslaná, zkontrolujte si připojení k internetu nebo naskytl u nás problém :(")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#contactme-form .success-form .close").click(function () {
|
||||||
|
$("#contactme-form .success-form-alert").fadeOut();
|
||||||
|
});
|
||||||
|
|
||||||
|
var opened_contact = false;
|
||||||
|
|
||||||
|
$(document).on("click", ".contact-me .opening", function () {
|
||||||
|
console.log("toggle mail");
|
||||||
|
|
||||||
|
const opening = $(".contact-me .opening");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Check if we're opening or closing
|
||||||
|
if (opened_contact === false) {
|
||||||
|
// Toggle rotation
|
||||||
|
opening.toggleClass('rotate-opening');
|
||||||
|
|
||||||
|
// Wait for the rotation to finish
|
||||||
|
setTimeout(function() {
|
||||||
|
$(".contact-me .content").addClass('content-moveup-index');
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$(".contact-me .content")[0].offsetHeight;
|
||||||
|
|
||||||
|
$(".contact-me .content").addClass('content-moveup');
|
||||||
|
}, 1000); // Small delay to trigger transition
|
||||||
|
|
||||||
|
opened_contact = true;
|
||||||
|
} else {
|
||||||
|
close_contact();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function close_contact(){
|
||||||
|
$(".contact-me .content").removeClass('content-moveup');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$(".contact-me .content").toggleClass('content-moveup-index');
|
||||||
|
$(".contact-me .opening").toggleClass('rotate-opening');
|
||||||
|
}, 700);
|
||||||
|
|
||||||
|
opened_contact = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
140
frontend/src/components/Forms/ContactMe/contact-me.module.css
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
.contact-me {
|
||||||
|
margin: 5em auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
|
||||||
|
background-color: #c8c8c8;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
.contact-me + .mail-box{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-me .opening {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
transform-origin: top;
|
||||||
|
|
||||||
|
padding-top: 4em;
|
||||||
|
|
||||||
|
clip-path: polygon(0 0, 100% 0, 50% 50%);
|
||||||
|
background-color: #d2d2d2;
|
||||||
|
|
||||||
|
transition: all 1s ease;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rotate-opening{
|
||||||
|
background-color: #c8c8c8;
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.contact-me .content {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 1s ease-out;
|
||||||
|
}
|
||||||
|
.content-moveup{
|
||||||
|
transform: translateY(-70%);
|
||||||
|
}
|
||||||
|
.content-moveup-index {
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-me .content form{
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #deefff;
|
||||||
|
padding: 1em;
|
||||||
|
border: 0.5em dashed #88d4ed;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
.contact-me .content form div{
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.contact-me .content form input[type=submit]{
|
||||||
|
margin: auto;
|
||||||
|
border: none;
|
||||||
|
background: #4ca4d5;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-me .content form input[type=text],
|
||||||
|
.contact-me .content form input[type=email],
|
||||||
|
.contact-me .content form textarea{
|
||||||
|
background-color: #bfe8ff;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 0.15em solid #064c7d;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.contact-me .cover {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
clip-path: polygon(0 0, 50% 50%, 100% 0, 100% 100%, 0 100%);
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.contact-me .triangle{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 3;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
clip-path: polygon(100% 0, 0 100%, 100% 100%);
|
||||||
|
background-color: rgb(255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||||
|
50% { transform: translateX(2px) rotate(4deg); }
|
||||||
|
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||||
|
100% { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.contact-me .opening i {
|
||||||
|
color: #797979;
|
||||||
|
font-size: 5em;
|
||||||
|
display: inline-block;
|
||||||
|
animation: 0.4s ease-in-out 2s infinite normal none running shake;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px){
|
||||||
|
.contact-me{
|
||||||
|
aspect-ratio: unset;
|
||||||
|
margin-top: 7ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/src/components/Forms/ContactMe/readme.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
10
frontend/src/components/navbar/navbar.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<nav>
|
||||||
|
<i id="toggle-nav" class="fa-solid fa-bars"></i>
|
||||||
|
<ul>
|
||||||
|
<li id="nav-logo"><span>vontor.cz</span></li>
|
||||||
|
<li><a href="{% url "home" %}">Home</a></li>
|
||||||
|
<li><a href="#portfolio">Portfolio</a></li>
|
||||||
|
<li><a href="#services">Services</a></li>
|
||||||
|
<li><a href="#contactme-form">Contact me</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
0
frontend/src/components/navbar/navbar.tsx
Normal file
83
frontend/src/features/ads/Drone/Drone.jsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{% load static %}
|
||||||
|
<div class="drone only-desktop">
|
||||||
|
|
||||||
|
<video id="drone-video" class="video-background" autoplay muted loop playsinline>
|
||||||
|
<source id="video-source" type="video/mp4">
|
||||||
|
Your browser does not support video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>Letecké snímky dronem</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<h2>Opravnění</h2>
|
||||||
|
|
||||||
|
|
||||||
|
A1, A2, A3 a průkaz na vysílačku!
|
||||||
|
|
||||||
|
Mohu garantovat bezpečný provoz dronu i ve složitějších podmínkách. Mám také možnost žádat o povolení k letu v blízkosti letišť!
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Cena</h2>
|
||||||
|
|
||||||
|
Nabízím letecké záběry dronem <br>za cenu <u>3 000 Kč</u>.
|
||||||
|
|
||||||
|
Pokud se nacházíte v Ostravě, doprava je zdarma. Pro oblasti mimo Ostravu účtuji 10 Kč/km.
|
||||||
|
|
||||||
|
Cena se může odvíjet ještě podle složitosti získaní povolení.*
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Výstup</h2>
|
||||||
|
|
||||||
|
Rád Vám připravím jednoduchý sestřih videa, který můžete rychle použít, nebo Vám mohu poskytnout samotné záběry k vlastní editaci. <br>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div>
|
||||||
|
V případě zájmu mě neváhejte<br><a href="#contacts">kontaktovat!</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<script src="{% static 'home/js/drone.js' %}"></script>
|
||||||
|
</div>
|
||||||
|
<!--<button id="debug-drone">force reload</button>-->
|
||||||
|
|
||||||
|
|
||||||
|
drone.js:
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
function setVideoDroneQuality() {
|
||||||
|
$sourceElement = $("#video-source");
|
||||||
|
|
||||||
|
const videoSources = {
|
||||||
|
fullHD: 'static/home/video/drone-background-video-1080p.mp4', // For desktops (1920x1080)
|
||||||
|
hd: 'static/home/video/drone-background-video-720p.mp4', // For tablets/smaller screens (1280x720)
|
||||||
|
lowRes: 'static/home/video/drone-background-video-480p.mp4' // For mobile devices or low performance (854x480)
|
||||||
|
};
|
||||||
|
|
||||||
|
const screenWidth = $(window).width(); // Get screen width
|
||||||
|
|
||||||
|
// Determine the appropriate video source
|
||||||
|
if (screenWidth >= 1920) {
|
||||||
|
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD);
|
||||||
|
} else if (screenWidth >= 1280) {
|
||||||
|
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd);
|
||||||
|
} else {
|
||||||
|
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the video
|
||||||
|
$('#drone-video')[0].load();
|
||||||
|
|
||||||
|
console.log("video set!");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(1000);
|
||||||
|
|
||||||
|
setVideoDroneQuality();
|
||||||
|
//$("#debug-drone").click(setVideoDroneQuality);
|
||||||
|
});
|
||||||
103
frontend/src/features/ads/Drone/drone.module.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.drone{
|
||||||
|
margin-top: -4em;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.drone .video-background {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
object-fit: cover;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
clip-path: polygon(0 3%, 15% 0, 30% 7%, 42% 3%, 61% 1%, 82% 5%, 100% 1%, 100% 94%, 82% 100%, 65% 96%, 47% 99%, 30% 90%, 14% 98%, 0 94%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.drone article{
|
||||||
|
padding: 5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
border-radius: 2em;
|
||||||
|
padding: 3em;
|
||||||
|
gap: 2em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
.drone article header h1{
|
||||||
|
font-size: 4em;
|
||||||
|
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.drone article header{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.drone article main{
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
font-size: 1em;
|
||||||
|
/* width: 60%; */
|
||||||
|
flex: 2;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
gap: 2em;
|
||||||
|
/* flex-wrap: wrap; */
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
.drone a{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.drone article div{
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px) {
|
||||||
|
.drone article header h1{
|
||||||
|
font-size: 2.3em;
|
||||||
|
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
.drone article header{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drone article main{
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.drone article{
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.drone article div{
|
||||||
|
margin: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.drone video{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/src/features/ads/Drone/readme.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
50
frontend/src/features/ads/Portfolio/Portfolio.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% load static %}
|
||||||
|
<div class="portfolio" id="portfolio">
|
||||||
|
<header>
|
||||||
|
<h1>Portfolio</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="door"><i class="fa-solid fa-arrow-pointer"></i></span>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<a href="https://davo1.cz"><img src="{% static 'home\img\portfolio\DAVO_logo_2024_bile.png' %}" alt="davo1.cz logo"></a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
</main>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<a href="https://perlica.cz"><img src="{% static 'home\img\portfolio\perlica-3.webp' %}" alt="Perlica logo"></a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
</main>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<a href="http://epinger2.cz"><img src="{% static 'home\img\portfolio\logo_epinger.svg' %}" alt="Epinger2 logo"></a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
</main>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="{% static 'home/js/portfolio.js' %}"></script>
|
||||||
|
|
||||||
|
|
||||||
|
portfolio.js:
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
var doorOpen= false;
|
||||||
|
|
||||||
|
$(".door").click(function(){
|
||||||
|
doorOpen = !doorOpen;//převrátí hodnotu
|
||||||
|
|
||||||
|
if ($(".door").hasClass('door-open')){
|
||||||
|
$(".door").removeClass('door-open');
|
||||||
|
}else{
|
||||||
|
$(".door").addClass('door-open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
155
frontend/src/features/ads/Portfolio/Portfolio.module.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
.portfolio {
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10em;
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: center;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio div .door {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #c2a67d;
|
||||||
|
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
|
transform-origin: bottom;
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||||
|
50% { transform: translateX(2px) rotate(4deg); }
|
||||||
|
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||||
|
100% { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.door i{
|
||||||
|
color: #5e5747;
|
||||||
|
font-size: 5em;
|
||||||
|
display: inline-block;
|
||||||
|
animation: shake 0.4s ease-in-out infinite;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio .door-open{
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio>header {
|
||||||
|
width: fit-content;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
top: -4.7em;
|
||||||
|
left: 0;
|
||||||
|
padding: 1em 3em;
|
||||||
|
padding-bottom: 0;
|
||||||
|
background-color: #cdc19c;
|
||||||
|
color: #5e5747;
|
||||||
|
border-top-left-radius: 1em;
|
||||||
|
border-top-right-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio>header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio>header i {
|
||||||
|
font-size: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio article{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.portfolio article::after{
|
||||||
|
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||||
|
content: "";
|
||||||
|
bottom: 0;
|
||||||
|
right: -2em;
|
||||||
|
|
||||||
|
height: 2em;
|
||||||
|
width: 6em;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.portfolio article::before{
|
||||||
|
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: -2em;
|
||||||
|
|
||||||
|
height: 2em;
|
||||||
|
width: 6em;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio article header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.portfolio div {
|
||||||
|
padding: 3em;
|
||||||
|
background-color: #cdc19c;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 5em;
|
||||||
|
|
||||||
|
border-radius: 1em;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio div article {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 0em;
|
||||||
|
background-color: #9c885c;
|
||||||
|
width: 30%;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio div article header a img {
|
||||||
|
padding: 2em 0;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px) {
|
||||||
|
.portfolio div{
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.portfolio div article{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/src/features/ads/Portfolio/readme.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
0
frontend/src/features/auth/LogOut.jsx
Normal file
0
frontend/src/features/auth/LoginForm.jsx
Normal file
68
frontend/src/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
frontend/src/layouts/AuthLayout.jsx
Normal file
0
frontend/src/layouts/HomeLayout.jsx
Normal file
0
frontend/src/layouts/HomeLayout.tsx
Normal file
69
frontend/src/layouts/LAYOUTS.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Layouts in React Router
|
||||||
|
|
||||||
|
## 📌 What is a Layout?
|
||||||
|
A **layout** in React Router is just a **React component** that wraps multiple pages with shared structure or styling (e.g., header, footer, sidebar).
|
||||||
|
|
||||||
|
Layouts usually contain:
|
||||||
|
- Global UI elements (navigation, footer, etc.)
|
||||||
|
- An `<Outlet />` component where nested routes will render their content
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Folder Structure Example
|
||||||
|
|
||||||
|
src/
|
||||||
|
layouts/
|
||||||
|
├── MainLayout.jsx
|
||||||
|
└── AdminLayout.jsx
|
||||||
|
pages/
|
||||||
|
├── HomePage.jsx
|
||||||
|
├── AboutPage.jsx
|
||||||
|
└── DashboardPage.jsx
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 How Layouts Are Used in Routes
|
||||||
|
|
||||||
|
### 1. Layout as a Parent Route
|
||||||
|
Use the layout component as the `element` of a **parent route** and place **pages** inside as nested routes.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import MainLayout from "./layouts/MainLayout";
|
||||||
|
import HomePage from "./pages/HomePage";
|
||||||
|
import AboutPage from "./pages/AboutPage";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<MainLayout />}>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/about" element={<AboutPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Inside the MainLayout.jsx
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>Header</header>
|
||||||
|
<main>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
<footer>Footer</footer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
10
frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
265
frontend/src/pages/home/Home.module.css
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Doto:wght@100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Doto:wght@300&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
|
||||||
|
html{
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
font-family: "Exo", serif;
|
||||||
|
|
||||||
|
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doto-font{
|
||||||
|
font-family: "Doto", serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings: "ROND" 0;
|
||||||
|
}
|
||||||
|
.bebas-neue-regular {
|
||||||
|
font-family: "Bebas Neue", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
footer a{
|
||||||
|
color: var(--c-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer a i{
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer{
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
|
||||||
|
background-color: var(--c-boxes);
|
||||||
|
|
||||||
|
margin-top: 2em;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
footer address{
|
||||||
|
padding: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
footer .contacts{
|
||||||
|
font-size: 2em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px){
|
||||||
|
footer{
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--c-text);
|
||||||
|
|
||||||
|
padding-bottom: 10em;
|
||||||
|
margin-top: 6em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
top:0;
|
||||||
|
|
||||||
|
/* gap: 4em;*/
|
||||||
|
}
|
||||||
|
.introduction h1{
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article {
|
||||||
|
/*background-color: cadetblue;*/
|
||||||
|
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article header {}
|
||||||
|
|
||||||
|
.introduction article:nth-child(1) {
|
||||||
|
width: 100%;
|
||||||
|
/* transform: rotate(5deg); */
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.introduction article:nth-child(2) {
|
||||||
|
width: 50%;
|
||||||
|
transform: rotate(3deg);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article:nth-child(3) {
|
||||||
|
width: 50%;
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
align-self: flex-start;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.animation-introduction{
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/*overflow: hidden; ZAPNOUT KDYŽ NECHCEŠ ANIMACI PŘECHÁZET DO OSTATNÍCH DIVŮ*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li{
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
list-style: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255, 255, 255, 35%);
|
||||||
|
animation: animate 4s linear infinite;
|
||||||
|
bottom: -150px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(1){
|
||||||
|
left: 25%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(2){
|
||||||
|
left: 10%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-duration: 12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(3){
|
||||||
|
left: 70%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(4){
|
||||||
|
left: 40%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-duration: 18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(5){
|
||||||
|
left: 65%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(6){
|
||||||
|
left: 75%;
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(7){
|
||||||
|
left: 35%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
animation-delay: 7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(8){
|
||||||
|
left: 50%;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
animation-delay: 15s;
|
||||||
|
animation-duration: 45s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(9){
|
||||||
|
left: 20%;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-duration: 35s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(10){
|
||||||
|
left: 85%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-duration: 11s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes animate {
|
||||||
|
|
||||||
|
0%{
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100%{
|
||||||
|
transform: translateY(-1000px) rotate(720deg);
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px) {
|
||||||
|
.animation-introduction ul li:nth-child(6){
|
||||||
|
left: 67%;
|
||||||
|
}
|
||||||
|
.animation-introduction ul li:nth-child(10) {
|
||||||
|
left: 60%;
|
||||||
|
}
|
||||||
|
.introduction {
|
||||||
|
margin: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article {
|
||||||
|
width: auto !important;
|
||||||
|
transform: none !important;
|
||||||
|
align-self: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
frontend/src/pages/home/HomeNav.module.css
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
nav{
|
||||||
|
padding: 1.1em;
|
||||||
|
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0; /* required */
|
||||||
|
|
||||||
|
transition: top 1s ease-in-out, border-radius 1s ease-in-out;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
z-index: 5;
|
||||||
|
padding-left: 2em;
|
||||||
|
padding-right: 2em;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
background: var(--c-boxes);
|
||||||
|
/*background: -moz-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||||
|
background: -webkit-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||||
|
background: linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#222222",endColorstr="#3b3636",GradientType=1);*/
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
border-radius: 2em;
|
||||||
|
}
|
||||||
|
nav.isSticky-nav{
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
nav ul #nav-logo{
|
||||||
|
border-right: 0.2em solid var(--c-lines);
|
||||||
|
}
|
||||||
|
nav ul #nav-logo span{
|
||||||
|
line-height: 0.75;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
nav a{
|
||||||
|
color: #fff;
|
||||||
|
transition: color 1s;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover{
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
nav a:hover::before {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li {
|
||||||
|
display: inline;
|
||||||
|
padding: 0 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
#toggle-nav{
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
-webkit-transition: transform 0.5s ease;
|
||||||
|
-moz-transition: transform 0.5s ease;
|
||||||
|
-o-transition: transform 0.5s ease;
|
||||||
|
-ms-transition: transform 0.5s ease;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
.toggle-nav-rotated {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
.nav-open{
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 990px){
|
||||||
|
#toggle-nav{
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-left: 0.75em;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
nav{
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 1em;
|
||||||
|
border-bottom-right-radius: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
nav ul {
|
||||||
|
margin-top: 1em;
|
||||||
|
gap: 2em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
-webkit-transition: max-height 1s ease;
|
||||||
|
-moz-transition: max-height 1s ease;
|
||||||
|
-o-transition: max-height 1s ease;
|
||||||
|
-ms-transition: max-height 1s ease;
|
||||||
|
transition: max-height 1s ease;
|
||||||
|
|
||||||
|
max-height: 2em;
|
||||||
|
}
|
||||||
|
nav ul:last-child{
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
nav ul #nav-logo {
|
||||||
|
margin: auto;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
margin-bottom: -1em;
|
||||||
|
border-bottom: 0.2em solid var(--c-lines);
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
frontend/src/pages/home/home.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Home.module.css";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h1 className={styles.title}>Vítejte na hlavní stránce</h1>
|
||||||
|
<p>Toto je obsah jen pro home page.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
$("body").click(function(event){
|
||||||
|
var randomId = "spark-" + Math.floor(Math.random() * 100000);
|
||||||
|
var $spark = $("<div>").addClass("spark-cursor").attr("id", randomId);
|
||||||
|
$("body").append($spark);
|
||||||
|
|
||||||
|
// Nastavení pozice
|
||||||
|
$spark.css({
|
||||||
|
"top": event.pageY + "px",
|
||||||
|
"left": event.pageX + "px",
|
||||||
|
"filter": "hue-rotate(" + Math.random() * 360 + "deg)"
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let index = 0; index < 8; index++) {
|
||||||
|
let $span = $("<span>");
|
||||||
|
$span.css("transform", 'rotate(' + (index * 45) +"deg)" );
|
||||||
|
$spark.append($span);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
$spark.find("span").addClass("animate");
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
$("#" + randomId).remove();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
192
frontend/src/pages/home/introduction.css
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
.introduction {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--c-text);
|
||||||
|
|
||||||
|
padding-bottom: 10em;
|
||||||
|
margin-top: 6em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
top:0;
|
||||||
|
|
||||||
|
/* gap: 4em;*/
|
||||||
|
}
|
||||||
|
.introduction h1{
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article {
|
||||||
|
/*background-color: cadetblue;*/
|
||||||
|
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article header {}
|
||||||
|
|
||||||
|
.introduction article:nth-child(1) {
|
||||||
|
width: 100%;
|
||||||
|
/* transform: rotate(5deg); */
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.introduction article:nth-child(2) {
|
||||||
|
width: 50%;
|
||||||
|
transform: rotate(3deg);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article:nth-child(3) {
|
||||||
|
width: 50%;
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
align-self: flex-start;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.animation-introduction{
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/*overflow: hidden; ZAPNOUT KDYŽ NECHCEŠ ANIMACI PŘECHÁZET DO OSTATNÍCH DIVŮ*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li{
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
list-style: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255, 255, 255, 35%);
|
||||||
|
animation: animate 4s linear infinite;
|
||||||
|
bottom: -150px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(1){
|
||||||
|
left: 25%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(2){
|
||||||
|
left: 10%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-duration: 12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(3){
|
||||||
|
left: 70%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(4){
|
||||||
|
left: 40%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-duration: 18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(5){
|
||||||
|
left: 65%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(6){
|
||||||
|
left: 75%;
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(7){
|
||||||
|
left: 35%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
animation-delay: 7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(8){
|
||||||
|
left: 50%;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
animation-delay: 15s;
|
||||||
|
animation-duration: 45s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(9){
|
||||||
|
left: 20%;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-duration: 35s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-introduction ul li:nth-child(10){
|
||||||
|
left: 85%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-duration: 11s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes animate {
|
||||||
|
|
||||||
|
0%{
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100%{
|
||||||
|
transform: translateY(-1000px) rotate(720deg);
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px) {
|
||||||
|
.animation-introduction ul li:nth-child(6){
|
||||||
|
left: 67%;
|
||||||
|
}
|
||||||
|
.animation-introduction ul li:nth-child(10) {
|
||||||
|
left: 60%;
|
||||||
|
}
|
||||||
|
.introduction {
|
||||||
|
margin: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introduction article {
|
||||||
|
width: auto !important;
|
||||||
|
transform: none !important;
|
||||||
|
align-self: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
frontend/src/pages/home/jquery-3.7.1.js
vendored
Normal file
17
frontend/src/pages/home/nav.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
const $stickyElm = $('nav');
|
||||||
|
const stickyOffset = $stickyElm.offset().top;
|
||||||
|
|
||||||
|
$(window).on('scroll', function() {
|
||||||
|
|
||||||
|
const isSticky = $(window).scrollTop() > stickyOffset;
|
||||||
|
//console.log("sticky: " + isSticky);
|
||||||
|
|
||||||
|
$stickyElm.toggleClass('isSticky-nav', isSticky);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#toggle-nav').click(function () {
|
||||||
|
$('nav ul').toggleClass('nav-open');
|
||||||
|
$('#toggle-nav').toggleClass('toggle-nav-rotated');
|
||||||
|
});
|
||||||
|
});
|
||||||
76
frontend/src/pages/home/reset.module.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*https://www.joshwcomeau.com/css/custom-css-reset/*/
|
||||||
|
|
||||||
|
|
||||||
|
/* 1. Use a more-intuitive box-sizing model */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Remove default margin */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* 3. Add accessible line-height */
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 4. Improve text rendering */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
ul, li{
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Improve media defaults */
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Inherit fonts for form controls */
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7. Avoid text overflows */
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
padding-bottom: 0.5ch;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8. Improve line wrapping */
|
||||||
|
p {
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
9. Create a root stacking context
|
||||||
|
*/
|
||||||
|
#root,
|
||||||
|
#__next {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
0
frontend/src/routes/AuthenticatedRoute.tsx
Normal file
71
frontend/src/routes/ROUTES.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Routes Folder
|
||||||
|
|
||||||
|
This folder contains the route definitions and components used to manage routing in the React application. It includes public and private routes, as well as nested layouts.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
routes/
|
||||||
|
├── PrivateRoute.jsx
|
||||||
|
├── AppRoutes.jsx
|
||||||
|
└── index.js
|
||||||
|
|
||||||
|
|
||||||
|
### `PrivateRoute.jsx`
|
||||||
|
|
||||||
|
`PrivateRoute` is a wrapper component that restricts access to certain routes based on the user's authentication status. Only logged-in users can access routes wrapped inside `PrivateRoute`.
|
||||||
|
|
||||||
|
#### Example Usage
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
|
import { useAuth } from "../auth"; // custom hook to get auth status
|
||||||
|
|
||||||
|
const PrivateRoute = () => {
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
|
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
|
```
|
||||||
|
|
||||||
|
` <Outlet /> ` allows nested routes to be rendered inside the PrivateRoute.
|
||||||
|
|
||||||
|
### AppRoutes.jsx
|
||||||
|
|
||||||
|
This file contains all the route definitions example of the app. It can use layouts from the layouts folder to wrap sections of the app.
|
||||||
|
|
||||||
|
Example Usage
|
||||||
|
```jsx
|
||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import PrivateRoute from "./PrivateRoute";
|
||||||
|
|
||||||
|
// Layouts
|
||||||
|
import MainLayout from "../layouts/MainLayout";
|
||||||
|
import AuthLayout from "../layouts/AuthLayout";
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
import Dashboard from "../pages/Dashboard";
|
||||||
|
import Profile from "../pages/Profile";
|
||||||
|
import Login from "../pages/Login";
|
||||||
|
|
||||||
|
const AppRoutes = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{/* Public Routes */}
|
||||||
|
<Route element={<AuthLayout />}>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Private Routes */}
|
||||||
|
<Route element={<PrivateRoute />}>
|
||||||
|
<Route element={<MainLayout />}>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
|
```
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||