Compare commits

..

2 commits

Author SHA1 Message Date
Nikola Kubeczkova
7dffa93977 gallery 2025-03-12 16:27:23 +01:00
KUB0570
04445a05a6 progress 2025-03-09 11:20:03 +01:00
17 changed files with 226 additions and 56 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

3
TODO
View file

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

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

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

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

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