fix gallery and menu

This commit is contained in:
Nikola Kubeczkova 2025-04-07 21:51:22 +02:00
parent 46be762b8a
commit e641ade1b2
11 changed files with 275 additions and 136 deletions

View file

@ -35,11 +35,14 @@ 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(self, obj):
def get_image(obj):
main_image = obj.images.order_by("main").first() main_image = obj.images.order_by("main").first()
url = 'http://localhost:8000' if main_image:
return f"{url}{main_image.image.url}" if main_image else None return ArticleImageSerializer(main_image, context=self.context).data
return {
"image": "http://localhost:8000/media/images/image.jpg",
"title": "Výchozí obrázek",
}
class EventListSerializer(serializers.ModelSerializer): class EventListSerializer(serializers.ModelSerializer):

View file

@ -12,6 +12,7 @@ h1 {
margin-bottom: 1rem; margin-bottom: 1rem;
margin-top: 2rem; margin-top: 2rem;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
cursor: default;
} }
h2 { h2 {
@ -20,6 +21,7 @@ h2 {
text-align: center; text-align: center;
word-break: break-word; word-break: break-word;
margin-bottom: 2rem; margin-bottom: 2rem;
cursor: default;
} }
h3 { h3 {
@ -27,6 +29,7 @@ h3 {
color: #666; color: #666;
text-align: center; text-align: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
cursor: default;
} }
h4 { h4 {
@ -34,12 +37,14 @@ h4 {
color: #666; color: #666;
text-align: center; text-align: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
cursor: default;
} }
h5 { h5 {
font-size: 1rem; font-size: 1rem;
color: #aaa; color: #aaa;
margin-top: 1rem; margin-top: 1rem;
cursor: default;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@ -68,6 +73,7 @@ h5 {
.app__title { .app__title {
font-size: 2rem; font-size: 2rem;
color: #333; color: #333;
cursor: default;
} }
.app__tab { .app__tab {
@ -95,6 +101,7 @@ h5 {
color: #333; color: #333;
margin-bottom: 1rem; margin-bottom: 1rem;
font-weight: bold; font-weight: bold;
cursor: default;
} }
.contact__dialog__title { .contact__dialog__title {
@ -102,6 +109,8 @@ h5 {
color: #333; color: #333;
margin-bottom: 1rem; margin-bottom: 1rem;
font-weight: bold; font-weight: bold;
word-break: break-word !important;
cursor: default;
} }
.contact__button { .contact__button {
@ -116,7 +125,6 @@ h5 {
} }
.contact { .contact {
min-width: unset;
margin: 10px; margin: 10px;
padding: 1rem; padding: 1rem;
} }
@ -130,6 +138,7 @@ h5 {
.contact__dialog__title { .contact__dialog__title {
font-size: 1.5rem; font-size: 1.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
word-break: break-word;
} }
} }
@ -138,6 +147,7 @@ h5 {
justify-content: center; justify-content: center;
margin: 0 auto; margin: 0 auto;
max-width: 1200px; max-width: 1200px;
cursor: default;
} }
.article { .article {
@ -146,17 +156,20 @@ h5 {
border-radius: 0; border-radius: 0;
box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0);
margin-bottom: 1rem; margin-bottom: 1rem;
cursor: default;
} }
.article__title { .article__title {
font-size: 1.4rem; font-size: 1.4rem;
color: #CF3476; color: #CF3476;
font-weight: bold; font-weight: bold;
cursor: default;
} }
.article__date { .article__date {
font-size: 0.8rem; font-size: 0.8rem;
text-align: left; text-align: left;
cursor: default;
} }
.article__text { .article__text {
@ -165,6 +178,11 @@ h5 {
line-height: 1.5; line-height: 1.5;
padding: 0.5rem; padding: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
cursor: default;
}
.article__image:hover {
cursor: pointer;
} }
.article__sign { .article__sign {
@ -172,6 +190,7 @@ h5 {
font-size: 1rem; font-size: 1rem;
font-style: italic; font-style: italic;
color: #555; color: #555;
cursor: default;
} }
.show_more { .show_more {
@ -222,6 +241,7 @@ h5 {
.pricing-title { .pricing-title {
min-height: 3rem; min-height: 3rem;
cursor: pointer;
} }
.pricing-desc { .pricing-desc {
@ -274,6 +294,7 @@ h5 {
height: 100%; height: 100%;
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
cursor: default;
} }
.trainer-avatar { .trainer-avatar {
@ -310,6 +331,7 @@ h5 {
margin-top: 1.2rem; margin-top: 1.2rem;
margin-left: calc(20% - 2.5rem); margin-left: calc(20% - 2.5rem);
margin-right: calc(20% - 2.5rem); margin-right: calc(20% - 2.5rem);
cursor: default;
} }
.advantage__title { .advantage__title {
@ -318,12 +340,14 @@ h5 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;
margin-top: 0.1rem; margin-top: 0.1rem;
cursor: default;
} }
.advantage__text { .advantage__text {
padding-bottom: 1rem; padding-bottom: 1rem;
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
cursor: default;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@ -352,6 +376,7 @@ h5 {
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
cursor: default;
} }
.about__parallax { .about__parallax {
@ -377,6 +402,8 @@ h5 {
font-weight: bold; font-weight: bold;
word-break: break-word; word-break: break-word;
white-space: normal; white-space: normal;
display: block;
cursor: default;
} }
.about__subtitle { .about__subtitle {
@ -385,6 +412,8 @@ h5 {
margin-bottom: 1rem; margin-bottom: 1rem;
word-break: break-word; word-break: break-word;
font-weight: bold; font-weight: bold;
display: block;
cursor: default;
} }
.about__text { .about__text {
@ -392,6 +421,7 @@ h5 {
font-size: 1.125rem; font-size: 1.125rem;
padding: 0 1rem; padding: 0 1rem;
line-height: 1.6; line-height: 1.6;
cursor: default;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@ -418,6 +448,23 @@ h5 {
} }
} }
.masonry-gallery {
columns: 3;
column-gap: 16px;
}
@media (max-width: 600px) {
.masonry-gallery {
columns: 1;
}
}
.masonry-item {
break-inside: avoid;
margin-bottom: 16px;
}
.footer { .footer {
background-color: black; background-color: black;
cursor: default;
} }

View file

@ -4,8 +4,8 @@
src="public/dark-dance.jpg" src="public/dark-dance.jpg"
scale="0.8" scale="0.8"
> >
<v-container id="about"> <v-container>
<v-card class="about"> <v-card class="about" id="about">
<v-card-title class="about__title">Vítejte v tanečním klubu!</v-card-title> <v-card-title class="about__title">Vítejte v tanečním klubu!</v-card-title>
<v-card-subtitle class="about__subtitle">Objevte kouzlo tance s námi!</v-card-subtitle> <v-card-subtitle class="about__subtitle">Objevte kouzlo tance s námi!</v-card-subtitle>
<v-card-text class="about__text"> <v-card-text class="about__text">

View file

@ -38,11 +38,10 @@
transition="dialog-bottom-transition" transition="dialog-bottom-transition"
> >
<v-card class="contact pa-4"> <v-card class="contact pa-4">
<v-card-title class="contact__dialog__title d-flex align-center"> <h1 class="contact__dialog__title d-flex align-center">
<span class="flex-grow-1 text-center" style="margin-left: 40px;">{{ chosenCourse.name }}</span> <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-btn icon="mdi-close" variant="text" @click="openDialog = false"></v-btn>
</v-card-title> </h1>
<v-card-text v-if="chosenCourse.desc" class="text-center"> <v-card-text v-if="chosenCourse.desc" class="text-center">
<div class="pricing__subtitle text-body-1">{{ chosenCourse.desc }}</div> <div class="pricing__subtitle text-body-1">{{ chosenCourse.desc }}</div>
</v-card-text> </v-card-text>

View file

@ -13,32 +13,19 @@
<v-row> <v-row>
<v-col> <v-col>
<v-img <v-img
:lazy-src="article.image" :src="article.image.image"
:src="article.image" :lazy-src="article.image.image"
aspect-ratio="1" class="article__image rounded-lg"
class="article__image" @click="openCarousel(article.image, article.images)"
@click="showCarousel = true"
> >
<template v-slot:placeholder> <template v-slot:placeholder>
<v-row justify="center" align="center" class="fill-height"> <v-row justify="center" align="center" class="fill-height">
<v-progress-circular color="grey-lighten-5" indeterminate></v-progress-circular> <v-progress-circular color="grey-lighten-5" indeterminate />
</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 cols="12" md="8">
cols="12"
md="8"
>
<v-row class="article__text">{{ article.content }}</v-row> <v-row class="article__text">{{ article.content }}</v-row>
</v-col> </v-col>
</v-row> </v-row>
@ -46,15 +33,28 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<v-dialog v-model="showCarousel" max-width="90vw" v-if="selectedImages.length > 0">
<dialog-carousel
v-if="selectedImage"
:image="selectedImage"
:images="selectedImages"
@isActive="showCarousel = false"
/>
</v-dialog>
<v-btn to="/aktuality" class="show_more" v-if="!props.forAll"> <v-btn to="/aktuality" class="show_more" v-if="!props.forAll">
<v-icon icon="mdi-chevron-down" />Další aktuality<v-icon icon="mdi-chevron-down" /> <v-icon icon="mdi-chevron-down" />Další aktuality<v-icon icon="mdi-chevron-down" />
</v-btn> </v-btn>
</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); const showCarousel = ref(false);
const selectedImage = ref<ArticleImage | null>(null);
const selectedImages = ref<ArticleImage[]>([]);
const props = defineProps<{ const props = defineProps<{
forAll: boolean forAll: boolean
@ -62,12 +62,13 @@ const props = defineProps<{
interface ArticleImage { interface ArticleImage {
image: string; image: string;
title: string;
} }
interface Article { interface Article {
id: number; id: number;
title: string; title: string;
image: string; image: ArticleImage;
images: ArticleImage[]; images: ArticleImage[];
date: string; date: string;
content: string; content: string;
@ -76,15 +77,18 @@ interface Article {
const articles = ref<Article[]>([]); const articles = ref<Article[]>([]);
const endpoint = props.forAll ? 'load-all-articles/' : 'load-articles/'; function openCarousel(image: ArticleImage, images: ArticleImage[]) {
selectedImage.value = image;
selectedImages.value = images;
showCarousel.value = true;
}
const endpoint = props.forAll ? 'load-all-articles/' : 'load-articles/';
const { error, data } = await useAPI<Article[]>(endpoint, { method: "GET" }); const { error, data } = await useAPI<Article[]>(endpoint, { method: "GET" });
if (data.value) { if (data.value) {
articles.value = data.value; articles.value = data.value;
} } else if (error.value) {
else if (error.value) {
console.error("Error loading articles:", error.value); console.error("Error loading articles:", error.value);
} }
</script> </script>

View file

@ -1,11 +1,11 @@
<template> <template>
<v-parallax class="trainers__parallax" src="public/img/black-pink.jpg"> <v-parallax class="trainers__parallax" src="public/img/black-pink.jpg">
<v-container id="trainers"> <v-container>
<v-card class="trainers pa-4"> <v-card class="about" id="trainers">
<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-carousel cycle interval="4000" hide-delimiters show-arrows="hover"> <v-carousel style="height: auto" cycle interval="4000" hide-delimiters :show-arrows="trainerGroups.length > 1 ? 'hover' : false">
<v-carousel-item v-for="(group, index) in trainerGroups" :key="index"> <v-carousel-item v-for="(group, index) in trainerGroups" :key="index">
<v-row justify="center"> <v-row justify="center">
<v-col <v-col
@ -25,8 +25,8 @@
> >
<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> <div class="about__subtitle">{{ lector.name }}</div>
<v-card-text class="about__text">{{ lector.desc }}</v-card-text> <div class="about__text">{{ lector.desc }}</div>
</v-col> </v-col>
</v-row> </v-row>
</v-carousel-item> </v-carousel-item>
@ -64,6 +64,11 @@ const lectors = [
img: "/trainers/img.png", img: "/trainers/img.png",
desc: "Lektorka - tance pro děti", desc: "Lektorka - tance pro děti",
}, },
{
name: "Ondřej Gilar",
img: "/trainers/img.png",
desc: "Trenér - latinskoamerické tance a Pro-AM",
},
] ]
const trainerGroups = computed(() => { const trainerGroups = computed(() => {

View file

@ -1,35 +1,46 @@
<template> <template>
<v-card class="article__image"> <v-card class="article__image">
<v-card-title>{{ title }} <v-card-title>
{{ images[model] && images[model].title }}
<v-icon class="to_right" @click="$emit('isActive', false)">mdi-close</v-icon> <v-icon class="to_right" @click="$emit('isActive', false)">mdi-close</v-icon>
</v-card-title> </v-card-title>
<v-carousel <v-carousel
v-model="model"
:hide-delimiters="true" :hide-delimiters="true"
height="90vh" height="90vh"
> >
<v-carousel-item <v-carousel-item
v-for="(item, i) in images" v-for="(item, i) in props.images"
:key="i" :key="i"
:src="item.image" :src="item.image"
cover cover
class="carousel__image" class="carousel__image"
></v-carousel-item> />
</v-carousel> </v-carousel>
</v-card> </v-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue';
import './assets/css/main.css'; import './assets/css/main.css';
interface ArticleImage { interface ArticleImage {
image: string; image: string;
title: string;
} }
defineProps<{ const props = defineProps<{
image: string; image: ArticleImage;
images: ArticleImage[]; images: ArticleImage[];
title: string;
}>(); }>();
defineEmits(["isActive"]) defineEmits(["isActive"]);
const model = ref(0);
watch(() => props.image, (newImage) => {
const index = props.images.findIndex((img) => img.image === newImage.image);
if (index !== -1) model.value = index;
}, { immediate: true });
</script> </script>

View file

@ -9,13 +9,15 @@
Taneční klub Ostrava Taneční klub Ostrava
</v-app-bar-title> </v-app-bar-title>
<v-tabs v-model="currentTab" class="d-none d-sm-flex app__tab"> <v-tabs
v-if="$vuetify.display.smAndUp"
v-model="currentTabName"
class="app__tab"
>
<v-tab <v-tab
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
:key="index" :key="index"
:text="tab.name"
:value="tab.name" :value="tab.name"
:href="tab.href ? tab.href : undefined"
@click="handleNavigation(tab)" @click="handleNavigation(tab)"
> >
{{ tab.name }} {{ tab.name }}
@ -48,37 +50,93 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useGoTo } from "~/composables/useGoTo"; import { computed, onMounted, watch, nextTick } from 'vue';
import { useRoute } from "#vue-router"; import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); const route = useRoute();
const currentTab = ref({ name: "O nás", ref: "#about", href: "o-nas" }); const router = useRouter();
const homeTabs = [ const homeTabs = [
{ name: "O nás", ref: "#about", href: "" }, { name: "O nás", href: "#about" },
{ name: "Trenéři", ref: "#trainers", href: "" }, { name: "Trenéři", href: "#trainers" },
{ name: "Kurzy", ref: "#courses", href: "" }, { name: "Kurzy", href: "#courses" },
{ name: "Galerie", ref: "", href: "/galerie" }, { name: "Galerie", href: "/galerie" },
{ name: "Aktuality", ref: "#article", href: "" }, { name: "Aktuality", href: "#article" },
{ name: "Kontakty", ref: "", href: "/kontakty" }, { name: "Kontakty", href: "/kontakty" },
]; ];
const otherTabs = [ const otherTabs = [
{ name: "O nás", ref: "", href: "/" }, { name: "O nás", href: "/#about" },
{ name: "Trenéři", ref: "", href: "/" }, { name: "Trenéři", href: "/#trainers" },
{ name: "Kurzy", ref: "", href: "/" }, { name: "Kurzy", href: "/#courses" },
{ name: "Galerie", ref: "", href: "/galerie" }, { name: "Galerie", href: "/galerie" },
{ name: "Aktuality", ref: "", href: "/aktuality" }, { name: "Aktuality", href: "/aktuality" },
{ name: "Kontakty", ref: "", href: "/kontakty" }, { name: "Kontakty", href: "/kontakty" },
]; ];
const tabs = computed(() => (route.path === "/" ? homeTabs : otherTabs)); const tabs = computed(() => (route.path === "/" ? homeTabs : otherTabs));
const handleNavigation = (tab: any) => { type Tab = {
if (tab.ref) { name: string;
useGoTo(tab.ref); href: string;
} else if (tab.href) { };
window.location.href = tab.href;
const currentTab = ref<Tab>({ name: "O nás", href: "#about" });
const currentTabName = computed<string>({
get: () => currentTab.value.name,
set: (newName: string) => {
const tab = tabs.value.find(t => t.name === newName);
if (tab) handleNavigation(tab);
}
});
const handleNavigation = async (tab: { name: string, href: string }) => {
currentTab.value = tab;
if (tab.href.startsWith("#")) {
await scrollToHash(tab.href);
} else if (tab.href.startsWith("/#")) {
await router.push(tab.href);
} else {
await router.push(tab.href);
} }
}; };
async function scrollToHash(hash: string) {
await nextTick();
const el = document.querySelector(hash);
if (el) {
const yOffset = -80;
const y = el.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({ top: y, behavior: 'smooth' });
}
}
onMounted(() => {
if (route.path === "/" && route.hash) {
const match = homeTabs.find(tab => tab.href === route.hash);
if (match) {
currentTab.value = match;
}
scrollToHash(route.hash);
}
});
watch(() => route.hash, (newHash) => {
if (route.path === "/" && newHash) {
const match = homeTabs.find(tab => tab.href === newHash);
if (match) currentTab.value = match;
scrollToHash(newHash);
}
});
watch(() => route.path, (newPath) => {
if (newPath !== "/") {
const match = otherTabs.find(tab => tab.href === newPath || tab.href === `${newPath}${route.hash}`);
if (match) currentTab.value = match;
}
}, { immediate: true });
</script> </script>

View file

@ -7,30 +7,35 @@ export async function useGoTo(
): Promise<void> { ): Promise<void> {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const yOffset = props?.offset ?? -80; const yOffset = props?.offset ?? -150;
const hash = selector.startsWith("#") ? selector : ""; const hash = selector.startsWith("#") ? selector : "";
const path = selector.startsWith("/") ? selector : route.path + hash; const basePath = route.path.split("#")[0];
const targetPath = selector.startsWith("/") ? selector : basePath + hash;
const [pathOnly, hashOnly] = targetPath.split("#");
// If navigating to another page if (route.path !== pathOnly) {
if (route.path !== path.split("#")[0]) { // Navigate to target page
await router.push(path); await router.push(pathOnly + (hashOnly ? `#${hashOnly}` : ""));
await nextTick(); // Ensure DOM updates await nextTick();
// Wait for the element to exist before scrolling if (hashOnly) {
try { try {
const element = await waitForElement(hash); const element = await waitForElement(`#${hashOnly}`);
scrollToElement(element, yOffset); scrollToElement(element, yOffset);
} catch (error) { } catch (err) {
console.warn(error); console.warn(err);
}
} }
} else { } else {
// If already on the correct page, scroll immediately // Same page — scroll directly
if (hashOnly || selector.startsWith("#")) {
try { try {
const element = await waitForElement(hash); const element = await waitForElement(`#${hashOnly ?? selector}`);
scrollToElement(element, yOffset); scrollToElement(element, yOffset);
} catch (error) { } catch (err) {
console.warn(error); console.warn(err);
}
} }
} }
} }

View file

@ -1,44 +1,47 @@
<template> <template>
<h1>Galerie</h1> <h1>Galerie</h1>
<v-row> <div class="masonry-gallery">
<v-col <div
class="masonry-item"
v-for="(photo, index) in gallery" v-for="(photo, index) in gallery"
:key="index" :key="index"
cols="4" @click="openCarousel(photo)"
style="padding: 0;"
> >
<v-img <v-img
:lazy-src="photo.image"
:src="photo.image" :src="photo.image"
aspect-ratio="1" :lazy-src="photo.image"
aspect-ratio=""
class="article__image rounded-lg"
cover cover
@click="showCarousel = true"
> >
<template v-slot:placeholder> <template #placeholder>
<v-row> <v-row justify="center" align="center" style="height: 100px;">
<v-progress-circular <v-progress-circular indeterminate color="grey-lighten-1" />
color="grey-lighten-5"
indeterminate
></v-progress-circular>
</v-row> </v-row>
</template> </template>
</v-img>
</div>
<v-dialog v-model="showCarousel"> <v-dialog v-model="showCarousel">
<dialog-carousel <dialog-carousel
:image="photo.image" v-if="selectedImage"
:image="selectedImage"
:images="gallery" :images="gallery"
:title="photo.title"
@isActive="showCarousel = false" @isActive="showCarousel = false"
/> />
</v-dialog> </v-dialog>
</v-img> </div>
</v-col>
</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"; import { useAPI } from "~/composables/useAPI";
const showCarousel = ref(false); const showCarousel = ref(false);
const selectedImage = ref<ArticleImage | null>(null);
function openCarousel(photo: ArticleImage) {
selectedImage.value = photo;
showCarousel.value = true;
}
interface ArticleImage { interface ArticleImage {
image: string; image: string;

View file

@ -62,6 +62,10 @@ export default defineNuxtPlugin((app) => {
cs: { cs: {
calendar: { calendar: {
today: "dnes", today: "dnes",
},
carousel: {
next: "další",
prev: "předchozí",
} }
} }
} }