Skip to content

Commit

Permalink
Merge pull request #144 from n-d-r-d-g/feature/meetup-img-carousel
Browse files Browse the repository at this point in the history
Added a full screen image slider on the `Meetup` detail page
  • Loading branch information
MrSunshyne committed Jul 10, 2024
2 parents aa7e08b + f814419 commit a548c01
Show file tree
Hide file tree
Showing 24 changed files with 9,220 additions and 5,921 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
},
"keywords": [],
"author": "",
"license": "ISC"
}
"license": "ISC",
"packageManager": "[email protected]+sha512.98a80fd11c2e7096747762304106432b3ddc67dcf54b5a8c01c93f68a2cd5e05e6821849522a06fb76284d41a2660d5e334f2ee3bbf29183bf2e739b1dafa771"
}
47 changes: 42 additions & 5 deletions packages/frontendmu-nuxt/components/meetup/Album.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
<template>
<div class="lg:mx-auto lg:w-[80%] px-4">
<div v-if="getCurrentEvent.album" class="flex flex-col items-center gap-8 py-20">
<div class="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="photo in currentAlbum" :key="photo" class="aspect-video">
<img :src="`${source}/${photo}`"
class="object-cover w-full h-full object-center block rounded-md overflow-hidden" loading="lazy" />
<Dialog>
<div class="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="(photo, index) in currentAlbum" :key="photo" class="aspect-video">
<DialogTrigger as-child>
<img
:src="`${source}/${photo}`"
@click="setActiveImageIndex(index)"
loading="lazy"
tabindex="0"
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"
/>
</DialogTrigger>
</div>
</div>
</div>
<DialogContent
class="bg-zinc-950 bg-opacity-80 border-zinc-800 max-w-7xl"
>
<DialogHeader>
<DialogTitle class="sr-only">Photos</DialogTitle>
<DialogDescription class="sr-only"
>Photos in carousel</DialogDescription
>
</DialogHeader>
<Carousel
:opts="{ startIndex: activeImageIndex }"
class="relative max-h-[calc(100svh-160px)] rounded-md overflow-hidden"
>
<CarouselContent class="h-full max-h-[calc(100svh-160px)]">
<CarouselItem v-for="photo in currentAlbum" :key="photo">
<div class="w-full h-full flex flex-row justify-center items-center">
<img :src="`${source}/${photo}`" class="object-contain max-w-full max-h-full block rounded-md overflow-hidden" />
</div>
</CarouselItem>
</CarouselContent>
<CarouselPrevious class="left-1 bg-[#00000097]" />
<CarouselNext class="right-1 bg-[#00000097]" />
</Carousel>
</DialogContent>
</Dialog>
<!-- ToDo -->
<!--
<div
Expand All @@ -32,9 +65,13 @@ const props = defineProps<{
const limit = ref(10); // Set your desired limit here
const maxAlbumLength = computed(() => props.currentAlbum.length);
const activeImageIndex = ref(0);
function viewMore() {
// Implement your logic here
}
function setActiveImageIndex(index: number) {
activeImageIndex.value = index;
}
</script>
26 changes: 26 additions & 0 deletions packages/frontendmu-nuxt/components/ui/button/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
import { cn } from '@/lib/utils'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>

<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
35 changes: 35 additions & 0 deletions packages/frontendmu-nuxt/components/ui/button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type VariantProps, cva } from 'class-variance-authority'

export { default as Button } from './Button.vue'

export const buttonVariants = cva(
'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',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)

export type ButtonVariants = VariantProps<typeof buttonVariants>
44 changes: 44 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/Carousel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { useProvideCarousel } from './useCarousel'
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: 'horizontal',
})
const emits = defineEmits<CarouselEmits>()
const carouselArgs = useProvideCarousel(props, emits)
defineExpose(carouselArgs)
function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
if (event.key === prevKey) {
event.preventDefault()
carouselArgs.scrollPrev()
return
}
if (event.key === nextKey) {
event.preventDefault()
carouselArgs.scrollNext()
}
}
</script>

<template>
<div
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown"
>
<slot v-bind="carouselArgs" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<WithClassAsProps>()
const { carouselRef, orientation } = useCarousel()
</script>

<template>
<div ref="carouselRef" class="overflow-hidden">
<div
:class="
cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
props.class,
)"
v-bind="$attrs"
>
<slot />
</div>
</div>
</template>
23 changes: 23 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/CarouselItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()
</script>

<template>
<div
role="group"
aria-roledescription="slide"
:class="cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
props.class,
)"
>
<slot />
</div>
</template>
32 changes: 32 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/CarouselNext.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { ArrowRight } from "lucide-vue-next";
import { useCarousel } from "./useCarousel";
import type { WithClassAsProps } from "./interface";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
const props = defineProps<WithClassAsProps>();
const { orientation, canScrollNext, scrollNext } = useCarousel();
</script>

<template>
<Button
:disabled="!canScrollNext"
:class="
cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
variant="outline"
@click="scrollNext"
>
<slot>
<ArrowRight class="h-4 w-4 text-current" />
</slot>
</Button>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { ArrowLeft } from "lucide-vue-next";
import { useCarousel } from "./useCarousel";
import type { WithClassAsProps } from "./interface";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
const props = defineProps<WithClassAsProps>();
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
</script>

<template>
<Button
:disabled="!canScrollPrev"
:class="
cn(
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class
)
"
variant="outline"
@click="scrollPrev"
>
<slot>
<ArrowLeft class="h-4 w-4 text-current" />
</slot>
</Button>
</template>
10 changes: 10 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { default as Carousel } from './Carousel.vue'
export { default as CarouselContent } from './CarouselContent.vue'
export { default as CarouselItem } from './CarouselItem.vue'
export { default as CarouselPrevious } from './CarouselPrevious.vue'
export { default as CarouselNext } from './CarouselNext.vue'
export { useCarousel } from './useCarousel'

export type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'
20 changes: 20 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {
EmblaCarouselType as CarouselApi,
EmblaOptionsType as CarouselOptions,
EmblaPluginType as CarouselPlugin,
} from 'embla-carousel'
import type { HTMLAttributes, Ref } from 'vue'

export interface CarouselProps {
opts?: CarouselOptions | Ref<CarouselOptions>
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
orientation?: 'horizontal' | 'vertical'
}

export interface CarouselEmits {
(e: 'init-api', payload: CarouselApi): void
}

export interface WithClassAsProps {
class?: HTMLAttributes['class']
}
59 changes: 59 additions & 0 deletions packages/frontendmu-nuxt/components/ui/carousel/useCarousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createInjectionState } from '@vueuse/core'
import emblaCarouselVue from 'embla-carousel-vue'
import { onMounted, ref } from 'vue'
import type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'
import type { CarouselEmits, CarouselProps } from './interface'

const [useProvideCarousel, useInjectCarousel] = createInjectionState(
({
opts,
orientation,
plugins,
}: CarouselProps, emits: CarouselEmits) => {
const [emblaNode, emblaApi] = emblaCarouselVue({
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
}, plugins)

function scrollPrev() {
emblaApi.value?.scrollPrev()
}
function scrollNext() {
emblaApi.value?.scrollNext()
}

const canScrollNext = ref(true)
const canScrollPrev = ref(true)

function onSelect(api: CarouselApi) {
canScrollNext.value = api.canScrollNext()
canScrollPrev.value = api.canScrollPrev()
}

onMounted(() => {
if (!emblaApi.value)
return

emblaApi.value?.on('init', onSelect)
emblaApi.value?.on('reInit', onSelect)
emblaApi.value?.on('select', onSelect)

emits('init-api', emblaApi.value)
})

return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
},
)

function useCarousel() {
const carouselState = useInjectCarousel()

if (!carouselState)
throw new Error('useCarousel must be used within a <Carousel />')

return carouselState
}

export { useCarousel, useProvideCarousel }
Loading

0 comments on commit a548c01

Please sign in to comment.