This commit is contained in:
Nikola Kubeczkova 2025-03-12 16:27:23 +01:00
parent 04445a05a6
commit 7dffa93977
11 changed files with 169 additions and 54 deletions

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

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)

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

@ -14,10 +14,8 @@ 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(upload_to="images/%Y", 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):
@ -26,7 +24,8 @@ class Article(models.Model):
class ArticleImage(models.Model): class ArticleImage(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='images') article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='images')
image = models.FileField(upload_to="images/%Y") image = models.FileField(upload_to="images/%Y", default="default.png")
main = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"Image for {self.article.title}, {self.article.date}" return f"Image for {self.article.title}, {self.article.date}"

View file

@ -11,13 +11,20 @@ class ContactSerializer(serializers.ModelSerializer):
class ArticleImageSerializer(serializers.ModelSerializer): class ArticleImageSerializer(serializers.ModelSerializer):
title = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
model = ArticleImage model = ArticleImage
fields = ['image'] 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) images = ArticleImageSerializer(many=True, read_only=True)
class Meta: class Meta:
@ -28,6 +35,11 @@ class ArticleListSerializer(serializers.ModelSerializer):
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 from tko.models import Article, Event, ArticleImage
from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer, ArticleImageSerializer
class ContactView(CreateAPIView): class ContactView(CreateAPIView):
@ -33,6 +33,12 @@ 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,6 +118,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;

View file

@ -12,20 +12,26 @@
<v-row> <v-row>
<v-col> <v-col>
<v-img <v-img
:lazy-src="article.image.image" :lazy-src="article.image"
:src="article.image.image" :src="article.image"
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,9 +53,10 @@
</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 { interface ArticleImage {
image: string; image: string;
} }
@ -75,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

@ -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

@ -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>