Galleries

Find the source code here.

Overview

Gallery is a set on photos in a grid-like design. Based on your needs, JSXPine provides you 3 types of gallery.

Discover each one and their variants with examples below.

Installation

Notes

To use this component, you need to initialize your project first. If not done yet, run one of the following command:

npx jsxpine init or yarn jsxpine init or pnpm jsxpine init or bunx jsxpine init.

Go to the installation and usage page to learn more.

jsxpine add gallery
Copied !

Copied !


	--------------------     Components Dependencies   -------------------------

		import { Button } from "./button.component";
import { Icon } from "./icon.component";
import { Image } from "./image.component";
import clsx from "clsx";

/**
 * @typedef CarouselNavigationProps
 * @property {import("../common/types").DirectionType} direction
 */

/**
 * @typedef CarouselIndicatorsProps
 * @type {import("../common/props").HTMLTagWithChildren & { indicator?: boolean | import("../common/types").PositionType }}
 */

/**
 * @typedef CarouselProps
 * @type {{ slides?: import("./image.component").ImageType[], loop?: boolean, indicator?: boolean | import("../common/types").PositionType, direction?: import("../common/types").DirectionType, hideNavigations?: boolean, slidesToShow?: number, navigations?: JSX.Element, indicators?: JSX.Element } & import("../common/props").HTMLTag}
 */

/**
 * Carousel Navigation component props
 * @type {import("../common/props").JSXComponent<CarouselNavigationProps>}
 */
export function CarouselNavigations(props) {
	const { direction } = props;
	/**
	 * @type {Map<import("../common/types").DirectionType, { previous: string; next: string }>}
	 */
	const navigationButtonDirectionClassMap = new Map([
		[
			"horizontal",
			{
				previous: "top-1/2 -translate-y-1/2 left-4",
				next: "top-1/2 -translate-y-1/2 right-4"
			}
		],
		[
			"vertical",
			{
				previous: "left-1/2 -translate-x-1/2 top-4",
				next: "left-1/2 -translate-x-1/2 bottom-4"
			}
		]
	]);

	/**
	 * @type {Map<import("../common/types").DirectionType, { previous: import("../common/types").IconName; next: import("../common/types").IconName }>}
	 */
	const navigationIconDirectionClassMap = new Map([
		[
			"horizontal",
			{ previous: "ri.arrow-left-s-line", next: "ri.arrow-right-s-line" }
		],
		[
			"vertical",
			{ previous: "ri.arrow-up-s-line", next: "ri.arrow-down-s-line" }
		]
	]);

	const navIcon = navigationIconDirectionClassMap.get(direction);
	return (
		<>
			<Button
				x-bind:disabled="!loop && areFirstSlidesToShow()"
				x-on:click="previous()"
				borderRadius="circle"
				class={`absolute ${
					navigationButtonDirectionClassMap.get(direction)?.previous
				}`}
			>
				{navIcon ? (
					<Icon
						name={navIcon.previous}
						size={6}
						fill="none"
						stroke="currentColor"
					/>
				) : (
					""
				)}
			</Button>
			<Button
				x-bind:disabled="!loop && areLastSlidesToShow()"
				x-on:click="next()"
				borderRadius="circle"
				class={`absolute ${
					navigationButtonDirectionClassMap.get(direction)?.next
				}`}
			>
				{navIcon ? (
					<Icon
						name={navIcon.next}
						size={6}
						fill="none"
						stroke="currentColor"
					/>
				) : (
					""
				)}
			</Button>
		</>
	);
}

/**
 * Carousel Indicators component props
 * @type {import("../common/props").JSXComponent<CarouselIndicatorsProps>}
 */
export function CarouselIndicators(props) {
	const { children, indicator } = props;

	/**
	 * @type {Map<import("../common/types").PositionType, string>}
	 */
	const indicatorPositionClassMap = new Map([
		["top", "top-0 left-1/2 -translate-x-1/2 gap-x-2"],
		["bottom", "bottom-0 left-1/2 -translate-x-1/2 gap-x-2"],
		["left", "left-0 top-1/2 -translate-y-1/2 flex-col gap-y-2"],
		["right", "right-0 top-1/2 -translate-y-1/2 flex-col gap-y-2"]
	]);

	return (
		<ul
			class={`absolute flex ${
				indicator && typeof indicator !== "boolean"
					? indicatorPositionClassMap.get(indicator)
					: indicatorPositionClassMap.get("bottom")
			} p-4`}
		>
			{children ?? (
				<template x-for="(indicator, index) in $refs.carousel.children">
					<li>
						<button
							x-on:click="setActiveIndex(Number(index))"
							x-bind:disabled="isActive(Number(index))"
							class="cursor-pointer p-2 rounded-full btn"
						></button>
					</li>
				</template>
			)}
		</ul>
	);
}

/**
 * Carousel component props
 * @type {import("../common/props").JSXComponent<CarouselProps>}
 */
export function Carousel(props) {
	const {
		children,
		slides,
		class: className,
		loop = false,
		indicator = false,
		direction = "horizontal",
		hideNavigations = false,
		slidesToShow = 1,
		navigations,
		indicators,
		...restProps
	} = props;

	/**
	 * @type {Map<import("../common/types").DirectionType, string>}
	 */
	const directionClassMap = new Map([
		["horizontal", "flex items-center"],
		[
			"vertical",
			slidesToShow > 1 ? "flex flex-col items-center" : "grid grid-flow-dense"
		]
	]);
	/**
	 * @type {Map<import("../common/types").DirectionType, string>}
	 */
	const slideDirectionClassMap = new Map([
		["horizontal", slidesToShow > 1 ? "" : "min-w-full h-full"],
		["vertical", slidesToShow > 1 ? "" : "min-w-full h-[inherit]"]
	]);
	const slideDirectionDimensionStyle =
		direction === "horizontal"
			? `width: ${100 / slidesToShow}%; height: 100%;`
			: `width: 100%; height: ${100 / slidesToShow}%;`;

	return (
		<div
			x-data={`carousel({ loop: ${loop}, slidesToShow: ${slidesToShow} })`}
			class={clsx("relative flex", className)}
			{...restProps}
		>
			<div class="relative flex grow h-full">
				<div
					x-ref="carousel"
					class={`${directionClassMap.get(direction)} overflow-hidden`}
				>
					{children ??
						slides?.map((slide) => (
							<Image
								class={slideDirectionClassMap.get(direction)}
								style={slideDirectionDimensionStyle}
								src={slide.src}
								alt={slide.alt}
							/>
						))}
				</div>
				{!hideNavigations ? (
					<>{navigations ?? <CarouselNavigations direction={direction} />}</>
				) : null}
			</div>
			{indicator === true || typeof indicator !== "boolean" ? (
				<>{indicators ?? <CarouselIndicators indicator={indicator} />}</>
			) : null}
		</div>
	);
}


import clsx from "clsx";

/**
 * @typedef HTMLButtonTagWithChildren
 * @type {import("@kitajs/html").PropsWithChildren<Omit<JSX.HtmlButtonTag, "className"> & import("../common/props").CLSXClassProps>}
 */

/**
 * @typedef {Object} ButtonProps
 * @type {{ text?: string } & HTMLButtonTagWithChildren & import("../common/props").BorderRadiusProps & import("../common/props").SizeProps & import("../common/props").VariantColorProps}
 */

/**
 * Button component props
 * @param {ButtonProps} props
 */
export function Button({
	children,
	class: className,
	size = "md",
	text,
	variant = "solid",
	borderRadius = "rounded",
	...restProps
}) {
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantColorMap = new Map([
		["solid", "btn"],
		["outlined", "btn-outlined"],
		["inversed", "btn-inversed"]
	]);

	/**
	 * @type {Map<import("../common/types").SizeType, string>}
	 */
	const sizeMap = new Map([
		["xs", "text-xs px-2 py-1"],
		["sm", "px-4 py-2 text-sm"],
		["md", "px-4 py-2"],
		["lg", "px-8 py-4 text-lg"],
		["xl", "px-12 py-6 text-xl"],
		["2xl", "px-16 py-8 text-4xl"]
	]);

	/**
	 * @type {Map<import("../common/types").BorderRadiusType, string>}
	 */
	const borderRadiusMap = new Map([
		["square", "rounded-none"],
		["rounded", "rounded"],
		["arc", "rounded-xl"],
		["pill", "rounded-full"],
		["curve", "rounded-lg"],
		["circle", "aspect-square rounded-full"]
	]);

	return (
		<button
			class={clsx(
				variantColorMap.get(variant),
				sizeMap.get(size),
				borderRadiusMap.get(borderRadius),
				className
			)}
			{...restProps}
		>
			{text ? <span safe>{text}</span> : children}
		</button>
	);
}

/**
 * Primary Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function PrimaryButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-primary"],
		["outlined", "btn-primary-outlined"],
		["inversed", "btn-primary-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}

/**
 * Secondary Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function SecondaryButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-secondary"],
		["outlined", "btn-secondary-outlined"],
		["inversed", "btn-secondary-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}

/**
 * Success Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function SuccessButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-success"],
		["outlined", "btn-success-outlined"],
		["inversed", "btn-success-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}

/**
 * Danger Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function DangerButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-danger"],
		["outlined", "btn-danger-outlined"],
		["inversed", "btn-danger-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}

/**
 * Info Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function InfoButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-info"],
		["outlined", "btn-info-outlined"],
		["inversed", "btn-info-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}

/**
 * Warning Button component props
 * @type {import("../common/props").JSXComponent<ButtonProps>}
 */
export function WarningButton(props) {
	const { children, class: className, variant = "solid", ...restProps } = props;
	/**
	 * @type {Map<import("../common/types").VariantColorType, string>}
	 */
	const variantClassMap = new Map([
		["solid", "btn-warning"],
		["outlined", "btn-warning-outlined"],
		["inversed", "btn-warning-inversed"]
	]);

	return (
		<Button
			class={[variantClassMap.get(variant), className].join(" ")}
			{...restProps}
		>
			{children}
		</Button>
	);
}


import { icons } from "@iconify-json/ri/icons.json";
import clsx from "clsx";
import { SVG } from "./svg.component";

/**
 * @typedef {`ri.${keyof typeof import("@iconify-json/ri/icons.json")["icons"]}`} IconName
 */

/**
 * @typedef IconProps
 * @type {{size?: number, name: IconName, color?: string, applyDefsId?: string} & import("./svg.component").SVGProps}
 */

/**
 * Icon component props
 * @param {Omit<IconProps, "viewBox">} props
 */
export function Icon({
	children,
	size = 4,
	name,
	applyDefsId,
	class: className,
	...restProps
}) {
	const iconType = name.split(".")[0];
	const iconName = /** @type {keyof typeof icons} */ (name.split(".")[1]);
	if (!icons[iconName]) {
		console.error(`"${name}" is not an icon from iconify/${iconType}`);
		return "";
	}
	const { body } = icons[iconName];

	// The purpose is to retrieve value from d attribute
	let retrieveDValue = "";
	const bodyMatch = body.match(/d=".+"/g);
	const retrieveDAttribute = bodyMatch ? bodyMatch[0] : "";
	if (retrieveDAttribute) {
		retrieveDValue = retrieveDAttribute.slice(3, -2);
	}

	return (
		<SVG
			viewBox={"0 0 24 24"}
			{...restProps}
			class={clsx(`size-${size}`, className)}
		>
			{children}
			<path
				stroke-linecap="round"
				stroke-linejoin="round"
				fill={applyDefsId ? `url(#${applyDefsId})` : "currentColor"}
				d={retrieveDValue}
			/>
		</SVG>
	);
}


/**
 * @typedef SVGProps
 * @type {{fill?: string, stroke?: string, strokeWidth?: number, viewBox: string } & Omit<JSX.HtmlTag, "className"> & import("../common/props").CLSXClassProps}
 */

/**
 * SVG component props
 * @type {import("../common/props").JSXComponent<SVGProps>}
 */
export function SVG(props) {
    const {
        children,
        class: className,
        fill = "none",
        stroke = "currentColor",
        strokeWidth = 0.5,
        ...restProps
        } = props;
    return (
        <svg
            xmlns="http://www.w3.org/2000/svg"
            fill={fill}
            stroke={stroke}
            class={className}
            style={{ strokeWidth: String(strokeWidth) }}
            {...restProps}
        >
            {children}
        </svg>
    )
}

/**
 * @typedef {Object} ImageType
 * @property {string} src
 * @property {string} [alt]
 */

/**
 * @typedef ImageProps
 * @type {Omit<JSX.HtmlImageTag, "className" | "loading"> & import("../common/props").CLSXClassProps}
 */

/**
 * Image Component props
 * @type {import("../common/props").JSXComponent<ImageProps>}
 */
export function Image(props) {
    const { ...restProps } = props;
    
    return (
        <img {...{ ...restProps, loading: "lazy" }} />
    )
}

import { Button } from "./button.component";
import { Icon } from "./icon.component";
import { Image } from "./image.component";
import clsx from "clsx";

/**
 * @typedef ZoomProps
 * @type {import("../common/props").HTMLTag & { selector?: string, showNavigation?: boolean }}
 */

/**
 * Zoom component props
 * @type {import("../common/props").JSXComponent<ZoomProps>}
 */
export function Zoom(props) {
	const {
		children,
		class: className,
		selector = "body",
		showNavigation = false
	} = props;
	return (
		<div
			x-data={`zoom(${showNavigation})`}
			class={clsx("w-full h-full select-none", className)}
		>
			{children}
			<template x-teleport={selector}>
				<div
					x-bind="shower"
					x-transition:enter="transition ease-in-out duration-300"
					x-transition:enter-start="opacity-0"
					x-transition:leave="transition ease-in-in duration-300"
					x-transition:leave-end="opacity-0"
					{...{
						"@click": "zoomOut",
						"@keyup.window.escape": "zoomOut",
						"@keyup.left.window": "showNavigation && previousImage($event)",
						"@keyup.right.window": "showNavigation && nextImage($event)",
						"x-trap.inert.noscroll": "zoom"
					}}
					class="fixed inset-0 z-[99] flex items-center justify-center bg-black bg-opacity-50 select-none cursor-zoom-out"
					x-cloak="true"
				>
					<div class="relative flex items-center justify-center aspect-square xl:aspect-video">
						{showNavigation && (
							<Button
								x-on:click="previousImage($event)"
								x-bind:disabled="disablePreviousNavigation"
								class="absolute left-0 flex items-center justify-center text-white translate-x-10 rounded-full cursor-pointer xl:-translate-x-24 2xl:-translate-x-32 bg-neutral-800/75 w-14 h-14 hover:bg-white/20"
							>
								<Icon name="ri.arrow-left-line" size={6} />
							</Button>
						)}
						<Image
							x-show="zoom"
							x-transition:enter="transition ease-in-out duration-300"
							x-transition:enter-start="opacity-0 transform scale-50"
							x-transition:leave="transition ease-in-in duration-300"
							x-transition:leave-end="opacity-0 transform scale-50"
							class="object-contain object-center w-full h-full select-none cursor-zoom-out"
							x-bind:src="selectedImage?.src"
							x-bind:alt="selectedImage?.alt"
						/>
						{showNavigation && (
							<Button
								x-on:click="nextImage($event)"
								x-bind:disabled="disableNextNavigation"
								class="absolute right-0 flex items-center justify-center text-white -translate-x-10 rounded-full cursor-pointer xl:translate-x-24 2xl:translate-x-32 bg-neutral-800/75 w-14 h-14 hover:bg-white/20"
							>
								<Icon name="ri.arrow-right-line" size={6} />
							</Button>
						)}
					</div>
				</div>
			</template>
		</div>
	);
}


	
	--------------------     Gallery Component   -------------------------

		/**
 * @typedef CarouselGalleryProps
 * @type {Omit<import("./carousel.component").CarouselProps, "slidesToShow" | "indicators" | "slides"> & { images: import("./image.component").ImageType[], thumbnailWidth?: string, zoom?: boolean }}
 */

import { Carousel } from "./carousel.component";
import { Image } from "./image.component";
import { Zoom } from "./zoom.component";
import clsx from "clsx";

/**
 * @typedef {"show-all" | "zoom"} RestGalleryRestEffectType
 */

/**
 * @typedef RestGalleryProps
 * @type {{ images?: import("./image.component").ImageType[], nbDisplayedImages?: number, restEffect?: RestGalleryRestEffectType, zoom?: boolean } & import("../common/props").HTMLTag}
 */

/**
 *  Carousel Gallery component props
 * @type {import("../common/props").JSXComponent<CarouselGalleryProps>}
 */
export function CarouselGallery(props) {
	const {
		images = [],
		thumbnailWidth = "10rem",
		direction = "horizontal",
		zoom = false,
		...restProps
	} = props;

	return (
		<Zoom>
			<Carousel
				slides={images}
				{...restProps}
				class={direction === "horizontal" ? "flex-col" : "flex-row"}
				x-init={
					zoom
						? `
                        [...$refs.carousel.children].forEach((element) => {
                            element.classList.add("cursor-zoom-in");
                            element.addEventListener("click", () => {
                                zoomIn({ src: element.src, alt: element.alt });
                            })
                        })
                    `
						: undefined
				}
				indicator
				indicators={
					<ul
						class={clsx(
							"items-center overflow-auto bg-black grow",
							direction === "vertical"
								? "flex flex-col -order-1"
								: "grid grid-flow-col-dense"
						)}
						x-init={`
                        if (${direction === "vertical"}) {
                            $el.style.height = $refs.carousel.getBoundingClientRect().height + 'px';
                        }
                    `}
						x-ref="thumbnails"
					>
						<template x-for="(indicator, index) in items">
							<Image
								x-bind:src="indicator.src"
								x-bind:alt="indicator.alt"
								x-on:click="setActiveIndex(Number(index))"
								x-bind:class="{ 'opacity-50 cursor-not-allowed': isActive(Number(index)), 'cursor-pointer': !isActive(Number(index)) }"
								x-init={`
                                $watch("activeIndex", () => {
                                    if (isActive(Number(index))) {
                                        xDistance = $el.getBoundingClientRect().x - $refs["thumbnails"].getBoundingClientRect().x;
                                        yDistance = $el.getBoundingClientRect().y - $refs["thumbnails"].getBoundingClientRect().y;
                                        $refs["thumbnails"].scrollBy(xDistance, yDistance);
                                    }
                                })
                            `}
								style={`min-width: ${thumbnailWidth}`}
								class={clsx(
									direction === "vertical" ? "w-full" : "h-full",
									"object-cover"
								)}
							/>
						</template>
					</ul>
				}
			/>
		</Zoom>
	);
}

/**
 *
 * @type {import("../common/props").JSXComponent<RestGalleryProps>}
 */
export function RestGallery(props) {
	const {
		images,
		nbDisplayedImages = 3,
		restEffect = "show-all",
		class: className,
		zoom = false,
		...restProps
	} = props;
    
	return (
		<Zoom>
			<ul
				x-data={`restGallery(${JSON.stringify(
					images
				)}, ${nbDisplayedImages}, "${restEffect}")`}
				class={className}
				{...restProps}
				x-ref="restGallery"
			>
				<template x-for="image in displayedImages">
					<li>
						<Image
							x-on:click={zoom && `zoomIn(image)`}
							x-bind:src="image.src"
							x-bind:alt="image.alt"
							class={clsx("object-cover h-full", { "cursor-zoom-in": zoom })}
						/>
					</li>
				</template>
				<template x-if="displayedImages.length !== images.length">
					<li
						class="flex items-center justify-center gap-x-2 bg-slate-800 text-white text-2xl cursor-pointer"
						x-on:click="clickOnRestImage()"
						x-ref="restImage"
					>
						<span class="">+</span>
						<span x-text="nbRestImages"></span>
					</li>
				</template>
			</ul>
		</Zoom>
	);
}

	
	
	--------------------     Alpine Dependencies   -------------------------

		/**
 * @typedef {Object} RestGalleryDataOutput
 * @property {import("../../../components/image.component").ImageType[]} images
 * @property {number} nbDisplayedImages
 * @property {number} nbRestImages
 * @property {import("../../../components/image.component").ImageType[]} displayedImages
 * @property {Function} showAllImages
 * @property {Function} updateRestImageHeight
 * @property {(images: import("../../../components/image.component").ImageType[]) => Promise<void>} initializeImage
 */

/**
 * Rest Gallery data props
 * @param {import("../../../components/image.component").ImageType[]} images
 * @param {number} nbDisplayedImages
 * @param {import("../../../components/galleries.component").RestGalleryRestEffectType} restEffect
 * @returns {import("alpinejs").AlpineComponent<RestGalleryDataOutput>}
 */
export function restGalleryData(
	images = [],
	nbDisplayedImages = 3,
	restEffect = "show-all"
) {
	return {
		async init() {
			this.initializeImage(this.images);

			this.$watch("images", async (imagesValue) => {
				this.initializeImage(imagesValue);
			});
		},
		images,
		nbDisplayedImages,
		nbRestImages: 0,
		displayedImages: [],
		showAllImages() {
			this.displayedImages = this.images;
		},
		clickOnRestImage() {
			switch (restEffect) {
				case "zoom":
					break;
				case "show-all":
				default:
					this.showAllImages();
					break;
			}
		},
		updateRestImageHeight() {
			const firstChildrenHeight =
				this.$refs.restGallery.children[1].getBoundingClientRect().height;
			if (this.$refs.restImage) {
				this.$refs.restImage.style.height = firstChildrenHeight + "px";
			}
		},
		async initializeImage(images) {
			this.displayedImages = images.slice(0, this.nbDisplayedImages);
			this.nbRestImages = images.length - this.nbDisplayedImages;
			await this.$nextTick();
			this.updateRestImageHeight();
			window.addEventListener("resize", async () => {
				await this.$nextTick();
				this.updateRestImageHeight();
			});
		}
	};
}


	
ComponentsDropdown
ComponentsIcon