昨天介紹如何使用 cloundinary
今天繼續介紹 page
部分。
register page
需要注意的是每當 user
上傳圖片時,需要即時顯示照片,然後當 form submit
時打 register api
並同時把圖片上傳到 cloundinary
,然後根據 uploadImageToCloudinary
return profile url
然後在 insert
到 user
的 photo
中。
uploadImageToCloudinary
: 圖片上傳到 cloundinary
,並呼叫 cloundinary
的 upload api
,然後 api
會 return
的 info
如 interface UploadImageApiResponse
。
interface UploadImageApiResponse {
asset_id: string;
public_id: string;
version: number;
version_id: string;
signature: string;
width: number;
height: number;
format: string;
resource_type: string;
created_at: string;
tags: any[];
bytes: number;
type: string;
etag: string;
placeholder: boolean;
url: string;
secure_url: string;
folder: string;
access_mode: string;
existing: boolean;
}
handleRegisterProfile
: 先呼叫 uploadImageToCloudinary
等他 return profile sourceURL
就 call
register api
。
api.auth.registerUser.useMutation
: 等到 onSuccess
就導頁到 login page
。
import { zodResolver } from '@hookform/resolvers/zod'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { ChangeEvent, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'react-toastify'
import { FileUpload } from '~/client/components/FileUpload'
import { FormInput } from '~/client/components/FormInput'
import { LoadingButton } from '~/client/components/LoadingButton'
import { useStore } from '~/client/store'
import { CreateUserFormSchema, createUserFormSchema } from '~/server/schema/user.schema'
import { api } from '~/utils/trpc'
function RegisterPage() {
const router = useRouter()
const { mutateAsync: registerUser, isLoading } = api.auth.registerUser.useMutation({
onSuccess: () => {
toast.success('success register user')
router.push('/login')
},
onError: (e) => {
toast.error(e.message)
}
})
const [avatar, setAvatar] = useState<string>()
const { setUPloadImage } = useStore()
const { register, handleSubmit, reset, formState: { errors }, getValues, watch } = useForm<CreateUserFormSchema>({
resolver: zodResolver(createUserFormSchema),
mode: 'onChange'
})
const onSubmitHandler = async (data: CreateUserFormSchema) => {
const { photo } = data
const url = await uploadImageToCloudinary(photo)
registerUser({
name: data.name,
password: data.password,
email: data.email,
photo: url || ''
})
}
const uploadImageToCloudinary = async (image: ((string | false | File) & (string | false | File | undefined)) | null) => {
if (!image || typeof image === 'string') return
let formdata = new FormData()
formdata.append("file", image);
formdata.append("upload_preset", process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET);
formdata.append("public_id", image.name);
formdata.append("api_key", process.env.NEXT_PUBLIC_CLOUDINARY_APIKEY);
formdata.append("tags", 'profile');
formdata.append("folder", getValues('name'));
setUPloadImage(true)
const result: UploadImageApiResponse = await fetch(`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_NAME}/image/upload`, {
method: 'POST',
body: formdata,
redirect: 'follow'
})
.then(response => response.json())
.catch(error => console.log('error', error))
.finally(() => {
setUPloadImage(false)
})
return result.secure_url
}
const handleRegisterProfile = async (e: ChangeEvent<HTMLInputElement>) => {
await register('photo').onChange(e)
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
if (!reader.result) return
setAvatar(reader.result?.toString())
}
}
return (
<section className="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
<div className="w-full">
<h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
Welcome
</h1>
<h2 className="text-lg text-center mb-4 text-ct-dark-200">
Sign Up To Get Started!
</h2>
<form
onSubmit={handleSubmit(onSubmitHandler)}
className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
>
<FormInput
register={register}
error={errors.name}
label="Full Name"
name="name"
/>
<FormInput
register={register}
error={errors.email}
label='email'
name='email'
/>
<FileUpload
register={register}
name='photo'
error={errors.photo}
label='profile'
onChange={handleRegisterProfile}
/>
{(avatar && !errors.photo) && (
<div className='w-[100px] h-[100px] rounded-xl overflow-hidden relative'>
<img src={avatar} alt="" className='object-cover w-full h-full ' />
</div>
)}
<FormInput
register={register}
error={errors.password}
label='password'
name='password'
/>
<FormInput
register={register}
error={errors.passwordConfirm}
label='confirm password'
name='passwordConfirm'
/>
<span className="block">
Already have an account?{" "}
<Link href="/login" className="text-ct-blue-600">
Login Here
</Link>
</span>
<LoadingButton loading={isLoading} textColor="text-ct-blue-600">
Sign Up
</LoadingButton>
</form>
</div>
</section>
)
}
export default RegisterPage
完整 demo
註冊成功導入 login page
api.auth.loginUser
: onSuccess
後 set access_token
到 useStore
中,然後導到 profile page
。
import Link from 'next/link'
import React from 'react'
import { FormInput } from '~/client/components/FormInput'
import { LoadingButton } from '~/client/components/LoadingButton'
import { useForm } from 'react-hook-form'
import { loginUserSchema, LoginUserSchema } from '~/server/schema/user.schema'
import { zodResolver } from '@hookform/resolvers/zod'
import { api } from '~/utils/trpc'
import { toast } from 'react-toastify'
import { useRouter } from 'next/router'
import { useStore } from '~/client/store'
function LoginPage() {
const { setAccessToken, access_token } = useStore()
const router = useRouter()
const { register, handleSubmit, formState: { errors } } = useForm<LoginUserSchema>({
resolver: zodResolver(loginUserSchema),
mode: 'onChange'
})
const { mutateAsync: userLogin, isLoading } = api.auth.loginUser.useMutation({
onSuccess: (data) => {
toast.success('success login ')
setAccessToken(data.access_token)
router.push('/profile')
},
onError: (e) => {
toast.error(e.message)
}
})
const onSubmitHandler = async (data: LoginUserSchema) => {
userLogin(data)
}
return (
<section className="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
<div className="w-full">
<h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
Welcome Back
</h1>
<h2 className="text-lg text-center mb-4 text-ct-dark-200">
Login to have access
</h2>
<form
onSubmit={handleSubmit(onSubmitHandler)}
className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
>
<FormInput
register={register}
error={errors.email}
label="email"
name="email"
/>
<FormInput
register={register}
error={errors.password}
label="password"
name="password"
/>
<LoadingButton loading={isLoading} textColor="text-ct-blue-600">
Login
</LoadingButton>
<span className="block">
Need an account?{" "}
<Link href="/register" className="text-ct-blue-600">
Sign Up Here
</Link>
</span>
</form>
</div>
</section>
)
}
export default LoginPage
getMe
: 每當進入 Profile page
時候都會先打 getMe
然後渲染 user info
。setImage
: 只要 user
成功 upload image
,同步更新 getMe
的 query dta
。
備註 : 在 getMe api
中用了refetchOnWindowFocus
跟refetchOnMount
,其目的是不希望 user
每次切換頁面都重新打 api
,主要是優化 UX
部分,減少 loading indictor
。
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'
import { api } from '~/utils/trpc'
import { useStore } from '~/client/store';
import { uploadFormSchema } from '~/server/schema/user.schema';
import { toast } from 'react-toastify';
function ProfilePage() {
const apiContext = api.useContext()
const { mutateAsync: setProfileImageUrl } = api.user.setImage.useMutation({
onSuccess: () => {
apiContext.user.getMe.invalidate()
toast.success('success update user profile')
}
})
const { data } = api.user.getMe.useQuery(undefined, {
refetchOnWindowFocus: false,
refetchOnMount: false,
trpc: {
context: {
skipBatch: true
}
}
})
const user = useMemo(() => data?.data.user, [data])
const { setPageLoading, setAccessToken, access_token } = useStore()
const uploadImageToCloudinary = async (e: ChangeEvent<HTMLInputElement>) => {
const image = e.target.files?.[0]
if (!image) return
const validateResult = uploadFormSchema.safeParse({ photo: e.target.files })
if (!validateResult.success) return
let formdata = new FormData()
formdata.append("file", image);
formdata.append("upload_preset", process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET);
formdata.append("public_id", image.name);
formdata.append("api_key", process.env.NEXT_PUBLIC_CLOUDINARY_APIKEY);
formdata.append("tags", 'profile');
formdata.append("folder", user?.name as string);
setPageLoading(true)
const result: UploadImageApiResponse = await fetch(`https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_NAME}/image/upload`, {
method: 'POST',
body: formdata,
redirect: 'follow',
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => response.json())
.catch(error => console.log('error', error))
.finally(() => {
setPageLoading(false)
})
setProfileImageUrl({ url: result.secure_url ?? 'default.png' })
}
return (
<>
<section className="bg-ct-blue-600 min-h-screen pt-20">
<div className="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-min-[20rem] flex justify-center items-center gap-4 p-4">
<div className='flex-1 flex flex-col items-center justify-center gap-4'>
<div className=' w-[200px] h-[200px] rounded-full overflow-hidden relative bg-red-500/20'>
<img className='object-cover w-full h-full ' src={user?.photo || 'default.png'} />
</div>
<div>
<label htmlFor='upload' className='cursor-pointer text-sm mb-2 text-slate-500 hover:text-white mr-4 py-2 px-4 rounded-full border-0 text-sm font-semibold bg-violet-200 text-violet-700 hover:bg-violet-400'>
edit
</label>
<input
id="upload"
type="file"
className='hidden'
onChange={uploadImageToCloudinary}
accept='image/*'
multiple={false}
/>
</div>
</div>
<div className='flex-1'>
<p className="text-5xl font-semibold">Profile Page</p>
<div className="mt-8">
<p className="mb-4">ID: {user?.id}</p>
<p className="mb-4">Name: {user?.name}</p>
<p className="mb-4">Email: {user?.email}</p>
<p className="mb-4">Role: {user?.role}</p>
</div>
</div>
</div>
</section>
</>
)
}
export default ProfilePage
但這時你發現你成功登入後,再回到 profile page
時候你會發現,user
還是可以進入,所以我們需要一個 validate
機制讓 user
不要造訪該頁面。
這邊我打算用 hook
方式來檢驗~
useRefreshToken
中主要會是透過打 refreshAccessToken api
方式去驗證 laccessToken
是否過期或是有沒有 validate
,error
就登出 success
就保留頁面狀態並同步更新 accessToken
。
import { useRouter } from "next/router"
import { useEffect } from "react"
import { useStore } from "~/client/store"
import { api } from "~/utils/trpc"
export const useRefreshToken = () => {
const { setAccessToken } = useStore()
const router = useRouter()
const { setPageLoading } = useStore()
const { data, isLoading, isError } = api.auth.refreshAccessToken.useQuery(undefined, {
trpc: {
context: {
skipBatch: true
}
}
})
useEffect(() => {
if (!data?.access_token) return
setAccessToken(data?.access_token)
}, [data?.access_token])
useEffect(() => {
setPageLoading(isLoading)
}, [isLoading])
useEffect(() => {
if (!isError) return
router.push('/login')
setAccessToken('')
}, [isError])
}
最後在 ProfilePage
加上 useRefreshToken
功能就完成摟~
function ProfilePage() {
useRefreshToken()
//..
https://github.com/Danny101201/refetch-token/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y