Skip to content

Commit a548c01

Browse files
authored
Merge pull request #144 from n-d-r-d-g/feature/meetup-img-carousel
Added a full screen image slider on the `Meetup` detail page
2 parents aa7e08b + f814419 commit a548c01

24 files changed

+9220
-5921
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
},
1010
"keywords": [],
1111
"author": "",
12-
"license": "ISC"
13-
}
12+
"license": "ISC",
13+
"packageManager": "pnpm@9.2.0+sha512.98a80fd11c2e7096747762304106432b3ddc67dcf54b5a8c01c93f68a2cd5e05e6821849522a06fb76284d41a2660d5e334f2ee3bbf29183bf2e739b1dafa771"
14+
}

packages/frontendmu-nuxt/components/meetup/Album.vue

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
<template>
22
<div class="lg:mx-auto lg:w-[80%] px-4">
33
<div v-if="getCurrentEvent.album" class="flex flex-col items-center gap-8 py-20">
4-
<div class="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
5-
<div v-for="photo in currentAlbum" :key="photo" class="aspect-video">
6-
<img :src="`${source}/${photo}`"
7-
class="object-cover w-full h-full object-center block rounded-md overflow-hidden" loading="lazy" />
4+
<Dialog>
5+
<div class="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
6+
<div v-for="(photo, index) in currentAlbum" :key="photo" class="aspect-video">
7+
<DialogTrigger as-child>
8+
<img
9+
:src="`${source}/${photo}`"
10+
@click="setActiveImageIndex(index)"
11+
loading="lazy"
12+
tabindex="0"
13+
class="object-cover w-full h-full object-center block rounded-md overflow-hidden cursor-zoom-in hover:scale-105 focus-visible:scale-105 transition-transform"
14+
/>
15+
</DialogTrigger>
16+
</div>
817
</div>
9-
</div>
18+
<DialogContent
19+
class="bg-zinc-950 bg-opacity-80 border-zinc-800 max-w-7xl"
20+
>
21+
<DialogHeader>
22+
<DialogTitle class="sr-only">Photos</DialogTitle>
23+
<DialogDescription class="sr-only"
24+
>Photos in carousel</DialogDescription
25+
>
26+
</DialogHeader>
27+
<Carousel
28+
:opts="{ startIndex: activeImageIndex }"
29+
class="relative max-h-[calc(100svh-160px)] rounded-md overflow-hidden"
30+
>
31+
<CarouselContent class="h-full max-h-[calc(100svh-160px)]">
32+
<CarouselItem v-for="photo in currentAlbum" :key="photo">
33+
<div class="w-full h-full flex flex-row justify-center items-center">
34+
<img :src="`${source}/${photo}`" class="object-contain max-w-full max-h-full block rounded-md overflow-hidden" />
35+
</div>
36+
</CarouselItem>
37+
</CarouselContent>
38+
<CarouselPrevious class="left-1 bg-[#00000097]" />
39+
<CarouselNext class="right-1 bg-[#00000097]" />
40+
</Carousel>
41+
</DialogContent>
42+
</Dialog>
1043
<!-- ToDo -->
1144
<!--
1245
<div
@@ -32,9 +65,13 @@ const props = defineProps<{
3265
3366
const limit = ref(10); // Set your desired limit here
3467
const maxAlbumLength = computed(() => props.currentAlbum.length);
68+
const activeImageIndex = ref(0);
3569
3670
function viewMore() {
3771
// Implement your logic here
3872
}
3973
74+
function setActiveImageIndex(index: number) {
75+
activeImageIndex.value = index;
76+
}
4077
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { Primitive, type PrimitiveProps } from 'radix-vue'
4+
import { type ButtonVariants, buttonVariants } from '.'
5+
import { cn } from '@/lib/utils'
6+
7+
interface Props extends PrimitiveProps {
8+
variant?: ButtonVariants['variant']
9+
size?: ButtonVariants['size']
10+
class?: HTMLAttributes['class']
11+
}
12+
13+
const props = withDefaults(defineProps<Props>(), {
14+
as: 'button',
15+
})
16+
</script>
17+
18+
<template>
19+
<Primitive
20+
:as="as"
21+
:as-child="asChild"
22+
:class="cn(buttonVariants({ variant, size }), props.class)"
23+
>
24+
<slot />
25+
</Primitive>
26+
</template>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type VariantProps, cva } from 'class-variance-authority'
2+
3+
export { default as Button } from './Button.vue'
4+
5+
export const buttonVariants = cva(
6+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
7+
{
8+
variants: {
9+
variant: {
10+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
11+
destructive:
12+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
13+
outline:
14+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15+
secondary:
16+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
17+
ghost: 'hover:bg-accent hover:text-accent-foreground',
18+
link: 'text-primary underline-offset-4 hover:underline',
19+
},
20+
size: {
21+
default: 'h-10 px-4 py-2',
22+
xs: 'h-7 rounded px-2',
23+
sm: 'h-9 rounded-md px-3',
24+
lg: 'h-11 rounded-md px-8',
25+
icon: 'h-10 w-10',
26+
},
27+
},
28+
defaultVariants: {
29+
variant: 'default',
30+
size: 'default',
31+
},
32+
},
33+
)
34+
35+
export type ButtonVariants = VariantProps<typeof buttonVariants>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script setup lang="ts">
2+
import { useProvideCarousel } from './useCarousel'
3+
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
7+
orientation: 'horizontal',
8+
})
9+
10+
const emits = defineEmits<CarouselEmits>()
11+
12+
const carouselArgs = useProvideCarousel(props, emits)
13+
14+
defineExpose(carouselArgs)
15+
16+
function onKeyDown(event: KeyboardEvent) {
17+
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
18+
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
19+
20+
if (event.key === prevKey) {
21+
event.preventDefault()
22+
carouselArgs.scrollPrev()
23+
24+
return
25+
}
26+
27+
if (event.key === nextKey) {
28+
event.preventDefault()
29+
carouselArgs.scrollNext()
30+
}
31+
}
32+
</script>
33+
34+
<template>
35+
<div
36+
:class="cn('relative', props.class)"
37+
role="region"
38+
aria-roledescription="carousel"
39+
tabindex="0"
40+
@keydown="onKeyDown"
41+
>
42+
<slot v-bind="carouselArgs" />
43+
</div>
44+
</template>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import { useCarousel } from './useCarousel'
3+
import type { WithClassAsProps } from './interface'
4+
import { cn } from '@/lib/utils'
5+
6+
defineOptions({
7+
inheritAttrs: false,
8+
})
9+
10+
const props = defineProps<WithClassAsProps>()
11+
12+
const { carouselRef, orientation } = useCarousel()
13+
</script>
14+
15+
<template>
16+
<div ref="carouselRef" class="overflow-hidden">
17+
<div
18+
:class="
19+
cn(
20+
'flex',
21+
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
22+
props.class,
23+
)"
24+
v-bind="$attrs"
25+
>
26+
<slot />
27+
</div>
28+
</div>
29+
</template>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import { useCarousel } from './useCarousel'
3+
import type { WithClassAsProps } from './interface'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<WithClassAsProps>()
7+
8+
const { orientation } = useCarousel()
9+
</script>
10+
11+
<template>
12+
<div
13+
role="group"
14+
aria-roledescription="slide"
15+
:class="cn(
16+
'min-w-0 shrink-0 grow-0 basis-full',
17+
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
18+
props.class,
19+
)"
20+
>
21+
<slot />
22+
</div>
23+
</template>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import { ArrowRight } from "lucide-vue-next";
3+
import { useCarousel } from "./useCarousel";
4+
import type { WithClassAsProps } from "./interface";
5+
import { cn } from "@/lib/utils";
6+
import { Button } from "@/components/ui/button";
7+
8+
const props = defineProps<WithClassAsProps>();
9+
10+
const { orientation, canScrollNext, scrollNext } = useCarousel();
11+
</script>
12+
13+
<template>
14+
<Button
15+
:disabled="!canScrollNext"
16+
:class="
17+
cn(
18+
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
19+
orientation === 'horizontal'
20+
? '-right-12 top-1/2 -translate-y-1/2'
21+
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
22+
props.class
23+
)
24+
"
25+
variant="outline"
26+
@click="scrollNext"
27+
>
28+
<slot>
29+
<ArrowRight class="h-4 w-4 text-current" />
30+
</slot>
31+
</Button>
32+
</template>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import { ArrowLeft } from "lucide-vue-next";
3+
import { useCarousel } from "./useCarousel";
4+
import type { WithClassAsProps } from "./interface";
5+
import { cn } from "@/lib/utils";
6+
import { Button } from "@/components/ui/button";
7+
8+
const props = defineProps<WithClassAsProps>();
9+
10+
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
11+
</script>
12+
13+
<template>
14+
<Button
15+
:disabled="!canScrollPrev"
16+
:class="
17+
cn(
18+
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
19+
orientation === 'horizontal'
20+
? '-left-12 top-1/2 -translate-y-1/2'
21+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
22+
props.class
23+
)
24+
"
25+
variant="outline"
26+
@click="scrollPrev"
27+
>
28+
<slot>
29+
<ArrowLeft class="h-4 w-4 text-current" />
30+
</slot>
31+
</Button>
32+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { default as Carousel } from './Carousel.vue'
2+
export { default as CarouselContent } from './CarouselContent.vue'
3+
export { default as CarouselItem } from './CarouselItem.vue'
4+
export { default as CarouselPrevious } from './CarouselPrevious.vue'
5+
export { default as CarouselNext } from './CarouselNext.vue'
6+
export { useCarousel } from './useCarousel'
7+
8+
export type {
9+
EmblaCarouselType as CarouselApi,
10+
} from 'embla-carousel'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {
2+
EmblaCarouselType as CarouselApi,
3+
EmblaOptionsType as CarouselOptions,
4+
EmblaPluginType as CarouselPlugin,
5+
} from 'embla-carousel'
6+
import type { HTMLAttributes, Ref } from 'vue'
7+
8+
export interface CarouselProps {
9+
opts?: CarouselOptions | Ref<CarouselOptions>
10+
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
11+
orientation?: 'horizontal' | 'vertical'
12+
}
13+
14+
export interface CarouselEmits {
15+
(e: 'init-api', payload: CarouselApi): void
16+
}
17+
18+
export interface WithClassAsProps {
19+
class?: HTMLAttributes['class']
20+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createInjectionState } from '@vueuse/core'
2+
import emblaCarouselVue from 'embla-carousel-vue'
3+
import { onMounted, ref } from 'vue'
4+
import type {
5+
EmblaCarouselType as CarouselApi,
6+
} from 'embla-carousel'
7+
import type { CarouselEmits, CarouselProps } from './interface'
8+
9+
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
10+
({
11+
opts,
12+
orientation,
13+
plugins,
14+
}: CarouselProps, emits: CarouselEmits) => {
15+
const [emblaNode, emblaApi] = emblaCarouselVue({
16+
...opts,
17+
axis: orientation === 'horizontal' ? 'x' : 'y',
18+
}, plugins)
19+
20+
function scrollPrev() {
21+
emblaApi.value?.scrollPrev()
22+
}
23+
function scrollNext() {
24+
emblaApi.value?.scrollNext()
25+
}
26+
27+
const canScrollNext = ref(true)
28+
const canScrollPrev = ref(true)
29+
30+
function onSelect(api: CarouselApi) {
31+
canScrollNext.value = api.canScrollNext()
32+
canScrollPrev.value = api.canScrollPrev()
33+
}
34+
35+
onMounted(() => {
36+
if (!emblaApi.value)
37+
return
38+
39+
emblaApi.value?.on('init', onSelect)
40+
emblaApi.value?.on('reInit', onSelect)
41+
emblaApi.value?.on('select', onSelect)
42+
43+
emits('init-api', emblaApi.value)
44+
})
45+
46+
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
47+
},
48+
)
49+
50+
function useCarousel() {
51+
const carouselState = useInjectCarousel()
52+
53+
if (!carouselState)
54+
throw new Error('useCarousel must be used within a <Carousel />')
55+
56+
return carouselState
57+
}
58+
59+
export { useCarousel, useProvideCarousel }

0 commit comments

Comments
 (0)