galery-to-article #1

Merged
Beeebooo merged 4 commits from galery-to-article into master 2025-04-04 11:16:34 +02:00
38 changed files with 1227 additions and 270 deletions

3
.gitignore vendored
View file

@ -3,3 +3,6 @@ node_modules/
.idea/ .idea/
services/backend/db.sqlite3 services/backend/db.sqlite3
__pycache__/ __pycache__/
htmlcov/
media/
.coverage

View file

@ -2,9 +2,10 @@
## Running ## Running
1. `docker compose run --rm frontend npm install` 1. create .env from .env.example
2. `docker compose build` 2. `docker compose run --rm frontend npm install`
3. `docker compose up` 3. `docker compose build`
4. `docker compose up`
## Adding new frontend packages ## Adding new frontend packages
@ -13,3 +14,4 @@
## Adding new backend packages ## Adding new backend packages
`docker compose run --rm backend uv add <package>` `docker compose run --rm backend uv add <package>`
`docker compose build` :)

10
TODO
View file

@ -1,15 +1,13 @@
Nastylovat: Nastylovat:
- footer
- lazyload načítat o něco dříve (neli zrušit alespoň mimo galerii) - lazyload načítat o něco dříve (neli zrušit alespoň mimo galerii)
- mobilní verze
Naprogramovat: Frontend:
- tlačítko "načíst další aktuality" v komponentě News (možná přidat i novou stránku /aktuality
- přidat v galerii zvětsovač na obrázky - přidat v galerii zvětsovač na obrázky
- přidat volání na api - notyfi
Naplánovat: Naplánovat:
- kurzy + cena: vyskakovací okno na rezervaci s jménem lekce? jak to vyřešit s kalendářem? (stránka /rezervace) - kurzy + cena: vyskakovací okno na rezervaci s jménem lekce? jak to vyřešit s kalendářem? (stránka /rezervace)
Backend:
DAVID:
- přidat posílání mailu panu vrchnímu - přidat posílání mailu panu vrchnímu

View file

@ -28,7 +28,11 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = env.str('SECRET_KEY', 'django-insecure-x&7qy$na7*@u_4(izkfaz2yiea9+t(nf&#p9cnlq6x)_)jkacf') SECRET_KEY = env.str('SECRET_KEY', 'django-insecure-x&7qy$na7*@u_4(izkfaz2yiea9+t(nf&#p9cnlq6x)_)jkacf')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = False
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": lambda request: False,
"IS_RUNNING_TESTS": False,
}
DEBUG_TOOLBAR_PANELS = [ DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.history.HistoryPanel', 'debug_toolbar.panels.history.HistoryPanel',
@ -72,6 +76,7 @@ INSTALLED_APPS = [
'tko.apps.TkoConfig', 'tko.apps.TkoConfig',
'debug_toolbar', 'debug_toolbar',
'rest_framework', 'rest_framework',
'post_office',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -115,6 +120,7 @@ DATABASES = {
'default': env.db('DATABASE_URL') 'default': env.db('DATABASE_URL')
} }
EMAIL_BACKEND = 'post_office.EmailBackend'
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@ -152,9 +158,9 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES_DIRS = [ # STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static') # os.path.join(BASE_DIR, 'static')
] # ]
MEDIA_URL = '/media/' MEDIA_URL = '/media/'

View file

@ -21,7 +21,7 @@ from debug_toolbar.toolbar import debug_toolbar_urls
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
from tko.views import ContactView, NewArticleListView, AllArticleListView, EventListView from tko.views import ContactView, NewArticleListView, AllArticleListView, EventListView, GalleryView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -29,6 +29,7 @@ urlpatterns = [
path('load-articles/', NewArticleListView.as_view(), name='load-articles'), path('load-articles/', NewArticleListView.as_view(), name='load-articles'),
path('load-all-articles/', AllArticleListView.as_view(), name='load-all-articles'), path('load-all-articles/', AllArticleListView.as_view(), name='load-all-articles'),
path('load-events/', EventListView.as_view(), name='load-events'), path('load-events/', EventListView.as_view(), name='load-events'),
path('load-gallery/', GalleryView.as_view(), name='load-gallery'),
] + debug_toolbar_urls() ] + debug_toolbar_urls()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -4,7 +4,8 @@ version = "0.1.0"
description = "Add your description here" description = "Add your description here"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [ authors = [
{name = "Jakub Kropáček", email = "kropikuba@gmail.com"} {name = "Jakub Kropáček", email = "kropikuba@gmail.com"},
{name = "Nikola Kubeczková", email = "kubeczkova.n@gmail.com"}
] ]
dependencies = [ dependencies = [
"django>=5.1.5", "django>=5.1.5",
@ -15,6 +16,10 @@ dependencies = [
"django-environ>=0.12.0", "django-environ>=0.12.0",
"wait-for-it>=2.3.0", "wait-for-it>=2.3.0",
"django-cors-headers>=4.7.0", "django-cors-headers>=4.7.0",
"django-post-office>=3.9.1",
"pytest-django>=4.10.0",
"factory-boy>=3.3.3",
"pytest-cov>=6.0.0",
] ]
[tool.uv] [tool.uv]
@ -23,3 +28,6 @@ package = false
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "backend.settings"

View file

@ -19,9 +19,15 @@ class ContactAdmin(admin.ModelAdmin):
return False return False
class ArticleImageInline(admin.TabularInline): # Or admin.StackedInline for a different layout
model = models.ArticleImage
extra = 1
@admin.register(models.Article) @admin.register(models.Article)
class ArticleAdmin(admin.ModelAdmin): class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'date', 'author'] list_display = ['title', 'date', 'author']
inlines = [ArticleImageInline]
@admin.register(models.Event) @admin.register(models.Event)

View file

@ -0,0 +1,32 @@
# Generated by Django 5.1.5 on 2025-03-05 15:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tko', '0004_alter_article_image'),
]
operations = [
migrations.RemoveField(
model_name='article',
name='image',
),
migrations.AlterField(
model_name='article',
name='date',
field=models.DateField(auto_now_add=True),
),
migrations.CreateModel(
name='ArticleImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.FileField(default='default.png', upload_to='images/%Y')),
('is_main', models.BooleanField(default=False)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='tko.article')),
],
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.1.5 on 2025-03-10 16:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tko', '0005_remove_article_image_alter_article_date_articleimage'),
]
operations = [
migrations.RemoveField(
model_name='articleimage',
name='is_main',
),
migrations.AddField(
model_name='article',
name='image',
field=models.FileField(blank=True, default='default.png', upload_to='images/%Y'),
),
migrations.AlterField(
model_name='articleimage',
name='image',
field=models.FileField(upload_to='images/%Y'),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.1.5 on 2025-03-10 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tko', '0006_remove_articleimage_is_main_article_image_and_more'),
]
operations = [
migrations.RemoveField(
model_name='article',
name='image',
),
migrations.AddField(
model_name='articleimage',
name='main',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='articleimage',
name='image',
field=models.FileField(default='default.png', upload_to='images/%Y'),
),
]

View file

@ -1,6 +1,6 @@
from django.db import models from django.db import models
# Create your models here.
class Contact(models.Model): class Contact(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
email = models.EmailField() email = models.EmailField()
@ -14,16 +14,23 @@ class Contact(models.Model):
class Article(models.Model): class Article(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
content = models.TextField() content = models.TextField()
image = models.FileField(default="default.png", blank=True)
date = models.DateField(auto_now_add=True) date = models.DateField(auto_now_add=True)
author = models.CharField(max_length=100) author = models.CharField(max_length=100)
active_to = models.DateField(null=True, blank=True) # do not show some invitation after this date active_to = models.DateField(null=True, blank=True) # do not show some invitation after this date
def __str__(self): def __str__(self):
return self.title return self.title
class ArticleImage(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='images')
image = models.FileField(upload_to="images/%Y", default="default.png")
main = models.BooleanField(default=False)
def __str__(self):
return f"Image for {self.article.title}, {self.article.date}"
class Event(models.Model): class Event(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
start_date = models.DateTimeField() start_date = models.DateTimeField()

View file

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.timezone import localtime from django.utils.timezone import localtime
from tko.models import Contact, Article, Event from tko.models import Contact, Article, Event, ArticleImage
class ContactSerializer(serializers.ModelSerializer): class ContactSerializer(serializers.ModelSerializer):
@ -10,17 +10,36 @@ class ContactSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class ArticleImageSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ArticleImage
fields = ['image', 'title']
@staticmethod
def get_title(obj):
return obj.article.title
class ArticleListSerializer(serializers.ModelSerializer): class ArticleListSerializer(serializers.ModelSerializer):
date = serializers.SerializerMethodField() date = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
images = ArticleImageSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Article model = Article
fields = ["id", "author", "content", "date", "image", "title"] fields = ["id", "author", "content", "date", "image", "title", "images"]
@staticmethod @staticmethod
def get_date(obj): def get_date(obj):
return obj.date.strftime("%-d. %-m. %Y") return obj.date.strftime("%-d. %-m. %Y")
@staticmethod
def get_image(obj):
main_image = obj.images.order_by("main").first()
return main_image.image.url if main_image else None
class EventListSerializer(serializers.ModelSerializer): class EventListSerializer(serializers.ModelSerializer):
start = serializers.SerializerMethodField() start = serializers.SerializerMethodField()

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

View file

@ -0,0 +1,62 @@
from random import randint
import factory
import datetime
from django.contrib.auth.models import User
from factory.fuzzy import FuzzyDate
from datetime import date
from tko.models import Article, Event, ArticleImage, Contact
class ContactFactory(factory.django.DjangoModelFactory):
class Meta:
model = Contact
name = factory.Faker("name")
email = factory.Faker("email")
phone_number = factory.Faker("phone_number")
content = factory.Faker("text")
class ArticleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker("name")
content = factory.Faker("text")
date = FuzzyDate(datetime.date(2021, 1, 1), date.today())
author = factory.Faker("name")
active_to = FuzzyDate(datetime.date(2021, 1, 1), datetime.date(2025, 1, 1))
class GalleryFactory(factory.django.DjangoModelFactory):
class Meta:
model = ArticleImage
article = factory.SubFactory(ArticleFactory)
image = factory.django.FileField(filename='image.png')
main = factory.Faker("boolean")
class EventFactory(factory.django.DjangoModelFactory):
class Meta:
model = Event
title = factory.Faker("name")
start_date = FuzzyDate(datetime.date(2021, 1, 1), datetime.date(2025, 1, 1))
end_date = factory.LazyAttribute(
lambda obj: obj.start_date + datetime.timedelta(minutes=randint(5, 5 * 24 * 60))
)
color = factory.Faker("color")
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Faker("name")
email = factory.Faker("email")
password = "password"
is_superuser = True

View file

@ -0,0 +1,37 @@
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
from tko.admin import ContactAdmin
from tko.models import Contact
from tko.tests.factories import UserFactory
from django.contrib.admin.sites import site
# Create your tests here.
@pytest.mark.django_db(transaction=True)
class TestAdmin:
url = reverse("admin:index")
client = APIClient()
def test_has_add_permission(self):
user = UserFactory()
admin = ContactAdmin(Contact, site)
request = self.client.get(self.url)
request.user = user
assert admin.has_add_permission(request) is False
def test_has_delete_permission(self):
user = UserFactory()
admin = ContactAdmin(Contact, site)
request = self.client.get(self.url)
request.user = user
assert admin.has_delete_permission(request) is False
def test_has_change_permission(self):
user = UserFactory()
admin = ContactAdmin(Contact, site)
request = self.client.get(self.url)
request.user = user
assert admin.has_change_permission(request) is False

View file

@ -0,0 +1,53 @@
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
from tko.tests.factories import ArticleFactory
@pytest.mark.django_db(transaction=True)
class TestLoadArticle:
client = APIClient()
@staticmethod
def create_article(old, new):
for _ in range(new): ArticleFactory(active_to=None)
for _ in range(old): ArticleFactory()
@pytest.mark.parametrize(
("articles", "length", "result"),
[
((2, 5), 2, 200),
((2, 1), 1, 200),
((5, 0), 0, 200),
((3, 1), 1, 200),
]
)
def test_load_articles(self, articles, length, result):
self.create_article(*articles)
response = self.client.get(reverse("load-articles"))
assert response.status_code == result
assert len(response.data) == length
@pytest.mark.parametrize(
("articles", "length", "result"),
[
((2, 5), 5, 200),
((2, 1), 1, 200),
((5, 0), 0, 200),
((3, 1), 1, 200),
]
)
def test_load_all_articles(self, articles, length, result):
self.create_article(*articles)
response = self.client.get(
path=reverse("load-all-articles")
)
assert response.status_code == result
assert len(response.data) == length
def test_str_article(self):
title = "Test_article"
assert str(ArticleFactory(title=title)) == title

View file

@ -0,0 +1,40 @@
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
from tko.tests.factories import ContactFactory
# Create your tests here.
@pytest.mark.django_db(transaction=True)
class TestContact:
url = reverse("create-contact")
client = APIClient()
@pytest.mark.parametrize(
("name", "email", "phone", "message", "result"),
[
("name", "email@tko.cz", "770707505", "message", 200),
("name", "email@tko.cz", "", "message", 400),
("name", "", "770707505", "message", 400),
("name", "", "", "message", 400),
("name ahoj", "email@tko.cz", "+420770707505", "message skdo nsdkl skd sakdksd", 200),
("", "email", "770707505", "message", 400),
])
def test_create_contact(self, name, email, phone, message, result):
response = self.client.post(
path=self.url,
data={
"name": name,
"email": email,
"phone_number": phone,
"content": message,
}
)
assert response.status_code == result
if result == 200:
assert str(ContactFactory(
name=name,
email=email,
phone_number=phone,
)) == f"{name}, {email}, {phone}"

View file

@ -0,0 +1,36 @@
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
from tko.tests.factories import EventFactory
# Create your tests here.
@pytest.mark.django_db(transaction=True)
class TestEvent:
url = reverse("load-events")
client = APIClient()
@staticmethod
def create_events(length):
EventFactory.create_batch(size=length)
@pytest.mark.parametrize(
("length", "result"),
[
(2, 200),
(6, 200),
]
)
def test_load_events(self, length, result):
self.create_events(length)
response = self.client.get(self.url)
assert response.status_code == result
assert len(response.data) == length
def test_str_article_image(self):
title = "Test_event"
assert str(
EventFactory(title=title)
) == title

View file

@ -0,0 +1,65 @@
import datetime
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
from tko.tests.factories import GalleryFactory, ArticleFactory
# Create your tests here.
@pytest.mark.django_db(transaction=True)
class TestGallery:
url = reverse("load-gallery")
client = APIClient()
@staticmethod
def create_images(old, new):
for _ in range(new):
article = ArticleFactory(active_to=None)
for _ in range(new): GalleryFactory(article=article)
for _ in range(old):
article = ArticleFactory()
for _ in range(old): GalleryFactory(article=article)
@pytest.mark.parametrize(
("gallery", "length", "result"),
[
((2, 5), 29, 200),
((2, 1), 5, 200),
((4, 2), 20, 200),
((3, 1), 10, 200),
]
)
def test_load_gallery(self, gallery, length, result):
self.create_images(*gallery)
response = self.client.get(self.url)
assert response.status_code == result
assert len(response.data) == length
@pytest.mark.parametrize(
("gallery", "length", "result"),
[
((2, 5), 5, 200),
((2, 1), 1, 200),
((4, 0), 0, 200),
((3, 1), 1, 200),
]
)
def test_load_article_images(self, gallery, length, result):
old, new = gallery
self.create_images(*gallery)
response = self.client.get(reverse("load-articles"))
for article in response.data:
assert len(article["images"]) == new
assert response.status_code == result
def test_str_article_image(self):
title = "Test_article"
assert str(
GalleryFactory(article=ArticleFactory(title=title))
) == f"Image for {title}, {datetime.date.today()}"

View file

@ -1,21 +1,37 @@
from django.utils import timezone from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from rest_framework.generics import ListAPIView, CreateAPIView from post_office import mail
from rest_framework import permissions
from tko.models import Article, Event from rest_framework.generics import ListAPIView, CreateAPIView
from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer from rest_framework.response import Response
from tko.models import Article, Event, ArticleImage
from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer, ArticleImageSerializer
class ContactView(CreateAPIView): class ContactView(CreateAPIView):
serializer_class = ContactSerializer serializer_class = ContactSerializer
permission_classes = [permissions.AllowAny]
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
mail.send(
recipients="kubeczkova.n@gmail.com",
subject="Zpráva z webu TKO",
message=f"Jméno: {serializer.data['name']}\n"
f"Email: {serializer.data['email']}\n"
f"Telefón: {serializer.data['phone_number']}\n\n"
f"Zpráva: {serializer.data['content']}",
)
return Response(serializer.data)
class NewArticleListView(ListAPIView): class NewArticleListView(ListAPIView):
serializer_class = ArticleListSerializer serializer_class = ArticleListSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self): def get_queryset(self):
return Article.objects.filter( return Article.objects.filter(
@ -25,7 +41,6 @@ class NewArticleListView(ListAPIView):
class AllArticleListView(ListAPIView): class AllArticleListView(ListAPIView):
serializer_class = ArticleListSerializer serializer_class = ArticleListSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self): def get_queryset(self):
return Article.objects.filter( return Article.objects.filter(
@ -33,7 +48,11 @@ class AllArticleListView(ListAPIView):
).order_by('-date') ).order_by('-date')
class GalleryView(ListAPIView):
queryset = ArticleImage.objects.all().order_by("-article_id", "main")
serializer_class = ArticleImageSerializer
class EventListView(ListAPIView): class EventListView(ListAPIView):
queryset = Event.objects.all() queryset = Event.objects.all()
serializer_class = EventListSerializer serializer_class = EventListSerializer
permission_classes = [permissions.AllowAny]

View file

@ -34,8 +34,12 @@ dependencies = [
{ name = "django-cors-headers" }, { name = "django-cors-headers" },
{ name = "django-debug-toolbar" }, { name = "django-debug-toolbar" },
{ name = "django-environ" }, { name = "django-environ" },
{ name = "django-post-office" },
{ name = "djangorestframework" }, { name = "djangorestframework" },
{ name = "factory-boy" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "pytest-cov" },
{ name = "pytest-django" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
{ name = "wait-for-it" }, { name = "wait-for-it" },
] ]
@ -46,12 +50,33 @@ requires-dist = [
{ name = "django-cors-headers", specifier = ">=4.7.0" }, { name = "django-cors-headers", specifier = ">=4.7.0" },
{ name = "django-debug-toolbar", specifier = ">=5.0.1" }, { name = "django-debug-toolbar", specifier = ">=5.0.1" },
{ name = "django-environ", specifier = ">=0.12.0" }, { name = "django-environ", specifier = ">=0.12.0" },
{ name = "django-post-office", specifier = ">=3.9.1" },
{ name = "djangorestframework", specifier = ">=3.15.2" }, { name = "djangorestframework", specifier = ">=3.15.2" },
{ name = "factory-boy", specifier = ">=3.3.3" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "pytest-django", specifier = ">=4.10.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
{ name = "wait-for-it", specifier = ">=2.3.0" }, { name = "wait-for-it", specifier = ">=2.3.0" },
] ]
[[package]]
name = "bleach"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 },
]
[package.optional-dependencies]
css = [
{ name = "tinycss2" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.1.8"
@ -73,6 +98,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
] ]
[[package]]
name = "coverage"
version = "7.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 },
{ url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 },
{ url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 },
{ url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 },
{ url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 },
{ url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 },
{ url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 },
{ url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 },
{ url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 },
{ url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 },
{ url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 },
{ url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 },
{ url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 },
{ url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 },
{ url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 },
{ url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 },
{ url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 },
{ url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 },
{ url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 },
{ url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 },
{ url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 },
{ url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 },
{ url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 },
{ url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 },
{ url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 },
{ url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 },
{ url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 },
{ url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 },
{ url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 },
{ url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 },
{ url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 },
]
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.5" version = "5.1.5"
@ -122,6 +186,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957 }, { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957 },
] ]
[[package]]
name = "django-post-office"
version = "3.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bleach", extra = ["css"] },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/e9/7369fb453269ded8d2fff58299fe653033702bd15a427bf35353d4606df3/django-post_office-3.9.1.tar.gz", hash = "sha256:3d8f502f7829e4cf83e830d9efd6909bb44690af6bc41c7e4fc5a85d7b04df10", size = 76624 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/12/19fdf17fb1a0d5e4418798cf9ed66b8aee39da564e875ec025627e08abdf/django_post_office-3.9.1-py3-none-any.whl", hash = "sha256:0082c4dd17854f66077ef457cf868c9bc5b6247de99d5755c45ddea1c518ed6f", size = 85671 },
]
[[package]] [[package]]
name = "djangorestframework" name = "djangorestframework"
version = "3.15.2" version = "3.15.2"
@ -134,6 +211,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 },
] ]
[[package]]
name = "factory-boy"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "faker" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036 },
]
[[package]]
name = "faker"
version = "37.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/b77f42021308ec8b134502343da882c0905d725a4d661c7adeaf7acaf515/faker-37.1.0.tar.gz", hash = "sha256:ad9dc66a3b84888b837ca729e85299a96b58fdaef0323ed0baace93c9614af06", size = 1875707 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a1/8936bc8e79af80ca38288dd93ed44ed1f9d63beb25447a4c59e746e01f8d/faker-37.1.0-py3-none-any.whl", hash = "sha256:dc2f730be71cb770e9c715b13374d80dbcee879675121ab51f9683d262ae9a1c", size = 1918783 },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.14.0" version = "0.14.0"
@ -174,6 +275,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
] ]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]] [[package]]
name = "psycopg2-binary" name = "psycopg2-binary"
version = "2.9.10" version = "2.9.10"
@ -205,6 +333,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
] ]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
]
[[package]]
name = "pytest-cov"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
]
[[package]]
name = "pytest-django"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/10/a096573b4b896f18a8390d9dafaffc054c1f613c60bf838300732e538890/pytest_django-4.10.0.tar.gz", hash = "sha256:1091b20ea1491fd04a310fc9aaff4c01b4e8450e3b157687625e16a6b5f3a366", size = 84710 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975 },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.1"
@ -258,6 +426,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 },
] ]
[[package]]
name = "tinycss2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -368,6 +548,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
] ]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "14.2" version = "14.2"

View file

@ -20,7 +20,7 @@ h2 {
font-size: 1.5rem; font-size: 1.5rem;
color: #CF3476; color: #CF3476;
text-align: center; text-align: center;
margin-bottom: 0.5rem; margin-bottom: 2rem;
} }
h3 { h3 {
@ -46,6 +46,17 @@ h5 {
margin-top: 1rem; margin-top: 1rem;
} }
@media (max-width: 600px) {
h1 {
font-size: 2rem; /* Smaller size for mobile */
margin-top: 1.5rem;
}
h2 {
font-size: 1.2rem; /* Slightly smaller h2 for mobile */
}
}
.sheet__box { .sheet__box {
min-width: 25rem; min-width: 25rem;
margin-left: calc(20% - 2.5rem); margin-left: calc(20% - 2.5rem);
@ -95,12 +106,33 @@ h5 {
font-weight: bold; font-weight: bold;
} }
.contact__dialog__title {
font-family: 'Futura', sans-serif;
font-size: 2rem;
color: #333;
margin-bottom: 1rem;
font-weight: bold;
}
.contact__button { .contact__button {
width: 100%; width: 100%;
color: #ffffff; color: #ffffff;
background-color: #CF3476; background-color: #CF3476;
} }
@media (max-width: 600px) {
.v-dialog > .v-card {
border-radius: 0 !important;
height: 100vh;
padding: 1rem;
}
.contact__dialog__title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
}
.articles { .articles {
margin-left: calc(20% - 2.5rem); margin-left: calc(20% - 2.5rem);
margin-right: calc(20% - 2.5rem); margin-right: calc(20% - 2.5rem);
@ -118,6 +150,12 @@ h5 {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.carousel__image img {
border-radius: 0.5rem;
object-fit: contain !important;
padding: 1rem;
}
.article__title { .article__title {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
font-size: 1.5rem; font-size: 1.5rem;
@ -164,35 +202,70 @@ h5 {
} }
.pricing { .pricing {
margin-left: calc(20% - 2.5rem); margin-left: auto;
margin-right: calc(20% - 2.5rem); margin-right: auto;
max-width: 1500px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
} }
.pricing-box { .pricing-box {
padding: 1rem; padding: 1.5rem;
text-align: center; text-align: center;
min-width: 25rem; min-width: 18rem;
border-radius: 1rem; border-radius: 1rem;
height: 230px; height: 100%;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease, box-shadow 0.3s ease;
background: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.pricing-box:hover { .pricing-box:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.8); transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25);
}
.pricing-content {
flex-grow: 1;
}
.pricing-title {
min-height: 3rem;
}
.pricing-desc {
min-height: 3rem;
} }
.pricing__price { .pricing__price {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
font-size: 3.5rem; font-size: 3rem;
font-weight: bold;
color: #CF3476; color: #CF3476;
text-align: center; margin-top: 0.5rem;
} }
.pricing__subtitle { .pricing__subtitle {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
font-size: 1rem; font-size: 1rem;
color: #333; color: #555;
text-align: center; margin-bottom: 0.5rem;
}
@media (max-width: 600px) {
.pricing-box {
min-width: 100%;
}
.pricing-title,
.pricing-desc {
min-height: unset; /* Removes forced height to prevent breakage */
}
} }
.trainers__parallax { .trainers__parallax {
@ -201,12 +274,45 @@ h5 {
} }
.trainers { .trainers {
min-width: 25rem; margin-left: auto;
margin-right: auto;
max-width: 1500px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
height: 100%; height: 100%;
margin-left: calc(20% - 2.5rem); text-align: center;
margin-right: calc(20% - 2.5rem); padding: 2rem;
}
.trainer-avatar {
width: 150px;
height: 150px;
margin: 20px auto;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* Mobile Styles */
@media (max-width: 600px) {
.trainers__parallax {
max-height: 30rem;
}
.trainers {
max-width: 100%;
padding: 1rem;
}
.trainer-avatar {
width: 120px;
height: 120px;
}
} }
.advantage { .advantage {
@ -221,6 +327,7 @@ h5 {
.advantage__title { .advantage__title {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
padding: 0.5rem 0;
color: #CF3476; color: #CF3476;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;
@ -229,17 +336,37 @@ h5 {
.advantage__text { .advantage__text {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
padding-bottom: 1rem;
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
} }
@media (max-width: 600px) {
.advantage {
min-width: 100%;
margin: 0.5rem auto;
padding: 0.5rem;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
.advantage__title {
font-size: 1.3rem;
}
.advantage__text {
font-size: 1rem;
padding: 0 1.5rem;
}
}
.about { .about {
min-width: 25rem;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
height: 100%; height: 100%;
margin-left: calc(20% - 2.5rem); max-width: 80%;
margin-right: calc(20% - 2.5rem); margin: 0 auto;
text-align: center;
padding: 2rem;
} }
.about__parallax { .about__parallax {
@ -247,10 +374,23 @@ h5 {
margin-top: 2rem; margin-top: 2rem;
} }
.history__parallax {
max-height: 34rem;
margin-top: 2rem;
}
@media (max-width: 600px) {
.history__parallax {
max-height: 45rem;
}
}
.about__title { .about__title {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
width: 100%;
display: block;
color: #fff; color: #fff;
margin-top: 1rem; margin-top: 1rem;
font-weight: bold; font-weight: bold;
@ -259,8 +399,10 @@ h5 {
.about__subtitle { .about__subtitle {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
color: #FF3D8C; color: #FF3D8C;
font-size: 1.5rem; font-size: 1.3rem;
text-align: center; text-align: center;
width: 100%;
display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: bold; font-weight: bold;
} }
@ -268,8 +410,29 @@ h5 {
.about__text { .about__text {
font-family: 'Futura', sans-serif; font-family: 'Futura', sans-serif;
color: #ddd; color: #ddd;
font-size: 1.2rem; font-size: 1rem;
text-align: center; text-align: center;
padding: 0 1rem;
}
@media (max-width: 600px) {
.about {
max-width: 100%;
padding: 1rem;
}
.about__title {
font-size: 1.5rem;
}
.about__subtitle {
font-size: 1rem;
}
.about__text {
font-size: 1rem;
padding: 0 10px;
}
} }
.footer { .footer {

View file

@ -6,16 +6,18 @@
:key="advantage.id" :key="advantage.id"
class="advantage" class="advantage"
> >
<v-row> <v-row align="center" class="pa-2">
<v-col cols="12" md="1"> <v-col cols="auto">
<small-icon :icon="advantage.icon" /> <small-icon :icon="advantage.icon" />
</v-col> </v-col>
<v-col> <v-col>
<v-card-title class="advantage__title"> <v-card-title class="advantage__title">
{{advantage.title}} {{ advantage.title }}
</v-card-title> </v-card-title>
</v-col> </v-col>
</v-row> </v-row>
<v-card-text class="advantage__text"> <v-card-text class="advantage__text">
{{ advantage.subtitle }} {{ advantage.subtitle }}
</v-card-text> </v-card-text>

View file

@ -1,87 +1,9 @@
<template> <template>
<v-card class="contact"> <v-card class="contact">
<v-form v-model="valid">
<v-container>
<v-card-title class="contact__title">Kontaktujte nás!</v-card-title> <v-card-title class="contact__title">Kontaktujte nás!</v-card-title>
<v-row> <dialog-contact-form />
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="fullName"
label="Jméno"
variant="underlined"
></v-text-field>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="email"
label="Emailová adresa"
variant="underlined"
></v-text-field>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="phone"
label="Telefonní číslo"
variant="underlined"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-textarea
v-model="textField"
label="Váš dotaz *"
variant="underlined"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
class="contact__button"
type="submit"
@click.prevent="sendContact"
>
Poslat
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</v-card> </v-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import './assets/css/main.css'
import {useAPI} from "~/composables/useAPI";
const valid = ref<boolean>(false);
const fullName = ref<string>("");
const email = ref<string>("");
const phone = ref<string>("");
const textField = ref<string>("");
async function sendContact() {
const { data, error } = await useAPI('create-contact/', {
method: "POST",
body: {
name: fullName.value,
email: email.value,
phone_number: phone.value,
content: textField.value,
}
});
}
</script> </script>

View file

@ -2,24 +2,62 @@
<h1 id="courses">Kurzy</h1> <h1 id="courses">Kurzy</h1>
<h2>Vyberte, co Vám nejlépe vyhovuje</h2> <h2>Vyberte, co Vám nejlépe vyhovuje</h2>
<v-row class="pricing"> <v-row class="pricing">
<v-col <v-col
cols="12" cols="12"
sm="6"
md="4" md="4"
lg="3"
v-for="(course, index) in courses" v-for="(course, index) in courses"
:key="index" :key="index"
> >
<v-card class="pricing-box"> <v-card class="pricing-box" @click="openDialog = true; chosenCourse = course">
<h3>{{ course.name }}</h3> <div class="pricing-content">
<v-card-subtitle><div class="pricing__subtitle">{{ course.time }}</div></v-card-subtitle> <h3 class="pricing-title">{{ course.name }}</h3>
<v-card-title><div class="pricing__price">{{ course.price }}</div></v-card-title> <v-card-subtitle>
<v-card-text v-if="course.desc"><div class="pricing__subtitle">{{ course.desc }}</div></v-card-text> <div class="pricing__subtitle">{{ course.time }}</div>
</v-card-subtitle>
<v-card-title>
<div class="pricing__price">{{ course.price }}</div>
</v-card-title>
<v-card-text v-if="course.desc">
<div class="pricing__subtitle pricing-desc">{{ course.desc }}</div>
</v-card-text>
</div>
<v-btn class="show_more"> <v-btn class="show_more">
Kontaktujte nás Kontaktujte nás
</v-btn> </v-btn>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<template>
<v-dialog
v-model="openDialog"
:fullscreen="$vuetify.display.smAndDown"
scrollable
transition="dialog-bottom-transition"
>
<v-card class="contact pa-4">
<v-card-title class="contact__dialog__title d-flex align-center">
<span class="flex-grow-1 text-center" style="margin-left: 40px;">{{ chosenCourse.name }}</span>
<v-btn icon="mdi-close" variant="text" @click="openDialog = false"></v-btn>
</v-card-title>
<v-card-text v-if="chosenCourse.desc" class="text-center">
<div class="pricing__subtitle text-body-1">{{ chosenCourse.desc }}</div>
</v-card-text>
<v-divider class="my-4"></v-divider>
<v-card-title class="contact__dialog__title text-center">
Kontaktujte nás!
</v-card-title>
<dialog-contact-form class="mt-2" />
</v-card>
</v-dialog>
</template>
<!-- <v-btn to="/kurzy" class="show_more">--> <!-- <v-btn to="/kurzy" class="show_more">-->
<!-- <v-icon icon="mdi-chevron-down"/>Více informací<v-icon icon="mdi-chevron-down"/>--> <!-- <v-icon icon="mdi-chevron-down"/>Více informací<v-icon icon="mdi-chevron-down"/>-->
<!-- </v-btn>--> <!-- </v-btn>-->
@ -27,7 +65,7 @@
<script setup lang="ts"> <script setup lang="ts">
import './assets/css/main.css' import './assets/css/main.css'
// TODO: vyskakovací okno s kontaktním formulářem po rozkliku :) const openDialog = ref<boolean>(false);
const courses = [ const courses = [
{name: "Sportovní taneční klub", time: "", price: "", desc: "Připojte se k našemu tanečnímu klubu a rozvíjejte své taneční dovednosti v přátelském prostředí. Nabízíme různé styly tance pro všechny úrovně."}, {name: "Sportovní taneční klub", time: "", price: "", desc: "Připojte se k našemu tanečnímu klubu a rozvíjejte své taneční dovednosti v přátelském prostředí. Nabízíme různé styly tance pro všechny úrovně."},
@ -38,4 +76,5 @@ const courses = [
{name: "Pronájem sálu", time: "", price: "", desc: "Hledáte ideální prostor pro tanec nebo jinou aktivitu? Náš taneční sál je k dispozici k pronájmu pro Vaše akce."}, {name: "Pronájem sálu", time: "", price: "", desc: "Hledáte ideální prostor pro tanec nebo jinou aktivitu? Náš taneční sál je k dispozici k pronájmu pro Vaše akce."},
] ]
const chosenCourse = ref(courses[0]);
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<v-parallax <v-parallax
class="about__parallax" class="history__parallax"
src="public/img/black-pink.jpg" src="public/img/black-pink.jpg"
> >
<v-container id="about"> <v-container id="about">

View file

@ -17,15 +17,21 @@
aspect-ratio="1" aspect-ratio="1"
cover cover
class="article__image" class="article__image"
@click="showCarousel = true"
> >
<template v-slot:placeholder> <template v-slot:placeholder>
<v-row> <v-row justify="center" align="center" class="fill-height">
<v-progress-circular <v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular>
color="grey-lighten-5"
indeterminate
></v-progress-circular>
</v-row> </v-row>
</template> </template>
<v-dialog v-model="showCarousel">
<dialog-carousel
:image="article.image"
:images="article.images"
:title="article.title"
@isActive="showCarousel = false"
/>
</v-dialog>
</v-img> </v-img>
</v-col> </v-col>
<v-col <v-col
@ -47,13 +53,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import './assets/css/main.css' import './assets/css/main.css'
import { useAPI } from "~/composables/useAPI"; import { useAPI } from "~/composables/useAPI";
const showCarousel = ref(false);
interface ArticleImage {
image: string;
}
interface Article { interface Article {
id: number; id: number;
title: string; title: string;
image: string; image: string;
images: ArticleImage[];
date: string; date: string;
content: string; content: string;
author: string; author: string;
@ -70,5 +82,4 @@ else if (error.value) {
console.error("Error loading articles:", error.value); console.error("Error loading articles:", error.value);
} }
</script> </script>

View file

@ -1,14 +1,22 @@
<template> <template>
<v-parallax <v-parallax class="trainers__parallax" src="public/img/black-pink.jpg">
class="trainers__parallax"
src="public/img/black-pink.jpg"
>
<v-container id="trainers"> <v-container id="trainers">
<v-card class="trainers"> <v-card class="trainers pa-4">
<v-card-title class="about__title">Naši trenéři a lektoři</v-card-title> <v-card-title class="about__title ">Naši trenéři a lektoři</v-card-title>
<v-card-subtitle class="about__subtitle">Seznamte se s námi!</v-card-subtitle> <v-card-subtitle class="about__subtitle ">Seznamte se s námi!</v-card-subtitle>
<v-row>
<v-col class="text-center" cols="12" md="3" v-for="lector in lectors" :key="lector.id"> <v-carousel cycle interval="4000" hide-delimiters show-arrows="hover">
<v-carousel-item v-for="(group, index) in trainerGroups" :key="index">
<v-row justify="center">
<v-col
v-for="(lector, index) in group"
:key="index"
cols="12"
sm="6"
md="4"
lg="3"
class="text-center"
>
<v-avatar <v-avatar
color="none" color="none"
rounded="1" rounded="1"
@ -17,10 +25,12 @@
> >
<v-img :src="lector.img" cover></v-img> <v-img :src="lector.img" cover></v-img>
</v-avatar> </v-avatar>
<v-card-subtitle class="about__subtitle">{{lector.name}}</v-card-subtitle> <v-card-subtitle class="about__subtitle">{{ lector.name }}</v-card-subtitle>
<v-card-text class="about__text">{{ lector.desc }}</v-card-text> <v-card-text class="about__text">{{ lector.desc }}</v-card-text>
</v-col> </v-col>
</v-row> </v-row>
</v-carousel-item>
</v-carousel>
</v-card> </v-card>
</v-container> </v-container>
</v-parallax> </v-parallax>
@ -30,28 +40,38 @@ import './assets/css/main.css'
const lectors = [ const lectors = [
{ {
id: 1,
name: "Ondřej Gilar", name: "Ondřej Gilar",
img: "/trainers/img.png", img: "/trainers/img.png",
desc: "Trenér - latinskoamerické tance a Pro-AM", desc: "Trenér - latinskoamerické tance a Pro-AM",
}, },
{ {
id: 2,
name: "Leona Hruštincová", name: "Leona Hruštincová",
img: "/trainers/img.png", img: "/trainers/img.png",
desc: "Lektorka - tance pro děti", desc: "Lektorka - tance pro děti",
}, },
{ {
id: 1,
name: "Ondřej Gilar", name: "Ondřej Gilar",
img: "/trainers/img.png", img: "/trainers/img.png",
desc: "Trenér - latinskoamerické tance a Pro-AM", desc: "Trenér - latinskoamerické tance a Pro-AM",
}, },
{ {
id: 2, name: "Leona Hruštincová",
img: "/trainers/img.png",
desc: "Lektorka - tance pro děti",
},
{
name: "Leona Hruštincová", name: "Leona Hruštincová",
img: "/trainers/img.png", img: "/trainers/img.png",
desc: "Lektorka - tance pro děti", desc: "Lektorka - tance pro děti",
}, },
] ]
const trainerGroups = computed(() => {
const groups: typeof lectors[] = [];
const perSlide = window.innerWidth >= 1280 ? 4 : window.innerWidth >= 600 ? 2 : 1;
for (let i = 0; i < lectors.length; i += perSlide) {
groups.push(lectors.slice(i, i + perSlide));
}
return groups;
});
</script> </script>

View file

@ -0,0 +1,35 @@
<template>
<v-card class="article__image">
<v-card-title>{{ title }}
<v-icon class="to_right" @click="$emit('isActive', false)">mdi-close</v-icon>
</v-card-title>
<v-carousel
:hide-delimiters="true"
height="90vh"
>
<v-carousel-item
v-for="(item, i) in images"
:key="i"
:src="item.image"
cover
class="carousel__image"
></v-carousel-item>
</v-carousel>
</v-card>
</template>
<script setup lang="ts">
import './assets/css/main.css';
interface ArticleImage {
image: string;
}
defineProps<{
image: string;
images: ArticleImage[];
title: string;
}>();
defineEmits(["isActive"])
</script>

View file

@ -0,0 +1,84 @@
<template>
<v-form v-model="valid">
<v-container>
<v-row>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="fullName"
label="Jméno"
variant="underlined"
></v-text-field>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="email"
label="Emailová adresa"
variant="underlined"
></v-text-field>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="phone"
label="Telefonní číslo"
variant="underlined"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-textarea
v-model="textField"
label="Váš dotaz"
variant="underlined"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
class="contact__button"
type="submit"
@click.prevent="sendContact"
>
Poslat
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</template>
<script setup lang="ts">
import './assets/css/main.css'
import {useAPI} from "~/composables/useAPI";
const valid = ref<boolean>(false);
const fullName = ref<string>("");
const email = ref<string>("");
const phone = ref<string>("");
const textField = ref<string>("");
async function sendContact() {
const { data, error } = await useAPI('create-contact/', {
method: "POST",
body: {
name: fullName.value,
email: email.value,
phone_number: phone.value,
content: textField.value,
}
});
}
</script>

View file

@ -11,15 +11,31 @@
</div> </div>
<v-app-bar-title class="app__title">Taneční klub Ostrava</v-app-bar-title> <v-app-bar-title class="app__title">Taneční klub Ostrava</v-app-bar-title>
<v-tabs <v-tabs v-model="currentTab" class="d-none d-sm-flex app__tab">
v-for="(tab, index) in tabs" :key="index" <div
v-model="currentTab" v-for="(tab, index) in tabs"
align-with-title :key="index"
class="d-none d-sm-flex app__tab"
> >
<v-tab v-if="tab.ref" :text="tab.name" :value="tab.name" @click="useGoTo(tab.ref)"></v-tab> <v-tab
<v-tab v-if="tab.href" :text="tab.name" :value="tab.name" :href="tab.href"></v-tab> v-if="tab.ref"
:text="tab.name"
:value="tab.name"
@click="useGoTo(tab.ref)"
>
{{ tab.name }}
</v-tab>
<v-tab
v-if="tab.href"
:text="tab.name"
:value="tab.name"
:href="tab.href"
>
{{ tab.name }}
</v-tab>
</div>
</v-tabs> </v-tabs>
<!-- <v-col--> <!-- <v-col-->
<!-- class="text-right"--> <!-- class="text-right"-->
<!-- @click="toggleTheme"--> <!-- @click="toggleTheme"-->
@ -29,7 +45,6 @@
</v-app-bar> </v-app-bar>
<v-fab <v-fab
:key="currentTab.ref"
class="ms-4 mb-4" class="ms-4 mb-4"
></v-fab> ></v-fab>
@ -39,10 +54,7 @@
fixed fixed
left left
> >
<v-list <v-list nav dense>
nav
dense
>
<v-list-item v-for="(tab, index) in tabs" :key="index"> <v-list-item v-for="(tab, index) in tabs" :key="index">
<v-list-item-title @click="useGoTo(tab.ref)">{{ tab.name }}</v-list-item-title> <v-list-item-title @click="useGoTo(tab.ref)">{{ tab.name }}</v-list-item-title>
</v-list-item> </v-list-item>
@ -54,18 +66,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGoTo } from "~/composables/useGoTo"; import { useGoTo } from "~/composables/useGoTo";
import {useRoute} from "#vue-router";
const route = useRoute();
const currentTab = ref({name: 'O nás', ref: "#about", href: "o-nas"}); const currentTab = ref({ name: 'O nás', ref: "#about", href: "o-nas" });
const drawer = ref(null) const drawer = ref(null)
const tabs = [
{name: 'O nás', ref: "#about", href: ""},
{name: "Trenéři", ref: "#trainers", href: ""},
{name: 'Kurzy', ref: "#courses", href: ""},
{name: 'Galerie', ref: "", href: "/galerie"},
{name: 'Aktuality', ref: "#article", href: ""},
{name: 'Kontakty', ref: "", href: "/kontakty"},
]
const homeTabs = [
{ name: "O nás", ref: "#about", href: "" },
{ name: "Trenéři", ref: "#trainers", href: "" },
{ name: "Kurzy", ref: "#courses", href: "" },
{ name: "Galerie", ref: "", href: "/galerie" },
{ name: "Aktuality", ref: "#article", href: "" },
{ name: "Kontakty", ref: "", href: "/kontakty" },
];
const otherTabs = [
{ name: "O nás", ref: "", href: "/" },
{ name: "Trenéři", ref: "", href: "/" },
{ name: "Kurzy", ref: "", href: "/" },
{ name: "Galerie", ref: "", href: "/galerie" },
{ name: "Aktuality", ref: "", href: "/" },
{ name: "Kontakty", ref: "", href: "/kontakty" },
];
const tabs = computed(() => (route.path === "/" ? homeTabs : otherTabs));
// import { useTheme } from 'vuetify' // import { useTheme } from 'vuetify'
// //
// const theme = useTheme() // const theme = useTheme()
@ -74,4 +99,5 @@ const tabs = [
// theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark' // theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
// } // }
</script> </script>

View file

@ -1,40 +1,66 @@
function waitForElement (selector: string, timeout = 2000) : Promise<Element> { import { useRouter, useRoute } from "vue-router";
import { nextTick } from "vue";
export async function useGoTo(
selector: string,
props: { offset?: number } = {}
): Promise<void> {
const router = useRouter();
const route = useRoute();
const yOffset = props?.offset ?? -80;
const hash = selector.startsWith("#") ? selector : "";
const path = selector.startsWith("/") ? selector : route.path + hash;
// If navigating to another page
if (route.path !== path.split("#")[0]) {
await router.push(path);
await nextTick(); // Ensure DOM updates
// Wait for the element to exist before scrolling
try {
const element = await waitForElement(hash);
scrollToElement(element, yOffset);
} catch (error) {
console.warn(error);
}
} else {
// If already on the correct page, scroll immediately
try {
const element = await waitForElement(hash);
scrollToElement(element, yOffset);
} catch (error) {
console.warn(error);
}
}
}
// Scroll to the element smoothly
function scrollToElement(element: Element, offset: number) {
const y = element.getBoundingClientRect().top + window.pageYOffset + offset;
window.scrollTo({ top: y, behavior: "smooth" });
}
// Wait for the element to exist
function waitForElement(selector: string, timeout = 2000): Promise<Element> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const startTime = Date.now(); const startTime = Date.now();
// eslint-disable-next-line prefer-const
let observer: MutationObserver; let observer: MutationObserver;
function checkElement () { function checkElement() {
const element = document.querySelector(selector); const element = document.querySelector(selector);
if (element) { if (element) {
observer.disconnect(); // Stop observing DOM changes
resolve(element); resolve(element);
observer.disconnect(); // Stop observing DOM changes
} else if (Date.now() - startTime >= timeout) { } else if (Date.now() - startTime >= timeout) {
reject(new Error(`Timeout exceeded while waiting for element with selector '${selector}'`));
observer.disconnect(); // Stop observing DOM changes observer.disconnect(); // Stop observing DOM changes
reject(new Error(`Timeout exceeded while waiting for element '${selector}'`));
} }
} }
observer = new MutationObserver(checkElement); observer = new MutationObserver(checkElement);
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
checkElement(); // Check initially in case the element is already present checkElement(); // Initial check in case the element already exists
}); });
} }
export async function useGoTo (selector: string, props: {offset? :number} = {}): Promise<void> {
console.log(selector);
let element: Element;
try {
element = await waitForElement(selector);
} catch {
// element not found
return;
}
const yOffset = props?.offset ?? -80;
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({ top: y, behavior: "smooth" });
}

View file

@ -2,16 +2,17 @@
<h1>Galerie</h1> <h1>Galerie</h1>
<v-row> <v-row>
<v-col <v-col
v-for="(photo, index) in photos" v-for="(photo, index) in gallery"
:key="index" :key="index"
cols="4" cols="4"
style="padding: 0;" style="padding: 0;"
> >
<v-img <v-img
:lazy-src="photo.src" :lazy-src="photo.image"
:src="photo.src" :src="photo.image"
aspect-ratio="1" aspect-ratio="1"
cover cover
@click="showCarousel = true"
> >
<template v-slot:placeholder> <template v-slot:placeholder>
<v-row> <v-row>
@ -21,43 +22,37 @@
></v-progress-circular> ></v-progress-circular>
</v-row> </v-row>
</template> </template>
<v-dialog v-model="showCarousel">
<dialog-carousel
:image="photo.image"
:images="gallery"
:title="photo.title"
@isActive="showCarousel = false"
/>
</v-dialog>
</v-img> </v-img>
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import './assets/css/main.css' import './assets/css/main.css'
import { useAPI } from "~/composables/useAPI";
const photos = [ const showCarousel = ref(false);
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"},
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"}, interface ArticleImage {
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"}, image: string;
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"}, title: string;
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"}, }
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"},
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"}, const gallery = ref<ArticleImage[]>([]);
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"},
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"}, const { error, data } = await useAPI<ArticleImage[]>('load-gallery/', {method: "GET"});
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"},
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"}, if ( data.value ){
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"}, gallery.value = data.value as ArticleImage[];
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"}, }
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"}, else if (error.value) {
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"}, console.error("Error loading gallery:", error.value);
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"}, }
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"},
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"},
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"},
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"},
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"},
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"},
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"},
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"},
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"},
{src: "https://www.danceus.org/parse/files/Bjy5anNVI0Q81M8bmrwIiuU20x4kepQTxzDBfqpR/70d831b8f51edc1f6e1a4320d52f164b_latin-dance.jpg"},
{src: "https://avatars0.githubusercontent.com/u/9064066?v=4&s=460"},
{src: "https://cdn.vuetifyjs.com/images/profiles/marcus.jpg"},
{src: "https://danceostrava.cz/wp-content/uploads/2021/10/IMG_5203-scaled.jpg"},
{src: "https://cdn11.bigcommerce.com/s-07991/product_images/uploaded_images/latin-dance.jpg"},
]
</script> </script>

View file

@ -49,7 +49,7 @@ export default defineNuxtPlugin((app) => {
defaults: { defaults: {
SmallIcon: { SmallIcon: {
color: 'color', color: 'color',
style: [{ 'margin-left': '20px', 'margin-top': '15px', 'font-size': '30px'}] style: [{ 'margin-left': '20px', 'font-size': '30px'}]
}, },
BigIcon: { BigIcon: {
color: 'color', color: 'color',