From dcf10a06b6ecc53d0fea83dc4cafd2c7cd028723 Mon Sep 17 00:00:00 2001 From: maretol Date: Mon, 13 Jan 2025 17:48:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=9E=E3=83=B3=E3=82=AC=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AE=E3=82=B9=E3=83=9E=E3=83=9B=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E7=AD=89=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 355 +++++++++++++++++++++++- pages/components/middle/comicbook.tsx | 380 ++++++++++++++++++++------ pages/components/ui/carousel.tsx | 226 +++++++++++++++ pages/components/ui/label.tsx | 26 ++ pages/components/ui/popover.tsx | 31 +++ pages/components/ui/slider.tsx | 25 ++ pages/components/ui/switch.tsx | 29 ++ pages/lib/hook/use_window_size.ts | 19 ++ pages/package.json | 7 +- 9 files changed, 1013 insertions(+), 85 deletions(-) create mode 100644 pages/components/ui/carousel.tsx create mode 100644 pages/components/ui/label.tsx create mode 100644 pages/components/ui/popover.tsx create mode 100644 pages/components/ui/slider.tsx create mode 100644 pages/components/ui/switch.tsx create mode 100644 pages/lib/hook/use_window_size.ts diff --git a/package-lock.json b/package-lock.json index f83d95d..7f9c1f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1832,6 +1832,44 @@ "node": ">=14" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2640,11 +2678,66 @@ "node": ">=14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -2709,6 +2802,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", @@ -2790,6 +2898,98 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", + "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", @@ -2858,10 +3058,44 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz", + "integrity": "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, @@ -2875,6 +3109,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "license": "MIT", @@ -2934,6 +3197,63 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -6776,6 +7096,34 @@ "dev": true, "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.2.tgz", + "integrity": "sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.2.tgz", + "integrity": "sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.5.2", + "embla-carousel-reactive-utils": "8.5.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.2.tgz", + "integrity": "sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.5.2" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "license": "MIT" @@ -13125,9 +13473,14 @@ "dependencies": { "@cloudflare/next-on-pages": "^1.13.7", "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-slider": "^1.2.2", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "embla-carousel-react": "^8.5.2", "lucide-react": "^0.471.0", "next": "15.1.4", "react": "^19", diff --git a/pages/components/middle/comicbook.tsx b/pages/components/middle/comicbook.tsx index 311d751..6c5f698 100644 --- a/pages/components/middle/comicbook.tsx +++ b/pages/components/middle/comicbook.tsx @@ -1,7 +1,7 @@ 'use client' -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' -import React, { use, useCallback, useMemo, useState } from 'react' +import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon } from 'lucide-react' +import React, { use, useCallback, useEffect, useMemo, useState } from 'react' import { Button } from '../ui/button' import { getHeaderImage } from '@/lib/image' import ClientImage from '../small/client_image' @@ -9,16 +9,41 @@ import ComicImage from '../small/comic_image' import { cn } from '@/lib/utils' import Link from 'next/link' import { bandeDessineeResult } from 'api-types' +import { Carousel, CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '../ui/carousel' +import useWindowSize from '@/lib/hook/use_window_size' +import { Switch } from '../ui/switch' +import { Label } from '../ui/label' +import { Slider } from '../ui/slider' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' type ComicBookProps = { cmsResult: Promise } -type pageState = { +type siglePageState = { + position: 'left' | 'right' | 'center' + src: string +} + +type doublePageState = { position: 'left' | 'right' | 'center' | 'pair' - src: string[] | string + src: string | { left: string; right: string } +} + +type pageOption = { + mode_static: boolean // モード固定 + controller_visible: boolean // コントローラー表示 +} + +const initPageOption: pageOption = { + mode_static: false, + controller_visible: false, } +// シングルモードとダブルモード(見開き)の切り替えの幅のしきい値 +// 指定未満の場合シングルモードになる +const modeThreshold = 980 + export default function ComicBook(props: ComicBookProps) { const { cmsResult } = props const data = use(cmsResult) @@ -33,18 +58,52 @@ export default function ComicBook(props: ComicBookProps) { const coverPageSrc = data.cover ? baseUrl + '/' + data.cover : null const backCoverPageSrc = data.back_cover ? baseUrl + '/' + data.back_cover : null const startPageLeftRight = data.first_left_right[0] + // マンガの画像srcはComicImageコンポーネントでCDN経由のURLに変換している const originPageSrc = pageArray.map((i) => getPageImageSrc(baseUrl, filename, i, format)) const headerImage = getHeaderImage() const [currentPage, setCurrentPage] = useState(0) + const [scrollSnaps, setScrollSnaps] = useState() + const [api, setApi] = useState() + const [width, height] = useWindowSize() + const [mode, setMode] = useState<'single' | 'double'>('single') + const [pageOption, setPageOption] = useState(initPageOption) - const memoPageList = useMemo(() => { - const pageList: pageState[] = [] + const singlePageList = useMemo(() => { + const pageList: siglePageState[] = [] // 表紙が指定済みの場合(ない場合スキップ if (coverPageSrc) { pageList.push({ position: 'center', src: coverPageSrc }) } + if (startPageLeftRight === 'left') { + // 本文1ページ目が左だった場合、最初のページは左でその次が交互に左右でセットされる + originPageSrc.forEach((src, i) => { + const position = i % 2 === 0 ? 'left' : 'right' + pageList.push({ position, src }) + }) + } else { + // 本文1ページ目が右だった場合、最初のページは右でその次が交互に左右でセットされる + originPageSrc.forEach((src, i) => { + const position = i % 2 === 0 ? 'right' : 'left' + pageList.push({ position, src }) + }) + } + // 裏表紙が指定済みの場合(ない場合スキップ + if (backCoverPageSrc) { + pageList.push({ position: 'center', src: backCoverPageSrc }) // 裏表紙 + } + + return pageList + }, [startPageLeftRight, coverPageSrc, backCoverPageSrc, originPageSrc]) + + const doublePageList = useMemo(() => { + const pageList: doublePageState[] = [] + // 表紙が指定済みの場合追加 + if (coverPageSrc) { + pageList.push({ position: 'center', src: coverPageSrc }) + } + if (startPageLeftRight === 'left') { // 本文1ページ目が左だった場合、最初のページは左だけで、そこから残りのページは2ページペアで処理する pageList.push({ position: 'left', src: originPageSrc[0] }) @@ -54,7 +113,7 @@ export default function ComicBook(props: ComicBookProps) { pageList.push({ position: 'right', src: originPageSrc[i] }) break } - pageList.push({ position: 'pair', src: [originPageSrc[i], originPageSrc[i + 1]] }) + pageList.push({ position: 'pair', src: { left: originPageSrc[i], right: originPageSrc[i + 1] } }) } } else { // 本文1ページ目が右だった場合、最初から2ページペアで処理する @@ -64,9 +123,10 @@ export default function ComicBook(props: ComicBookProps) { pageList.push({ position: 'right', src: originPageSrc[i] }) break } - pageList.push({ position: 'pair', src: [originPageSrc[i], originPageSrc[i + 1]] }) + pageList.push({ position: 'pair', src: { left: originPageSrc[i], right: originPageSrc[i + 1] } }) } } + // 裏表紙が指定済みの場合(ない場合スキップ if (backCoverPageSrc) { pageList.push({ position: 'center', src: backCoverPageSrc }) // 裏表紙 @@ -76,12 +136,12 @@ export default function ComicBook(props: ComicBookProps) { }, [startPageLeftRight, coverPageSrc, backCoverPageSrc, originPageSrc]) const leftClick = useCallback(() => { - setCurrentPage((prev) => (memoPageList.length - 1 === prev ? prev : prev + 1)) - }, [memoPageList.length]) + api?.scrollNext() + }, [api]) const rightClick = useCallback(() => { - setCurrentPage((prev) => (prev === 0 ? prev : prev - 1)) - }, []) + api?.scrollPrev() + }, [api]) const keyEvent = (e: React.KeyboardEvent) => { const key = e.code @@ -94,11 +154,73 @@ export default function ComicBook(props: ComicBookProps) { } } + const onInit = useCallback( + (api: CarouselApi) => { + setScrollSnaps(api?.scrollSnapList()) + }, + [api] + ) + + const onSelected = useCallback( + (api: CarouselApi) => { + if (!api) return + setCurrentPage(api.selectedScrollSnap()) + }, + [api] + ) + + const changeContollerVisible = useCallback( + (controller_visible: boolean) => { + setPageOption((props) => { + const newProps = { ...props, controller_visible } + localStorage.setItem('page_option', JSON.stringify(newProps)) + return newProps + }) + }, + [pageOption] + ) + + const changeModeStatic = useCallback( + (mode_static: boolean) => { + setPageOption((props) => { + const newProps = { ...props, mode_static } + localStorage.setItem('page_option', JSON.stringify(newProps)) + return newProps + }) + }, + [pageOption] + ) + + useEffect(() => { + const opt = localStorage.getItem('page_option') + if (opt) { + setPageOption(JSON.parse(opt)) + } + }, []) + + useEffect(() => { + if (!api) return + + onInit(api) + onSelected(api) + api.on('reInit', onInit).on('reInit', onSelected).on('select', onSelected) + }, [api, onInit, onSelected]) + + useEffect(() => { + if (width < modeThreshold) { + if (mode === 'single' || pageOption.mode_static) return + setMode('single') + } else { + if (mode === 'double' || pageOption.mode_static) return + setMode('double') + } + }, [width, pageOption.mode_static]) + return ( -
+
@@ -110,77 +232,169 @@ export default function ComicBook(props: ComicBookProps) {
-
- -
-
- -
-
- {memoPageList.map((page, i) => { - const src = typeof page.src === 'string' ? page.src : page.src[0] - switch (page.position) { - case 'center': - return ( -
- -
- ) - case 'pair': - return ( -
- {/** 番号は右が若いページ、表示は左から処理するため入れ替えてる */} - - -
- ) - case 'left': - return ( -
-
- -
-
-
- ) - case 'right': - return ( -
-
-
- +
+ + + {mode === 'single' && + singlePageList.map((page, i) => ( + +
+
-
- ) - } - })} + + ))} + {mode === 'double' && + doublePageList.map((page, i) => { + switch (page.position) { + case 'center': + if (typeof page.src !== 'string') return
+ return ( + + + + ) + case 'pair': + if (typeof page.src === 'string') return
+ return ( + +
+
+ +
+
+ +
+
+
+ ) + case 'left': + if (typeof page.src !== 'string') return
+ return ( + +
+
+
+ +
+
+ + ) + case 'right': + if (typeof page.src !== 'string') return
+ return ( + +
+
+ +
+
+
+ + ) + } + })} + +
+ +
+
+ +
+
-
-

現在スマートフォン等の縦長画面には対応できていません

+
+
+ {mode === 'single' && ( +
+ { + const [v] = value + setCurrentPage(v) + api?.scrollTo(v) + }} + /> +

+ Page: {currentPage + 1}/{singlePageList.length} +

+
+ )} + {mode === 'double' && ( +
+ { + const [v] = value + setCurrentPage(v) + api?.scrollTo(v) + }} + /> +

+ Page: {currentPage + 1}/{doublePageList.length} +

+
+ )} +
+
+ + + + + +
+ + +
+
+ + {mode === 'double' && } + {mode === 'single' && } +
+
+
+
) diff --git a/pages/components/ui/carousel.tsx b/pages/components/ui/carousel.tsx new file mode 100644 index 0000000..fe87fc1 --- /dev/null +++ b/pages/components/ui/carousel.tsx @@ -0,0 +1,226 @@ +'use client' + +import * as React from 'react' +import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react' +import { ArrowLeft, ArrowRight } from 'lucide-react' + +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: 'horizontal' | 'vertical' + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error('useCarousel must be used within a ') + } + + return context +} + +const Carousel = React.forwardRef & CarouselProps>( + ({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault() + scrollPrev() + } else if (event.key === 'ArrowRight') { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on('reInit', onSelect) + api.on('select', onSelect) + + return () => { + api?.off('select', onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = 'Carousel' + +const CarouselContent = React.forwardRef>( + ({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) + } +) +CarouselContent.displayName = 'CarouselContent' + +const CarouselItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) + } +) +CarouselItem.displayName = 'CarouselItem' + +const CarouselPrevious = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) + } +) +CarouselPrevious.displayName = 'CarouselPrevious' + +const CarouselNext = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) + } +) +CarouselNext.displayName = 'CarouselNext' + +export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } diff --git a/pages/components/ui/label.tsx b/pages/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/pages/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/pages/components/ui/popover.tsx b/pages/components/ui/popover.tsx new file mode 100644 index 0000000..a0ec48b --- /dev/null +++ b/pages/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/pages/components/ui/slider.tsx b/pages/components/ui/slider.tsx new file mode 100644 index 0000000..4622e78 --- /dev/null +++ b/pages/components/ui/slider.tsx @@ -0,0 +1,25 @@ +'use client' + +import * as React from 'react' +import * as SliderPrimitive from '@radix-ui/react-slider' + +import { cn } from '@/lib/utils' + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/pages/components/ui/switch.tsx b/pages/components/ui/switch.tsx new file mode 100644 index 0000000..bc69cf2 --- /dev/null +++ b/pages/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/pages/lib/hook/use_window_size.ts b/pages/lib/hook/use_window_size.ts new file mode 100644 index 0000000..5286108 --- /dev/null +++ b/pages/lib/hook/use_window_size.ts @@ -0,0 +1,19 @@ +'use client' + +import { useLayoutEffect, useState } from 'react' + +export default function useWindowSize() { + const [windowSize, setWindowSize] = useState<[number, number]>([0, 0]) + useLayoutEffect(() => { + const updateSize = () => { + setWindowSize([window.innerWidth, window.innerHeight]) + } + + window.addEventListener('resize', updateSize) + updateSize() + + return () => window.removeEventListener('resize', updateSize) + }, []) + + return windowSize +} diff --git a/pages/package.json b/pages/package.json index e608ea7..8b57de2 100644 --- a/pages/package.json +++ b/pages/package.json @@ -15,9 +15,14 @@ "dependencies": { "@cloudflare/next-on-pages": "^1.13.7", "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-slider": "^1.2.2", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "embla-carousel-react": "^8.5.2", "lucide-react": "^0.471.0", "next": "15.1.4", "react": "^19", From a45dc6b5d07ae9a24d2922aebb361ab67c2cb862 Mon Sep 17 00:00:00 2001 From: maretol Date: Mon, 13 Jan 2025 17:50:45 +0900 Subject: [PATCH 2/2] remove unuse import --- pages/components/middle/comicbook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/components/middle/comicbook.tsx b/pages/components/middle/comicbook.tsx index 6c5f698..32bbdf5 100644 --- a/pages/components/middle/comicbook.tsx +++ b/pages/components/middle/comicbook.tsx @@ -9,7 +9,7 @@ import ComicImage from '../small/comic_image' import { cn } from '@/lib/utils' import Link from 'next/link' import { bandeDessineeResult } from 'api-types' -import { Carousel, CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '../ui/carousel' +import { Carousel, CarouselApi, CarouselContent, CarouselItem } from '../ui/carousel' import useWindowSize from '@/lib/hook/use_window_size' import { Switch } from '../ui/switch' import { Label } from '../ui/label'