From 1d98fd5b87170bcb840d098640a60f3f43742cdc Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 7 Dec 2023 03:31:00 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/elements/HeaderImage.tsx | 17 ++++++++++ .../_components/elements/UserIcon.tsx | 20 ++++++++++++ .../_components/layouts/ProfileCard.tsx | 31 ++----------------- 3 files changed, 40 insertions(+), 28 deletions(-) create mode 100644 src/app/(menu)/(public)/[username]/_components/elements/HeaderImage.tsx create mode 100644 src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx diff --git a/src/app/(menu)/(public)/[username]/_components/elements/HeaderImage.tsx b/src/app/(menu)/(public)/[username]/_components/elements/HeaderImage.tsx new file mode 100644 index 00000000..839eab75 --- /dev/null +++ b/src/app/(menu)/(public)/[username]/_components/elements/HeaderImage.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { styled } from '@mui/material'; + +const HeaderImage = styled('div')<{ + image?: string; +}>` + display: block; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + aspect-ratio: 3 / 1; + background-color: ${({ theme }) => theme.palette.primary.light}; + background-image: ${({ image }) => (image ? `url(${image})` : 'none')}; +`; + +export default HeaderImage; diff --git a/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx b/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx new file mode 100644 index 00000000..a1e5d3d4 --- /dev/null +++ b/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { Avatar, styled } from '@mui/material'; + +const UserIcon = styled(Avatar)` + width: 120px; + height: 120px; + + margin-top: -80px; + border-color: ${({ theme }) => theme.palette.background.paper}; + border-style: solid; + + ${({ theme }) => theme.breakpoints.down('tablet')} { + width: 80px; + height: 80px; + margin-top: -48px; + } +`; + +export default UserIcon; diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx index c79a5632..555f2127 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx @@ -1,12 +1,14 @@ 'use client'; -import { Avatar, Box, Typography, styled } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import { FollowButton, FollowStatus, } from '@/app/(menu)/(public)/[username]/_components/elements/FollowButton'; import UserCount from '@/app/(menu)/(public)/[username]/_components/elements/UserCount'; import { usePathname } from 'next/navigation'; +import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; +import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; const UnselectableCard = styled('div')` border-bottom: 1px solid ${({ theme }) => theme.palette.grey[100]}; @@ -14,18 +16,6 @@ const UnselectableCard = styled('div')` color: rgba(0, 0, 0, 0.87); `; -const HeaderImage = styled('div')<{ - image?: string; -}>` - display: block; - background-size: cover; - background-repeat: no-repeat; - background-position: center; - aspect-ratio: 3 / 1; - background-color: ${({ theme }) => theme.palette.primary.light}; - background-image: ${({ image }) => (image ? `url(${image})` : 'none')}; -`; - const Flex = styled(Box)` display: flex; flex-wrap: nowrap; @@ -43,21 +33,6 @@ const FillFlex = styled(Box)` flex-grow: 1; `; -const UserIcon = styled(Avatar)` - width: 120px; - height: 120px; - - margin-top: -80px; - border-color: ${({ theme }) => theme.palette.background.paper}; - border-style: solid; - - ${({ theme }) => theme.breakpoints.down('tablet')} { - width: 80px; - height: 80px; - margin-top: -48px; - } -`; - const DisplayName = styled(Typography)` word-wrap: break-word; font-weight: bold; From 383b3274d78aa59e2c8ec3864153f49d594e9b33 Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 7 Dec 2023 03:37:32 +0900 Subject: [PATCH 02/21] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=AE=E5=A4=A7=E3=81=8D?= =?UTF-8?q?=E3=81=95=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(public)/[username]/_components/elements/UserIcon.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx b/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx index a1e5d3d4..d861babb 100644 --- a/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx +++ b/src/app/(menu)/(public)/[username]/_components/elements/UserIcon.tsx @@ -11,9 +11,9 @@ const UserIcon = styled(Avatar)` border-style: solid; ${({ theme }) => theme.breakpoints.down('tablet')} { - width: 80px; - height: 80px; - margin-top: -48px; + width: 92px; + height: 92px; + margin-top: -61px; } `; From d598eb19745f17bb01b57a155df1395a0ae7e1dd Mon Sep 17 00:00:00 2001 From: takecchi Date: Fri, 8 Dec 2023 14:28:06 +0900 Subject: [PATCH 03/21] =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=82=A2=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=97=E3=81=A6=E3=83=A1?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=82=A2=E3=82=92=E7=B7=A8=E9=9B=86=E3=81=BE?= =?UTF-8?q?=E3=81=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layouts/ProfileSettingModal.tsx | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx new file mode 100644 index 00000000..c89e2076 --- /dev/null +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { Box, Dialog as MuiDialog, styled } from '@mui/material'; +import { ChangeEvent, useCallback, useState } from 'react'; +import { AddAPhoto, Close, ArrowBack } from '@mui/icons-material'; +import { IconButton } from '@/app/_components/button/IconButton'; +import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; +import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; + +const Dialog = styled(MuiDialog)` + .MuiDialog-paper { + margin: 0; + max-width: 100vw; + max-height: 100vh; + + ${({ theme }) => theme.breakpoints.down('tablet')} { + border-radius: 0; + } + } +`; + +const Container = styled('div')` + display: flex; + flex-direction: column; + text-align: center; + + ${({ theme }) => theme.breakpoints.down('tablet')} { + width: 100vw; + height: 100vh; + } +`; + +const Header = styled('div')` + display: flex; + align-items: center; + border-style: solid; + border-color: ${({ theme }) => theme.palette.grey[100]}; + border-width: 0; + border-bottom-width: 1px; + color: ${({ theme }) => theme.palette.grey[800]}; + min-height: 50px; + padding: 0 8px; + gap: 12px; +`; + +const Content = styled('div')` + max-width: 598px; + //width: 630px; + width: 100vw; +`; + +const Flex = styled(Box)` + display: flex; + flex-wrap: nowrap; +`; + +const HFlex = styled(Flex)` + flex-direction: row; +`; + +function ProfileImageCrop({ + src, + onClose, +}: { + src: string | undefined; + onClose: () => void; +}) { + return ( + + +
+ + + + メディアを編集 +
+ + {/* TODO ここでCrop出来るようにする */} + Crop me + +
+
+ ); +} + +export default function ProfileSettingModal({ open: init }: { open: boolean }) { + const [open, setOpen] = useState(init); + + const [iconSrc, setIconSrc] = useState(undefined); + + function handleClose() { + setOpen(false); + } + + const handleFileChange = useCallback((e: ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const reader = new FileReader(); + reader.addEventListener('load', () => + setIconSrc(reader.result?.toString() || undefined), + ); + reader.readAsDataURL(e.target.files[0]); + } + }, []); + + return ( + <> + { + setOpen(true); + setIconSrc(undefined); + }} + /> + + +
+ + + + プロフィールを編集 +
+ + +
+ + {/* TODO アイコン */} + + + + + +
+
+
+
+ + ); +} From c70461cefeef3077498bb1489b94f08dc78c754b Mon Sep 17 00:00:00 2001 From: takecchi Date: Sat, 9 Dec 2023 08:42:22 +0900 Subject: [PATCH 04/21] =?UTF-8?q?react-easy-crop=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 24 +++++++++++++++++ package.json | 1 + .../layouts/ProfileSettingModal.tsx | 27 +++++++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f26b890a..a41e8748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "next": "^13.5.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-easy-crop": "^5.0.2", "swr": "^2.2.1", "virtua": "^0.17.4" }, @@ -24185,6 +24186,11 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==" + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -25708,6 +25714,24 @@ "react": "^18.2.0" } }, + "node_modules/react-easy-crop": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.2.tgz", + "integrity": "sha512-j4A/0s0v/Gx5YGXvw3SOFIMmRk5YCdob2ABL5cD00Q9HQPKIz6tkCYLdj0RMO0REPtCAOsZ2ZZLI6fUofiDP6w==", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, + "node_modules/react-easy-crop/node_modules/tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + }, "node_modules/react-element-to-jsx-string": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", diff --git a/package.json b/package.json index 79ddd810..05aded94 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "next": "^13.5.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-easy-crop": "^5.0.2", "swr": "^2.2.1", "virtua": "^0.17.4" }, diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index c89e2076..15318dee 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -6,6 +6,7 @@ import { AddAPhoto, Close, ArrowBack } from '@mui/icons-material'; import { IconButton } from '@/app/_components/button/IconButton'; import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; +import Cropper, { Point } from 'react-easy-crop'; const Dialog = styled(MuiDialog)` .MuiDialog-paper { @@ -45,10 +46,19 @@ const Header = styled('div')` const Content = styled('div')` max-width: 598px; - //width: 630px; width: 100vw; `; +const CropContainer = styled('div')` + position: relative; + + height: calc(100vh - 50px); + + ${({ theme }) => theme.breakpoints.up('tablet')} { + max-height: 600px; + } +`; + const Flex = styled(Box)` display: flex; flex-wrap: nowrap; @@ -65,6 +75,9 @@ function ProfileImageCrop({ src: string | undefined; onClose: () => void; }) { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + return ( @@ -76,7 +89,17 @@ function ProfileImageCrop({ {/* TODO ここでCrop出来るようにする */} - Crop me + + + From 39ec26b18751f68aeec38d7962fa437be72ad435 Mon Sep 17 00:00:00 2001 From: takecchi Date: Sat, 9 Dec 2023 09:50:25 +0900 Subject: [PATCH 05/21] =?UTF-8?q?=E3=82=B9=E3=83=A9=E3=82=A4=E3=83=80?= =?UTF-8?q?=E3=83=BC=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layouts/ProfileSettingModal.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index 15318dee..f6ab2c10 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -1,13 +1,22 @@ 'use client'; -import { Box, Dialog as MuiDialog, styled } from '@mui/material'; +import { Box, Dialog as MuiDialog, Slider, styled } from '@mui/material'; import { ChangeEvent, useCallback, useState } from 'react'; -import { AddAPhoto, Close, ArrowBack } from '@mui/icons-material'; +import { + AddAPhoto, + Close, + ArrowBack, + ZoomIn, + ZoomOut, +} from '@mui/icons-material'; import { IconButton } from '@/app/_components/button/IconButton'; import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; import Cropper, { Point } from 'react-easy-crop'; +const HEADER_HEIGHT = '50px'; +const SLIDER_HEIGHT = '50px'; + const Dialog = styled(MuiDialog)` .MuiDialog-paper { margin: 0; @@ -39,7 +48,7 @@ const Header = styled('div')` border-width: 0; border-bottom-width: 1px; color: ${({ theme }) => theme.palette.grey[800]}; - min-height: 50px; + height: ${HEADER_HEIGHT}; padding: 0 8px; gap: 12px; `; @@ -49,14 +58,26 @@ const Content = styled('div')` width: 100vw; `; +const SliderContainer = styled('div')` + display: flex; + flex-direction: row; + align-items: center; + height: ${SLIDER_HEIGHT}; + padding: 0 30px; + gap: 10px; +`; + const CropContainer = styled('div')` position: relative; - - height: calc(100vh - 50px); + height: calc(100vh - ${HEADER_HEIGHT} - ${SLIDER_HEIGHT}); ${({ theme }) => theme.breakpoints.up('tablet')} { max-height: 600px; } + + .crop-area { + border: 3px solid #00a0ff; + } `; const Flex = styled(Box)` @@ -67,7 +88,6 @@ const Flex = styled(Box)` const HFlex = styled(Flex)` flex-direction: row; `; - function ProfileImageCrop({ src, onClose, @@ -97,9 +117,26 @@ function ProfileImageCrop({ aspect={1 / 1} onCropChange={setCrop} onZoomChange={setZoom} + classes={{ + cropAreaClassName: 'crop-area', + }} showGrid={false} /> + + + { + setZoom(newValue as number); + }} + min={1} + max={3} + step={0.1} + /> + + From d226e15e32c993a399794d295cd747b3b82ae700 Mon Sep 17 00:00:00 2001 From: takecchi Date: Sat, 9 Dec 2023 14:56:00 +0900 Subject: [PATCH 06/21] =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=81=AE=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=BE=E3=81=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layouts/ProfileSettingModal.tsx | 52 +++++++++++++++---- .../(public)/[username]/_utils/cropImage.ts | 52 +++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 src/app/(menu)/(public)/[username]/_utils/cropImage.ts diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index f6ab2c10..4d42affc 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -12,7 +12,9 @@ import { import { IconButton } from '@/app/_components/button/IconButton'; import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; -import Cropper, { Point } from 'react-easy-crop'; +import Cropper, { Area, Point } from 'react-easy-crop'; +import CapsuleButton from '@/app/_components/button/CapsuleButton'; +import { getCroppedImg } from '@/app/(menu)/(public)/[username]/_utils/cropImage'; const HEADER_HEIGHT = '50px'; const SLIDER_HEIGHT = '50px'; @@ -88,15 +90,30 @@ const Flex = styled(Box)` const HFlex = styled(Flex)` flex-direction: row; `; + function ProfileImageCrop({ src, onClose, + onComplete, }: { src: string | undefined; onClose: () => void; + onComplete: (blob: Blob) => void; }) { const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(); + + const handleSave = useCallback(async () => { + if (!croppedAreaPixels || !src) return; + + try { + const croppedImage = await getCroppedImg(src, croppedAreaPixels, 400); + onComplete(croppedImage); + } catch (e) { + console.error(e); + } + }, [croppedAreaPixels]); return ( @@ -106,6 +123,17 @@ function ProfileImageCrop({ メディアを編集 +
+ { + // FIXME 可能ならここは非同期で投げっぱなしではなく、ちゃんと処理を待機させたい + void handleSave(); + }} + > + 保存 + {/* TODO ここでCrop出来るようにする */} @@ -114,19 +142,22 @@ function ProfileImageCrop({ image={src} crop={crop} zoom={zoom} - aspect={1 / 1} + aspect={1} onCropChange={setCrop} onZoomChange={setZoom} classes={{ cropAreaClassName: 'crop-area', }} + onCropComplete={(_, area) => { + setCroppedAreaPixels(area); + }} showGrid={false} /> { setZoom(newValue as number); @@ -146,6 +177,9 @@ function ProfileImageCrop({ export default function ProfileSettingModal({ open: init }: { open: boolean }) { const [open, setOpen] = useState(init); + const [src, setSrc] = useState( + 'https://media.develop.cuculus.jp/profile_images/d691e7ec-5622-42a1-92c6-1f89bff87acd.png', + ); const [iconSrc, setIconSrc] = useState(undefined); function handleClose() { @@ -170,6 +204,11 @@ export default function ProfileSettingModal({ open: init }: { open: boolean }) { setOpen(true); setIconSrc(undefined); }} + onComplete={(blob) => { + const croppedImageUrl = URL.createObjectURL(blob); + setSrc(croppedImageUrl); + setIconSrc(undefined); + }} /> @@ -184,12 +223,7 @@ export default function ProfileSettingModal({ open: init }: { open: boolean }) {
{/* TODO アイコン */} - + { + const image = new Image(); + image.src = imageSrc; + await new Promise((resolve) => { + image.onload = resolve; + }); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // トリミングされた画像のサイズを取得 + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + const cropWidth = area.width * scaleX; + const cropHeight = area.height * scaleY; + + // キャンバスのサイズを設定 + canvas.width = Math.min(cropWidth, maxSize); + canvas.height = Math.min(cropHeight, maxSize); + + // トリミングされた画像をキャンバスに描画 + if (ctx) { + ctx.drawImage( + image, + area.x * scaleX, + area.y * scaleY, + cropWidth, + cropHeight, + 0, + 0, + canvas.width, + canvas.height, + ); + } + + // キャンバスの内容をBlobとして取得 (PNG形式) + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Canvas is empty')); + return; + } + resolve(blob); + }, 'image/png'); + }); +} From 56e51cb59057e9262ac74492d99883d7e77e55ba Mon Sep 17 00:00:00 2001 From: takecchi Date: Sat, 9 Dec 2023 14:56:31 +0900 Subject: [PATCH 07/21] =?UTF-8?q?gif=E3=81=AF=E4=B8=80=E6=97=A6=E3=82=B5?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E5=A4=96=E3=81=AB=E3=80=82=E5=88=A5?= =?UTF-8?q?=E9=80=94=E5=8F=A3=E3=82=92=E7=94=A8=E6=84=8F=E3=81=99=E3=82=8B?= =?UTF-8?q?=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/layouts/ProfileSettingModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index 4d42affc..88b26712 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -234,7 +234,7 @@ export default function ProfileSettingModal({ open: init }: { open: boolean }) { From 007ce653e666a4a93ec6b9b6d0c8f5bdbd9610ec Mon Sep 17 00:00:00 2001 From: takecchi Date: Sat, 9 Dec 2023 14:58:30 +0900 Subject: [PATCH 08/21] =?UTF-8?q?=E3=82=AF=E3=83=AD=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=A8=E3=83=AA=E3=82=A2=E3=82=92=E4=B8=B8=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/layouts/ProfileSettingModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index 88b26712..adff1c12 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -78,6 +78,7 @@ const CropContainer = styled('div')` } .crop-area { + border-radius: 9999px; border: 3px solid #00a0ff; } `; From 03bda899256c72ac8cfae9faaea9bdc863261e9d Mon Sep 17 00:00:00 2001 From: takecchi Date: Sun, 10 Dec 2023 14:57:48 +0900 Subject: [PATCH 09/21] =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E5=90=8D=E3=81=A8?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E7=B4=B9=E4=BB=8B=E6=96=87=E3=82=92=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layouts/ProfileSettingModal.tsx | 99 ++++++++++++++++--- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index adff1c12..1aecd5ff 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -1,6 +1,12 @@ 'use client'; -import { Box, Dialog as MuiDialog, Slider, styled } from '@mui/material'; +import { + Box, + Dialog as MuiDialog, + Slider, + styled, + TextField, +} from '@mui/material'; import { ChangeEvent, useCallback, useState } from 'react'; import { AddAPhoto, @@ -92,6 +98,17 @@ const HFlex = styled(Flex)` flex-direction: row; `; +const VFlex = styled(Flex)` + flex-direction: column; +`; + +/** + * アイコンを編集するモーダル + * @param src + * @param onClose + * @param onComplete + * @constructor + */ function ProfileImageCrop({ src, onClose, @@ -104,17 +121,21 @@ function ProfileImageCrop({ const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(); + const [isProcessing, setIsProcessing] = useState(false); - const handleSave = useCallback(async () => { + // 適用処理 + const handleApply = useCallback(async () => { if (!croppedAreaPixels || !src) return; - + setIsProcessing(true); try { const croppedImage = await getCroppedImg(src, croppedAreaPixels, 400); onComplete(croppedImage); } catch (e) { console.error(e); + } finally { + setIsProcessing(false); } - }, [croppedAreaPixels]); + }, [croppedAreaPixels, onComplete, src]); return ( @@ -127,13 +148,13 @@ function ProfileImageCrop({
{ - // FIXME 可能ならここは非同期で投げっぱなしではなく、ちゃんと処理を待機させたい - void handleSave(); + void handleApply(); }} > - 保存 + 適用 @@ -175,12 +196,27 @@ function ProfileImageCrop({ ); } -export default function ProfileSettingModal({ open: init }: { open: boolean }) { - const [open, setOpen] = useState(init); +/** + * プロフィールを編集するモーダル + * @param init + * @constructor + */ +export default function ProfileSettingModal({ + open: initOpen, + src: initSrc, + displayName: initDisplayName, + bio: initBio, +}: { + open: boolean; + src?: string; + displayName: string; + bio: string; +}) { + const [open, setOpen] = useState(initOpen); + const [src, setSrc] = useState(initSrc); + const [displayName, setDisplayName] = useState(initDisplayName); + const [bio, setBio] = useState(initBio); - const [src, setSrc] = useState( - 'https://media.develop.cuculus.jp/profile_images/d691e7ec-5622-42a1-92c6-1f89bff87acd.png', - ); const [iconSrc, setIconSrc] = useState(undefined); function handleClose() { @@ -218,12 +254,21 @@ export default function ProfileSettingModal({ open: init }: { open: boolean }) { プロフィールを編集 +
+ { + // TODO 保存処理 + }} + > + 保存 +
- {/* TODO アイコン */} + + { + setDisplayName(event.target.value); + }} + /> + { + setBio(event.target.value); + }} + /> +
From 5f0c14f621647017db2c91ed003d2037460fd6be Mon Sep 17 00:00:00 2001 From: takecchi Date: Mon, 11 Dec 2023 11:20:12 +0900 Subject: [PATCH 10/21] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AE=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/FollowButton.stories.ts | 2 +- .../_components/elements/FollowButton.tsx | 78 ++----------------- 2 files changed, 9 insertions(+), 71 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.stories.ts b/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.stories.ts index 9ac0c847..9b50d55d 100644 --- a/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.stories.ts +++ b/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.stories.ts @@ -13,6 +13,6 @@ type Story = StoryObj; export const NormalFollowButton: Story = { args: { userId: 123, - followStatus: 'NotFollowing', + isFollowing: true, }, }; diff --git a/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.tsx b/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.tsx index 20abd7d0..9f9217dd 100644 --- a/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.tsx +++ b/src/app/(menu)/(public)/[username]/_components/elements/FollowButton.tsx @@ -1,83 +1,21 @@ 'use client'; import CapsuleButton from '@/app/_components/button/CapsuleButton'; -import { ButtonTypeMap } from '@mui/material'; -import { MouseEventHandler } from 'react'; -import { OverridableStringUnion } from '@mui/types'; -import { ButtonPropsVariantOverrides } from '@mui/material/Button'; -// static class propertyにして文字列で持たせる?(interfaceどうするか)(enum使う?) -export type FollowStatus = - | 'NotFollowing' - | 'Following' - | 'Pending' - | 'Blocked' - | 'EditProfile'; - -interface Props { +type Props = { userId: number; - followStatus: FollowStatus; -} - -export function FollowButton({ followStatus }: Props) { - // TODO ボタン処理実装 - const follow: MouseEventHandler = () => { - // doPost(followActionUrl) - }; - - // TODO ボタン処理実装 - const unfollow: MouseEventHandler = () => { - // doDelete(followActionUrl); - }; - - // TODO ボタン処理実装 - const cancelRequest: MouseEventHandler = () => { - // doCancel(???); - }; - - const editProfile: MouseEventHandler = () => { - // editProfile(???); - }; + isFollowing: boolean; +}; - const [color, enabled, text, onClick, variant] = ((): [ - ButtonTypeMap['props']['color'], - boolean, - string, - MouseEventHandler | undefined, - OverridableStringUnion< - 'text' | 'outlined' | 'contained', - ButtonPropsVariantOverrides - >, - ] => { - switch (followStatus) { - case 'NotFollowing': - return ['primary', true, 'フォロー', unfollow, 'contained']; - case 'Following': - return ['primary', true, 'フォロー中', follow, 'outlined']; - case 'Pending': - return ['secondary', true, '承認待ち', cancelRequest, 'outlined']; - case 'Blocked': - return [ - 'warning', - false, - 'ブロックされています', - undefined, - 'outlined', - ]; - case 'EditProfile': - return ['primary', true, 'プロフィールを編集', editProfile, 'outlined']; - default: - return ['error', false, '(invalid value)', undefined, 'outlined']; - } - })(); +export function FollowButton({ isFollowing }: Props) { + const text = isFollowing ? 'フォロー中' : 'フォロー'; return ( {text} From 5da63e5e093e041f772d043b46bf0fa5218f13f4 Mon Sep 17 00:00:00 2001 From: takecchi Date: Mon, 11 Dec 2023 11:20:58 +0900 Subject: [PATCH 11/21] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=82=92=E7=B7=A8=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/ProfilePage.tsx | 18 +------- .../_components/layouts/EditProfileButton.tsx | 38 ++++++++++++++++ .../_components/layouts/ProfileCard.tsx | 43 ++++++------------- .../layouts/ProfileSettingModal.tsx | 15 +++---- 4 files changed, 61 insertions(+), 53 deletions(-) create mode 100644 src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx diff --git a/src/app/(menu)/(public)/[username]/_components/ProfilePage.tsx b/src/app/(menu)/(public)/[username]/_components/ProfilePage.tsx index 856c3c4c..241f056f 100644 --- a/src/app/(menu)/(public)/[username]/_components/ProfilePage.tsx +++ b/src/app/(menu)/(public)/[username]/_components/ProfilePage.tsx @@ -12,7 +12,7 @@ type Props = { }; export default function ProfilePage({ fallbackData }: Props) { const { data, isLoading } = useUser(fallbackData.username, fallbackData); - const { data: authId, isLoading: authorizing } = useAuth(); + const { data: authId } = useAuth(); if (!data) { // FIXME 読み込み中 return <>; @@ -20,21 +20,7 @@ export default function ProfilePage({ fallbackData }: Props) { return ( - + {!isLoading && } ); diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx new file mode 100644 index 00000000..f6106502 --- /dev/null +++ b/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx @@ -0,0 +1,38 @@ +'use client'; + +import CapsuleButton from '@/app/_components/button/CapsuleButton'; +import ProfileSettingModal from '@/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal'; +import { useState } from 'react'; + +type Props = { + src: string; + displayName: string; + bio: string; +}; + +export function EditProfileButton({ src, displayName, bio }: Props) { + const text = 'プロフィールを編集'; + const [open, setOpen] = useState(false); + + return ( + <> + { + setOpen(true); + }} + variant="outlined" + > + {text} + + setOpen(false)} + /> + + ); +} diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx index 555f2127..b1a31a5b 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx @@ -1,14 +1,13 @@ 'use client'; import { Box, Typography, styled } from '@mui/material'; -import { - FollowButton, - FollowStatus, -} from '@/app/(menu)/(public)/[username]/_components/elements/FollowButton'; +import { FollowButton } from '@/app/(menu)/(public)/[username]/_components/elements/FollowButton'; import UserCount from '@/app/(menu)/(public)/[username]/_components/elements/UserCount'; import { usePathname } from 'next/navigation'; import HeaderImage from '@/app/(menu)/(public)/[username]/_components/elements/HeaderImage'; import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/UserIcon'; +import { UserWithFollows } from '@cuculus/cuculus-api'; +import { EditProfileButton } from '@/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton'; const UnselectableCard = styled('div')` border-bottom: 1px solid ${({ theme }) => theme.palette.grey[100]}; @@ -49,21 +48,9 @@ const Bio = styled(Typography)` margin-bottom: 12px; `; -interface ProfileCardProps { - id: number; - name: string; - username: string; - createdAt: Date; - bio: string; - profileImageUrl: string; - protected: boolean; - url: string; - verified: boolean; - followersCount?: number; - followingCount?: number; +type ProfileCardProps = { authId: number | undefined; - authorizing: boolean; -} +} & UserWithFollows; export default function ProfileCard({ id, @@ -74,16 +61,10 @@ export default function ProfileCard({ followersCount, followingCount, authId, - authorizing, }: ProfileCardProps) { const path = usePathname(); - const getFollowStatus = (): FollowStatus => { - if (id === authId) { - return 'EditProfile'; - } - return 'NotFollowing'; - }; + const isMe = id === authId; return ( <> @@ -128,10 +109,14 @@ export default function ProfileCard({ {/* */} {/*)}*/} {/* フォローボタン */} - {!authorizing && ( - + )} + {authId && isMe && ( + )} diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index 1aecd5ff..7ed03e85 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -202,26 +202,26 @@ function ProfileImageCrop({ * @constructor */ export default function ProfileSettingModal({ - open: initOpen, + open, + onClose, src: initSrc, displayName: initDisplayName, bio: initBio, }: { open: boolean; + onClose: () => void; src?: string; displayName: string; bio: string; }) { - const [open, setOpen] = useState(initOpen); const [src, setSrc] = useState(initSrc); const [displayName, setDisplayName] = useState(initDisplayName); const [bio, setBio] = useState(initBio); - const [iconSrc, setIconSrc] = useState(undefined); - function handleClose() { - setOpen(false); - } + const handleClose = () => { + onClose(); + }; const handleFileChange = useCallback((e: ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { @@ -238,7 +238,6 @@ export default function ProfileSettingModal({ { - setOpen(true); setIconSrc(undefined); }} onComplete={(blob) => { @@ -247,7 +246,7 @@ export default function ProfileSettingModal({ setIconSrc(undefined); }} /> - +
From bc28f22b3857e23d28908e0936bf59b7764390ae Mon Sep 17 00:00:00 2001 From: takecchi Date: Mon, 11 Dec 2023 11:24:07 +0900 Subject: [PATCH 12/21] =?UTF-8?q?storybook=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/layouts/ProfileCard.stories.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.stories.ts b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.stories.ts index dfad2eae..68cf3f28 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.stories.ts +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.stories.ts @@ -22,13 +22,12 @@ export const NormalProfileCard: Story = { username: 'takecchi', createdAt: new Date('2023-10-04T18:57:44.373Z'), profileImageUrl: '/mock/profileAvatarImage.png', - protected: false, + _protected: false, verified: false, bio: 'こんにちは。', url: '', followersCount: 2, followingCount: 1, authId: undefined, - authorizing: false, }, }; From d9347057a32a48b36c6be1bc78447336b3eb3e6d Mon Sep 17 00:00:00 2001 From: takecchi Date: Mon, 11 Dec 2023 17:37:19 +0900 Subject: [PATCH 13/21] =?UTF-8?q?=E5=AE=9F=E8=A3=85=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layouts/ProfileSettingModal.tsx | 52 ++++++++++++++++- .../button/CapsuleLoadingButton.tsx | 16 +++++ src/libs/cuculus-client.ts | 3 + src/swr/client/profile.ts | 58 +++++++++++++++++++ 4 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/app/_components/button/CapsuleLoadingButton.tsx create mode 100644 src/swr/client/profile.ts diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index 7ed03e85..f6bd3370 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -1,9 +1,11 @@ 'use client'; import { + Alert, Box, Dialog as MuiDialog, Slider, + Snackbar, styled, TextField, } from '@mui/material'; @@ -21,6 +23,8 @@ import UserIcon from '@/app/(menu)/(public)/[username]/_components/elements/User import Cropper, { Area, Point } from 'react-easy-crop'; import CapsuleButton from '@/app/_components/button/CapsuleButton'; import { getCroppedImg } from '@/app/(menu)/(public)/[username]/_utils/cropImage'; +import { useProfileMutation } from '@/swr/client/profile'; +import CapsuleLoadingButton from '@/app/_components/button/CapsuleLoadingButton'; const HEADER_HEIGHT = '50px'; const SLIDER_HEIGHT = '50px'; @@ -215,9 +219,14 @@ export default function ProfileSettingModal({ bio: string; }) { const [src, setSrc] = useState(initSrc); + const [blob, setBlob] = useState(undefined); const [displayName, setDisplayName] = useState(initDisplayName); const [bio, setBio] = useState(initBio); const [iconSrc, setIconSrc] = useState(undefined); + const { trigger, isMutating } = useProfileMutation(); + + const [errorMessage, setErrorMesssage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); const handleClose = () => { onClose(); @@ -243,6 +252,7 @@ export default function ProfileSettingModal({ onComplete={(blob) => { const croppedImageUrl = URL.createObjectURL(blob); setSrc(croppedImageUrl); + setBlob(blob); setIconSrc(undefined); }} /> @@ -254,15 +264,36 @@ export default function ProfileSettingModal({ プロフィールを編集
- { - // TODO 保存処理 + const request = { + bio: initBio !== bio ? bio : undefined, + name: + initDisplayName !== displayName ? displayName : undefined, + profileImage: blob, + }; + const isAllUndefined = Object.values(request).every( + (value) => value === undefined, + ); + if (isAllUndefined) { + handleClose(); + } else { + void trigger(request) + .then(() => { + setSuccessMessage('プロフィールを更新しました。'); + handleClose(); + }) + .catch(() => { + setErrorMesssage('プロフィールの更新に失敗しました。'); + }); + } }} + loading={isMutating} > 保存 - +
@@ -323,6 +354,21 @@ export default function ProfileSettingModal({
+ + setErrorMesssage('')} + autoHideDuration={2_000} + > + {errorMessage} + + setSuccessMessage('')} + autoHideDuration={2_000} + > + {successMessage} + ); } diff --git a/src/app/_components/button/CapsuleLoadingButton.tsx b/src/app/_components/button/CapsuleLoadingButton.tsx new file mode 100644 index 00000000..c373ab68 --- /dev/null +++ b/src/app/_components/button/CapsuleLoadingButton.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { styled } from '@mui/material'; +import { LoadingButton as MuiLoadingButton } from '@mui/lab'; + +const CapsuleButton = styled(MuiLoadingButton)` + border-radius: 9999px; + box-shadow: none; + + &:hover, + &:focus { + box-shadow: none; + } +`; + +export default CapsuleButton; diff --git a/src/libs/cuculus-client.ts b/src/libs/cuculus-client.ts index 284aef30..b04f447b 100644 --- a/src/libs/cuculus-client.ts +++ b/src/libs/cuculus-client.ts @@ -1,4 +1,5 @@ import { + AccountsApi, AuthApi, Configuration, DefaultApi, @@ -18,6 +19,7 @@ const defaultApi = new DefaultApi(config); const invitationsApi = new InvitationsApi(config); const timelinesApi = new TimelinesApi(config); const postsApi = new PostsApi(config); +const accountsApi = new AccountsApi(config); export { authApi, @@ -26,4 +28,5 @@ export { invitationsApi, timelinesApi, postsApi, + accountsApi, }; diff --git a/src/swr/client/profile.ts b/src/swr/client/profile.ts new file mode 100644 index 00000000..bc458f90 --- /dev/null +++ b/src/swr/client/profile.ts @@ -0,0 +1,58 @@ +import { getAuthorizationHeader } from '@/libs/auth'; +import { accountsApi } from '@/libs/cuculus-client'; +import { useAuth } from '@/swr/client/auth'; +import useSWRMutation from 'swr/mutation'; +import { UserWithFollows } from '@cuculus/cuculus-api/dist/models'; + +type SWRKey = { + key: string; + authId: number; +}; + +type Arg = { + name?: string; + bio?: string; + profileImage?: Blob; +}; + +const update = async ( + key: SWRKey, + { arg }: { arg: Arg }, +): Promise => { + const headers = { + ...(await getAuthorizationHeader(key.authId)), + // 'Content-Type': 'application/json', + }; + + let user: UserWithFollows | undefined = undefined; + + if (arg.profileImage) { + user = await accountsApi.updateProfileImage( + { file: arg.profileImage }, + { headers }, + ); + } + if (arg.bio || arg.name) { + // TODO SDKの更新待ち + user = await accountsApi.updateProfile( + { + updateProfile: { name: arg.name, bio: arg.bio as unknown as object }, + }, + { headers }, + ); + } + if (user) { + return user; + } else { + throw new Error('更新に失敗しました。'); + } +}; + +export const useProfileMutation = () => { + const { data: authId } = useAuth(); + const key = authId ? { key: 'useProfile', authId } : null; + return useSWRMutation( + key, + update, + ); +}; From 00883160ef53e32e3313976cd2213961da00ad5b Mon Sep 17 00:00:00 2001 From: takecchi Date: Tue, 12 Dec 2023 14:06:42 +0900 Subject: [PATCH 14/21] =?UTF-8?q?storybook=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 46 ++++++++++++++++++++++--------------------- .storybook/preview.ts | 2 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 4dc884ff..a95f3c9a 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,29 +22,31 @@ const config: StorybookConfig = { autodocs: true, }, webpackFinal: async (config) => { - /** - * FIXME 良い方法があればそっちに変更したい。 - * SVGに関するルールを削除 - */ - config.module.rules = config.module.rules.map((rule) => { - if ( - rule && - rule !== '...' && - rule.test instanceof RegExp && - rule.test.test('.svg') - ) { - return undefined; - } - return rule; - }); + if (config.module && config.module.rules) { + /** + * FIXME 良い方法があればそっちに変更したい。 + * SVGに関するルールを削除 + */ + config.module.rules = config.module.rules.map((rule) => { + if ( + rule && + rule !== '...' && + rule.test instanceof RegExp && + rule.test.test('.svg') + ) { + return undefined; + } + return rule; + }); - config.module.rules.push({ - test: /\.svg$/, - issuer: { - and: [/\.(js|ts)x?$/], - }, - use: ['@svgr/webpack'], - }); + config.module.rules.push({ + test: /\.svg$/, + issuer: { + and: [/\.(js|ts)x?$/], + }, + use: ['@svgr/webpack'], + }); + } if (config.resolve?.alias) { config.resolve.alias = { ...config.resolve.alias, diff --git a/.storybook/preview.ts b/.storybook/preview.ts index b00b0f34..4119bd2d 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import type { Preview } from '@storybook/react'; -import theme from '@/theme/theme'; +import theme from '../src/theme/theme'; import { withThemeFromJSXProvider } from '@storybook/addon-styling'; import { CssBaseline, ThemeProvider } from '@mui/material'; import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; From e791a084a90d9fc856fe55c86a55ca2c38eade54 Mon Sep 17 00:00:00 2001 From: takecchi Date: Tue, 12 Dec 2023 14:53:58 +0900 Subject: [PATCH 15/21] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E7=B7=A8=E9=9B=86API=E3=82=92=E5=8F=A9?= =?UTF-8?q?=E3=81=8F=E5=87=A6=E7=90=86=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 8 ++++---- package.json | 2 +- src/swr/client/profile.ts | 25 +++++++++++++------------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index a41e8748..f5dfb779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "cuculus", "version": "0.1.0", "dependencies": { - "@cuculus/cuculus-api": "^0.4.0", + "@cuculus/cuculus-api": "^0.4.1", "@ducanh2912/next-pwa": "^9.7.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", @@ -4748,9 +4748,9 @@ } }, "node_modules/@cuculus/cuculus-api": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cuculus/cuculus-api/-/cuculus-api-0.4.0.tgz", - "integrity": "sha512-wgHfW4RfQfINS6/5elfdDcrKGTk/fBe/mqkjzPDJKDepW0wvC2dy0QCjqWFeJ5ePQ+KnJ/NKMhjRvhYkQ8gOqQ==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cuculus/cuculus-api/-/cuculus-api-0.4.1.tgz", + "integrity": "sha512-ZL0MFw4J9I+GWtw5Iq/MK6pUTgBVBzGhVa/Duq5jSVP7kcYCJ0Fre82OxQcu6930WFkSD/0EBKOA3bR2nIH8zQ==" }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", diff --git a/package.json b/package.json index 05aded94..c515d43f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ } }, "dependencies": { - "@cuculus/cuculus-api": "^0.4.0", + "@cuculus/cuculus-api": "^0.4.1", "@ducanh2912/next-pwa": "^9.7.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/src/swr/client/profile.ts b/src/swr/client/profile.ts index bc458f90..fa2ba4b4 100644 --- a/src/swr/client/profile.ts +++ b/src/swr/client/profile.ts @@ -19,25 +19,26 @@ const update = async ( key: SWRKey, { arg }: { arg: Arg }, ): Promise => { - const headers = { - ...(await getAuthorizationHeader(key.authId)), - // 'Content-Type': 'application/json', - }; + const headers = await getAuthorizationHeader(key.authId); let user: UserWithFollows | undefined = undefined; - if (arg.profileImage) { - user = await accountsApi.updateProfileImage( - { file: arg.profileImage }, - { headers }, - ); - } if (arg.bio || arg.name) { - // TODO SDKの更新待ち user = await accountsApi.updateProfile( { - updateProfile: { name: arg.name, bio: arg.bio as unknown as object }, + updateProfile: { name: arg.name, bio: arg.bio }, }, + { + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }, + ); + } + if (arg.profileImage) { + user = await accountsApi.updateProfileImage( + { file: arg.profileImage }, { headers }, ); } From bbc762efc7254113d73c3d399d4b4cd1264f904d Mon Sep 17 00:00:00 2001 From: takecchi Date: Wed, 13 Dec 2023 16:22:12 +0900 Subject: [PATCH 16/21] =?UTF-8?q?=E8=87=AA=E8=BA=AB=E3=81=AE=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=82=92=E5=8F=82=E7=85=A7=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/layouts/EditProfileButton.tsx | 20 +++++++++---------- .../_components/layouts/ProfileCard.tsx | 8 +------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx index f6106502..73a77844 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/EditProfileButton.tsx @@ -3,16 +3,16 @@ import CapsuleButton from '@/app/_components/button/CapsuleButton'; import ProfileSettingModal from '@/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal'; import { useState } from 'react'; +import { useProfile } from '@/swr/client/auth'; -type Props = { - src: string; - displayName: string; - bio: string; -}; - -export function EditProfileButton({ src, displayName, bio }: Props) { +export function EditProfileButton() { const text = 'プロフィールを編集'; const [open, setOpen] = useState(false); + const { data } = useProfile(); + + if (!data) { + return <>; + } return ( <> @@ -28,9 +28,9 @@ export function EditProfileButton({ src, displayName, bio }: Props) { setOpen(false)} /> diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx index b1a31a5b..203c8d8a 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileCard.tsx @@ -112,13 +112,7 @@ export default function ProfileCard({ {authId && !isMe && ( )} - {authId && isMe && ( - - )} + {authId && isMe && } From 2de912e2f23f07e07f926a1fdf1ef555e52f20ad Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 14 Dec 2023 13:20:27 +0900 Subject: [PATCH 17/21] =?UTF-8?q?bio=E3=81=AE=E7=A9=BA=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=82=92=E8=A8=B1=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/swr/client/profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swr/client/profile.ts b/src/swr/client/profile.ts index fa2ba4b4..18054656 100644 --- a/src/swr/client/profile.ts +++ b/src/swr/client/profile.ts @@ -23,7 +23,7 @@ const update = async ( let user: UserWithFollows | undefined = undefined; - if (arg.bio || arg.name) { + if (arg.bio != undefined || arg.name) { user = await accountsApi.updateProfile( { updateProfile: { name: arg.name, bio: arg.bio }, From ebd92bbc8b48db18d4e1b01a84ec8a87598e7b1f Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 14 Dec 2023 17:46:05 +0900 Subject: [PATCH 18/21] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(menu)/(public)/[username]/_utils/cropImage.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/(menu)/(public)/[username]/_utils/cropImage.ts b/src/app/(menu)/(public)/[username]/_utils/cropImage.ts index 4d26767b..06e72eb1 100644 --- a/src/app/(menu)/(public)/[username]/_utils/cropImage.ts +++ b/src/app/(menu)/(public)/[username]/_utils/cropImage.ts @@ -43,7 +43,11 @@ export async function getCroppedImg( return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (!blob) { - reject(new Error('Canvas is empty')); + reject( + new Error( + 'Failed to create blob from canvas. Canvas might be empty.', + ), + ); return; } resolve(blob); From 729446214b147d25a5bc3141d20fdb76bb6a5515 Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 14 Dec 2023 17:47:34 +0900 Subject: [PATCH 19/21] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=81=AE=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(menu)/(public)/[username]/_utils/cropImage.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/(menu)/(public)/[username]/_utils/cropImage.ts b/src/app/(menu)/(public)/[username]/_utils/cropImage.ts index 06e72eb1..0fe2c9c7 100644 --- a/src/app/(menu)/(public)/[username]/_utils/cropImage.ts +++ b/src/app/(menu)/(public)/[username]/_utils/cropImage.ts @@ -21,8 +21,17 @@ export async function getCroppedImg( const cropHeight = area.height * scaleY; // キャンバスのサイズを設定 - canvas.width = Math.min(cropWidth, maxSize); - canvas.height = Math.min(cropHeight, maxSize); + const aspectRatio = cropWidth / cropHeight; + if (cropWidth > maxSize) { + canvas.width = maxSize; + canvas.height = maxSize / aspectRatio; + } else if (cropHeight > maxSize) { + canvas.height = maxSize; + canvas.width = maxSize * aspectRatio; + } else { + canvas.width = cropWidth; + canvas.height = cropHeight; + } // トリミングされた画像をキャンバスに描画 if (ctx) { From 27ff0fa8e5c65de0cd7519e4fbba6f0a04ea7453 Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 14 Dec 2023 18:04:20 +0900 Subject: [PATCH 20/21] =?UTF-8?q?iOS=20Safari=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/layouts/ProfileSettingModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index f6bd3370..d27f9f4a 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -33,7 +33,9 @@ const Dialog = styled(MuiDialog)` .MuiDialog-paper { margin: 0; max-width: 100vw; - max-height: 100vh; + max-height: calc( + 100vh - env(safe-area-inset-bottom, 0) - env(safe-area-inset-top, 0) + ); ${({ theme }) => theme.breakpoints.down('tablet')} { border-radius: 0; From d07436559735a20fe87f35ba7f61b04bf0b9b829 Mon Sep 17 00:00:00 2001 From: takecchi Date: Thu, 14 Dec 2023 18:15:47 +0900 Subject: [PATCH 21/21] =?UTF-8?q?iOS=20Safari=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[username]/_components/layouts/ProfileSettingModal.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx index d27f9f4a..5324f3cd 100644 --- a/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx +++ b/src/app/(menu)/(public)/[username]/_components/layouts/ProfileSettingModal.tsx @@ -30,6 +30,8 @@ const HEADER_HEIGHT = '50px'; const SLIDER_HEIGHT = '50px'; const Dialog = styled(MuiDialog)` + top: env(safe-area-inset-top, 0); + .MuiDialog-paper { margin: 0; max-width: 100vw;