Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/features/edit-profile/api/updateProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// import { instance } from '@/shared/api/ky'

// type UpdateProfilePayload = {
// bio: string | null
// profileImageUrl: string | null
// }

// export async function updateProfile(payload: UpdateProfilePayload) {
// const response = await instance.patch('api/v1/users/me', {
// json: payload,
// })

// if (!response.ok) {
// throw new Error('Failed to update profile')
// }
// }
5 changes: 5 additions & 0 deletions src/features/edit-profile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { ProfileEditForm } from './ui/ProfileEditForm'
export { ProfileEditCard } from './ui/ProfileEditCard'
export { BioTextarea } from './ui/BioTextarea'
export { WebsiteField } from './ui/WebsiteField'
export { ChangePhotoModal } from './ui/ChangePhotoModal'
39 changes: 39 additions & 0 deletions src/features/edit-profile/ui/BioTextarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Textarea } from '@/shared/ui/textarea'

type BioTextareaProps = {
value: string
onChange: (value: string) => void
maxLength?: number
}

const DEFAULT_MAX_LENGTH = 255

export function BioTextarea({
value,
onChange,
maxLength = DEFAULT_MAX_LENGTH,
}: BioTextareaProps) {
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
if (newValue.length <= maxLength) {
onChange(newValue)
}
}

return (
<div className="flex flex-col gap-2">
<label className="text-base font-semibold text-gray-900">소개</label>
<div className="relative">
<Textarea
value={value}
onChange={handleChange}
placeholder="소개"
className="min-h-[80px] resize-none rounded-xl border-gray-200 bg-gray-50 pr-16"
/>
<span className="absolute right-3 bottom-3 text-sm text-gray-400">
{value.length} / {maxLength}
</span>
</div>
</div>
)
}
65 changes: 65 additions & 0 deletions src/features/edit-profile/ui/ChangePhotoModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/shared/ui/dialog'

type ChangePhotoModalProps = {
open: boolean
onOpenChange: (open: boolean) => void
onUpload: () => void
onDelete: () => void
}

export function ChangePhotoModal({
open,
onOpenChange,
onUpload,
onDelete,
}: ChangePhotoModalProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-md gap-0 rounded-2xl bg-white p-0"
showCloseButton={false}
>
<DialogHeader className="border-b border-gray-200 p-6">
<DialogTitle className="text-center text-xl font-semibold">
프로필 사진 바꾸기
</DialogTitle>
</DialogHeader>

<button
type="button"
className="w-full cursor-pointer border-b border-gray-200 py-4 text-center text-base font-semibold text-blue-500"
onClick={() => {
onUpload()
onOpenChange(false)
}}
>
사진 업로드
</button>

<button
type="button"
className="w-full cursor-pointer border-b border-gray-200 py-4 text-center text-base font-semibold text-red-500"
onClick={() => {
onDelete()
onOpenChange(false)
}}
>
현재 사진 삭제
</button>

<button
type="button"
className="w-full cursor-pointer py-4 text-center text-base text-gray-900"
onClick={() => onOpenChange(false)}
>
취소
</button>
</DialogContent>
</Dialog>
)
}
41 changes: 41 additions & 0 deletions src/features/edit-profile/ui/ProfileEditCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar'
import { Button } from '@/shared/ui/button'

type ProfileEditCardProps = {
avatarUrl?: string | null
nickname: string
name?: string | null
onChangePhotoClick: () => void
}

export function ProfileEditCard({
avatarUrl,
nickname,
name,
onChangePhotoClick,
}: ProfileEditCardProps) {
return (
<div className="flex items-center justify-between rounded-2xl bg-gray-100 p-4">
<div className="flex items-center gap-4">
<Avatar
className="size-14 cursor-pointer text-gray-500"
onClick={onChangePhotoClick}
>
<AvatarImage src={avatarUrl ?? undefined} alt={nickname} />
<AvatarFallback>{nickname.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="font-semibold text-gray-900">{nickname}</span>
{name && <span className="text-sm text-gray-500">{name}</span>}
</div>
</div>
<Button
type="button"
className="rounded-lg bg-blue-500 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-600"
onClick={onChangePhotoClick}
>
사진 변경
</Button>
</div>
)
}
133 changes: 133 additions & 0 deletions src/features/edit-profile/ui/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useRef, useState } from 'react'
import { useMutation } from '@tanstack/react-query'
// import { toast } from 'sonner'
import {
useCurrentUser,
// useInvalidateCurrentUser,
} from '@/shared/auth/useCurrentUser'
import { cn } from '@/shared/lib/utils'
import { uploadImages } from '@/features/create-post/api/uploadImages'
// import { updateProfile } from '../api/updateProfile'
import { ProfileEditCard } from './ProfileEditCard'
import { WebsiteField } from './WebsiteField'
import { BioTextarea } from './BioTextarea'
import { ChangePhotoModal } from './ChangePhotoModal'

export function ProfileEditForm() {
const { data: currentUser } = useCurrentUser()
// const invalidateCurrentUser = useInvalidateCurrentUser()
const [bio, setBio] = useState(currentUser?.bio ?? '')
const [avatarUrl, setAvatarUrl] = useState<string | null>(
currentUser?.profileImageUrl ?? null
)
const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)

const originalBio = currentUser?.bio ?? ''
const originalAvatarUrl = currentUser?.profileImageUrl ?? null
const hasChanges = bio !== originalBio || avatarUrl !== originalAvatarUrl

const uploadMutation = useMutation({
mutationFn: uploadImages,
onSuccess: (urls) => {
setAvatarUrl(urls[0])
},
})

// const updateMutation = useMutation({
// mutationFn: updateProfile,
// onSuccess: () => {
// invalidateCurrentUser()
// toast.success('프로필이 저장되었습니다.')
// },
// onError: () => {
// toast.error('프로필 저장에 실패했습니다.')
// },
// })

// const handleSubmit = () => {
// updateMutation.mutate({
// bio: bio || null,
// profileImageUrl: avatarUrl,
// })
// }

const handleUploadPhoto = () => {
fileInputRef.current?.click()
}

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
uploadMutation.mutate([file])
}
e.target.value = ''
}

const handleDeletePhoto = () => {
setAvatarUrl(null)
}

if (!currentUser) {
return null
}

return (
<div className="flex flex-col gap-8">
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png"
className="hidden"
onChange={handleFileChange}
/>

<ChangePhotoModal
open={isPhotoModalOpen}
onOpenChange={setIsPhotoModalOpen}
onUpload={handleUploadPhoto}
onDelete={handleDeletePhoto}
/>

<ProfileEditCard
avatarUrl={avatarUrl}
nickname={currentUser.nickname}
name={currentUser.name}
onChangePhotoClick={() => setIsPhotoModalOpen(true)}
/>

<WebsiteField />

<BioTextarea value={bio} onChange={setBio} />

<p className="text-sm text-gray-500">
회원님의 이름, 소개, 링크와 같은 특정 프로필 정보가 모든 사람에게
공개됩니다.{' '}
<a
href="https://help.instagram.com/347751748650214?ref=igweb"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
공개되는 프로필 정보를 확인해보세요
</a>
</p>

<div className="flex justify-end">
<button
type="button"
disabled={!hasChanges}
// onClick={handleSubmit}
className={cn(
'w-1/2 rounded-xl py-3 text-sm font-semibold transition-colors',
hasChanges
? 'cursor-pointer bg-blue-500 text-white hover:bg-blue-600'
: 'cursor-default bg-blue-200 text-white'
)}
>
제출
</button>
</div>
</div>
)
}
23 changes: 23 additions & 0 deletions src/features/edit-profile/ui/WebsiteField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Input } from '@/shared/ui/input'

type WebsiteFieldProps = {
value?: string
}

export function WebsiteField({ value = '' }: WebsiteFieldProps) {
return (
<div className="flex cursor-not-allowed flex-col gap-2">
<label className="text-base font-semibold text-gray-900">웹사이트</label>
<Input
value={value}
placeholder="웹사이트"
disabled
className="rounded-xl border-gray-200 bg-gray-50 py-5"
/>
<p className="text-sm text-gray-500">
링크 수정은 모바일에서만 가능합니다. Instagram 앱으로 이동하여 프로필의
소개에서 웹사이트를 변경하여 수정하세요.
</p>
</div>
)
}
18 changes: 18 additions & 0 deletions src/pages/ProfileEditPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ProfileEditForm } from '@/features/edit-profile'
import { AppFooter } from '@/shared/ui/app-footer'

export function ProfileEditPage() {
return (
<div className="flex flex-1 flex-col px-20 py-10 2xl:px-52">
<div className="mx-auto w-full max-w-2xl">
<h1 className="mb-8 text-xl font-semibold text-gray-900">
프로필 편집
</h1>
<ProfileEditForm />
</div>
<div className="mt-auto">
<AppFooter />
</div>
</div>
)
}
Loading