Compare commits

..

No commits in common. "galery-to-article" and "master" have entirely different histories.

17 changed files with 56 additions and 226 deletions

View file

@ -2,10 +2,9 @@
## Running ## Running
1. create .env from .env.example 1. `docker compose run --rm frontend npm install`
2. `docker compose run --rm frontend npm install` 2. `docker compose build`
3. `docker compose build` 3. `docker compose up`
4. `docker compose up`
## Adding new frontend packages ## Adding new frontend packages

3
TODO
View file

@ -1,8 +1,11 @@
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)
Naprogramovat: Naprogramovat:
- 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
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)

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, GalleryView from tko.views import ContactView, NewArticleListView, AllArticleListView, EventListView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -29,7 +29,6 @@ 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.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View file

@ -19,15 +19,9 @@ 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

@ -1,32 +0,0 @@
# 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

@ -1,27 +0,0 @@
# 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

@ -1,27 +0,0 @@
# 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,23 +14,16 @@ 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, ArticleImage from tko.models import Contact, Article, Event
class ContactSerializer(serializers.ModelSerializer): class ContactSerializer(serializers.ModelSerializer):
@ -10,36 +10,17 @@ 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", "images"] fields = ["id", "author", "content", "date", "image", "title"]
@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

@ -4,8 +4,8 @@ from django.db.models import Q
from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.generics import ListAPIView, CreateAPIView
from rest_framework import permissions from rest_framework import permissions
from tko.models import Article, Event, ArticleImage from tko.models import Article, Event
from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer, ArticleImageSerializer from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer
class ContactView(CreateAPIView): class ContactView(CreateAPIView):
@ -33,12 +33,6 @@ class AllArticleListView(ListAPIView):
).order_by('-date') ).order_by('-date')
class GalleryView(ListAPIView):
queryset = ArticleImage.objects.all().order_by("-article_id", "main")
serializer_class = ArticleImageSerializer
permission_classes = [permissions.AllowAny]
class EventListView(ListAPIView): class EventListView(ListAPIView):
queryset = Event.objects.all() queryset = Event.objects.all()
serializer_class = EventListSerializer serializer_class = EventListSerializer

View file

@ -118,12 +118,6 @@ 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;

View file

@ -17,21 +17,15 @@
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 justify="center" align="center" class="fill-height"> <v-row>
<v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular> <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
@ -53,19 +47,13 @@
</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;
@ -82,4 +70,5 @@ else if (error.value) {
console.error("Error loading articles:", error.value); console.error("Error loading articles:", error.value);
} }
</script> </script>

View file

@ -1,35 +0,0 @@
<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

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