Skip to content

Commit

Permalink
✨feat: swipe image gallery
Browse files Browse the repository at this point in the history
  • Loading branch information
Marukome0743 committed Jan 27, 2025
1 parent b7e4da3 commit 319fb77
Showing 1 changed file with 117 additions and 11 deletions.
128 changes: 117 additions & 11 deletions app/@modal/(.)history/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,115 @@
"use client"

import { XMarkIcon } from "@heroicons/react/24/solid"
import { useRouter } from "next/navigation"
import type { Picture } from "@/app/interfaces/picture"
import type { EventDate } from "@/app/interfaces/schedule"
import { HIROSHIMA_HISTORY, KANTO, KANTO_HISTORY } from "@/app/lib/constant"
import {
ChevronLeftIcon,
ChevronRightIcon,
XMarkIcon,
} from "@heroicons/react/24/solid"
import { SlashIcon } from "@heroicons/react/24/solid"
import { usePathname, useRouter } from "next/navigation"
import {
type JSX,
type MouseEvent,
type ReactNode,
type RefObject,
type TouchEvent,
useEffect,
useRef,
useState,
} from "react"
import styles from "./modal.module.css"

export function Modal({
children,
}: Readonly<{ children: ReactNode }>): JSX.Element {
const router = useRouter()
const pathname: string = usePathname()
const pathParts: string[] = pathname.split("/")
const history: EventDate[] =
`/${pathParts[3]}` === KANTO.pathname ? KANTO_HISTORY : HIROSHIMA_HISTORY
const eventDate: EventDate = history.find(
(history) => history.date === pathParts[4],
) as EventDate
const picture: Picture = eventDate.pictures.find(
(picture) => picture.src.split("/")[5].split(".")[0] === pathParts[5],
) as Picture
const indexOfPicture: number = eventDate.pictures.indexOf(picture)
const dialogRef: RefObject<HTMLDialogElement | null> =
useRef<HTMLDialogElement | null>(null)
const [touchState, setTouchState] = useState<{
touchStart: number
touchEnd: number
}>({ touchStart: 0, touchEnd: 0 })
const minSwipeDistance = 50

useEffect(() => {
if (!dialogRef.current?.open) {
dialogRef.current?.showModal()
}
})

function onTouchStart(e: MouseEvent | TouchEvent) {
setTouchState({
touchStart: "targetTouches" in e ? e.targetTouches[0].clientX : e.clientX,
touchEnd: 0,
})
}

function onTouchMove(e: MouseEvent | TouchEvent) {
setTouchState((prev) => ({
...prev,
touchEnd: "targetTouches" in e ? e.targetTouches[0].clientX : e.clientX,
}))
}

function onTouchEnd() {
if (touchState.touchStart === 0 || touchState.touchEnd === 0) {
return
}

const distance: number = touchState.touchStart - touchState.touchEnd
const isLeftSwipe: boolean = minSwipeDistance < distance
const isRightSwipe: boolean = distance < -minSwipeDistance

if (!(isLeftSwipe || isRightSwipe)) {
return
}

if (isLeftSwipe && indexOfPicture < eventDate.pictures.length - 1) {
MovePage("next")
}
if (isRightSwipe && indexOfPicture > 0) {
MovePage("prev")
}
}

function MovePage(direction: "next" | "prev"): void {
const newPicture: Picture =
direction === "next"
? eventDate.pictures[indexOfPicture + 1]
: eventDate.pictures[indexOfPicture - 1]
pathParts[pathParts.length - 1] = newPicture.src.split("/")[5].split(".")[0]
router.replace(pathParts.join("/"))
}

return (
<dialog
ref={dialogRef}
onClose={router.back}
className={`bg-transparent duration-200 ease-out grid h-full items-center justify-items-center max-h-none max-w-none opacity-0 overflow-hidden overscroll-contain pointer-events-none text-inherit w-full z-50 backdrop:bg-[#0006] open:opacity-100 open:pointer-events-auto open:visible ${styles.dialog}`}
>
<div className="bg-white col-start-1 duration-200 ease-out max-h-[90vh] max-w-3xl overflow-y-auto overscroll-contain p-2 pt-7 rounded-2xl row-start-1 scale-90 shadow-2xl space-y-2 transition w-full in-open:scale-100 in-open:translate-y-0 sm:p-6 sm:space-y-4">
<div
onMouseDown={onTouchStart}
onMouseMove={onTouchMove}
onMouseUp={onTouchEnd}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
className="bg-white col-start-1 duration-200 ease-out max-h-[90vh] max-w-3xl overflow-y-auto overscroll-contain p-2 pt-7 rounded-2xl row-start-1 scale-90 select-none shadow-2xl space-y-2 transition w-full in-open:scale-100 in-open:translate-y-0 sm:p-6 sm:space-y-4"
>
<button
type="button"
onClick={router.back}
Expand All @@ -39,14 +118,41 @@ export function Modal({
<XMarkIcon className="size-6" />
</button>
{children}
<button
type="button"
onClick={router.back}
className="bg-gray-100 button-pop cursor-pointer flex float-right font-bold gap-2 h-12 items-center justify-center px-4 rounded-xl shadow-xs text-sm hover:bg-gray-300"
>
<XMarkIcon className="size-6" />
閉じる
</button>
<div className="flex gap-2 items-center justify-between">
<strong className="flex items-center text-base sm:w-26">
<sup>{indexOfPicture + 1}</sup>
<SlashIcon className="size-6" />
<sub>{eventDate.pictures.length}</sub>
</strong>
<div className="flex gap-4 sm:gap-6">
<button
type="button"
onClick={() => MovePage("prev")}
disabled={indexOfPicture === 0}
className="bg-yellow-300 button-pop cursor-pointer flex font-bold gap-2 h-12 items-center justify-center px-4 rounded-xl shadow-xs text-sm disabled:cursor-not-allowed disabled:opacity-30 enabled:hover:bg-yellow-400"
>
<ChevronLeftIcon className="size-6" />
<span className="hidden sm:block">前へ</span>
</button>
<button
type="button"
onClick={() => MovePage("next")}
disabled={indexOfPicture === eventDate.pictures.length - 1}
className="bg-sky-300 button-pop cursor-pointer flex font-bold gap-2 h-12 items-center justify-center px-4 rounded-xl shadow-xs text-sm disabled:cursor-not-allowed disabled:opacity-30 enabled:hover:bg-blue-400"
>
<span className="hidden sm:block">次へ</span>
<ChevronRightIcon className="size-6" />
</button>
</div>
<button
type="button"
onClick={router.back}
className="bg-gray-100 button-pop cursor-pointer flex font-bold gap-2 h-12 items-center justify-center px-4 rounded-xl shadow-xs text-sm hover:bg-gray-300"
>
<XMarkIcon className="size-6" />
閉じる
</button>
</div>
</div>
<button
type="button"
Expand Down

0 comments on commit 319fb77

Please sign in to comment.