Skip to content

Commit

Permalink
refactor(fe): refactor settings page (#2128)
Browse files Browse the repository at this point in the history
* fix(fe): refactor save

* fix(fe): refactor settings

* chore(fe): change import path name

* fix(fe): change navigation

* chore(fe): fix typo

* chore(fe): delete import react

* chore(fe): delete import react

* chore(fe): use cn
  • Loading branch information
jihorobert authored Oct 2, 2024
1 parent 2191e86 commit b22bca3
Show file tree
Hide file tree
Showing 13 changed files with 686 additions and 368 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Route } from 'next'
import type { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useRouter } from 'next/navigation'
import type { MutableRefObject } from 'react'
import { useEffect } from 'react'
import { toast } from 'sonner'

// const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
// // Recommended
// event.preventDefault()

// // Included for legacy support, e.g. Chrome/Edge < 119
// event.returnValue = true
// return true
// }

/**
* Prompt the user with a confirmation dialog when they try to navigate away from the page.
*/
export const useConfirmNavigation = (
bypassConfirmation: MutableRefObject<boolean>,
updateNow: boolean
) => {
const router = useRouter()
useEffect(() => {
const originalPush = router.push
const newPush = (
href: string,
options?: NavigateOptions | undefined
): void => {
if (updateNow) {
if (!bypassConfirmation.current) {
toast.error('You must update your information')
} else {
originalPush(href as Route, options)
}
return
}
if (!bypassConfirmation.current) {
const isConfirmed = window.confirm(
'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?'
)
if (isConfirmed) {
originalPush(href as Route, options)
}
return
}
originalPush(href as Route, options)
}
router.push = newPush
return () => {
router.push = originalPush
}
}, [router, bypassConfirmation.current])
}
93 changes: 93 additions & 0 deletions apps/frontend/app/(main)/settings/_components/CurrentPwSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import invisible from '@/public/24_invisible.svg'
import visible from '@/public/24_visible.svg'
import type { SettingsFormat } from '@/types/type'
import Image from 'next/image'
import React from 'react'
import type { FieldErrors, UseFormRegister } from 'react-hook-form'
import { FaCheck } from 'react-icons/fa6'

interface CurrentPwSectionProps {
currentPassword: string
isCheckButtonClicked: boolean
isPasswordCorrect: boolean
setPasswordShow: React.Dispatch<React.SetStateAction<boolean>>
passwordShow: boolean
checkPassword: () => Promise<void>
register: UseFormRegister<SettingsFormat>
errors: FieldErrors<SettingsFormat>
updateNow: boolean
}

export default function CurrentPwSection({
currentPassword,
isCheckButtonClicked,
isPasswordCorrect,
setPasswordShow,
passwordShow,
checkPassword,
register,
errors,
updateNow
}: CurrentPwSectionProps) {
return (
<>
<label className="-mb-4 mt-4 text-xs">Password</label>
<div className="flex items-center gap-2">
<div className="relative w-full justify-between">
<Input
type={passwordShow ? 'text' : 'password'}
placeholder="Current password"
{...register('currentPassword')}
disabled={
updateNow ? true : isCheckButtonClicked && isPasswordCorrect
}
className={cn(
'flex justify-stretch border-neutral-300 text-neutral-600 ring-0 placeholder:text-neutral-400 focus-visible:ring-0 disabled:bg-neutral-200 disabled:text-neutral-400',
errors.currentPassword && 'border-red-500',
isCheckButtonClicked &&
(isPasswordCorrect ? 'border-primary' : 'border-red-500')
)}
/>
<span
className="absolute right-0 top-0 flex h-full items-center p-3"
onClick={() => setPasswordShow(!passwordShow)}
>
<Image
src={passwordShow ? visible : invisible}
alt={passwordShow ? 'visible' : 'invisible'}
/>
</span>
</div>
<Button
disabled={!currentPassword}
className="h-4/5 px-2 disabled:bg-neutral-400"
onClick={() => {
checkPassword()
}}
>
<FaCheck size={20} />
</Button>
</div>
{errors.currentPassword &&
errors.currentPassword.message === 'Required' && (
<div className="-mt-4 inline-flex items-center text-xs text-red-500">
Required
</div>
)}
{!errors.currentPassword &&
isCheckButtonClicked &&
(isPasswordCorrect ? (
<div className="text-primary -mt-4 inline-flex items-center text-xs">
Correct
</div>
) : (
<div className="-mt-4 inline-flex items-center text-xs text-red-500">
Incorrect
</div>
))}
</>
)
}
20 changes: 20 additions & 0 deletions apps/frontend/app/(main)/settings/_components/IdSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Input } from '@/components/ui/input'

export default function IdSection({
isLoading,
defaultUsername
}: {
isLoading: boolean
defaultUsername: string
}) {
return (
<>
<label className="-mb-4 text-xs">ID</label>
<Input
placeholder={isLoading ? 'Loading...' : defaultUsername}
disabled={true}
className="border-neutral-300 text-neutral-600 placeholder:text-neutral-400 disabled:bg-neutral-200"
/>
</>
)
}
22 changes: 22 additions & 0 deletions apps/frontend/app/(main)/settings/_components/LogoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import codedangSymbol from '@/public/codedang-editor.svg'
import Image from 'next/image'

export default function LogoSection() {
return (
<div
className="flex h-svh max-h-[846px] w-full flex-col items-center justify-center gap-3 rounded-2xl"
style={{
background: `var(--banner,
linear-gradient(325deg, rgba(79, 86, 162, 0.00) 28.16%, rgba(79, 86, 162, 0.50) 93.68%),
linear-gradient(90deg, #3D63B8 0%, #0E1322 100%)
)`
}}
>
<div className="flex items-center gap-3">
<Image src={codedangSymbol} alt="codedang" width={65} />
<p className="font-mono text-[40px] font-bold text-white">CODEDANG</p>
</div>
<p className="font-medium text-white">Online Judge Platform for SKKU</p>
</div>
)
}
107 changes: 107 additions & 0 deletions apps/frontend/app/(main)/settings/_components/MajorSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Button } from '@/components/ui/button'
import {
Command,
CommandInput,
CommandGroup,
CommandItem,
CommandList,
CommandEmpty
} from '@/components/ui/command'
import {
Popover,
PopoverTrigger,
PopoverContent
} from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { majors } from '@/lib/constants'
import { cn } from '@/lib/utils'
import React from 'react'
import { FaChevronDown, FaCheck } from 'react-icons/fa6'

interface MajorSectionProps {
majorOpen: boolean
setMajorOpen: React.Dispatch<React.SetStateAction<boolean>>
majorValue: string
setMajorValue: React.Dispatch<React.SetStateAction<string>>
updateNow: boolean
isLoading: boolean
defaultProfileValues: {
major?: string
}
}

export default function MajorSection({
majorOpen,
setMajorOpen,
majorValue,
setMajorValue,
updateNow,
isLoading,
defaultProfileValues
}: MajorSectionProps) {
return (
<>
<label className="-mb-4 mt-2 text-xs">First Major</label>
<div className="flex flex-col gap-1">
<Popover open={majorOpen} onOpenChange={setMajorOpen} modal={true}>
<PopoverTrigger asChild>
<Button
aria-expanded={majorOpen}
variant="outline"
role="combobox"
className={cn(
'justify-between border-gray-200 font-normal text-neutral-600 hover:bg-white',
updateNow
? `${majorValue === 'none' || isLoading ? 'border-red-500 text-neutral-400' : 'border-primary'}`
: majorValue === defaultProfileValues.major
? 'text-neutral-400'
: 'border-primary'
)}
>
{isLoading
? 'Loading...'
: updateNow
? majorValue === 'none'
? 'Department Information Unavailable / 학과 정보 없음'
: majorValue
: !majorValue
? defaultProfileValues.major
: majorValue}
<FaChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[555px] p-0">
<Command>
<CommandInput placeholder="Search major..." />
<ScrollArea className="h-40">
<CommandEmpty>No major found.</CommandEmpty>
<CommandGroup>
<CommandList>
{majors?.map((major) => (
<CommandItem
key={major}
value={major}
onSelect={(currentValue) => {
setMajorValue(currentValue)
setMajorOpen(false)
}}
>
<FaCheck
className={cn(
'mr-2 h-4 w-4',
majorValue === major ? 'opacity-100' : 'opacity-0'
)}
/>
{major}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
</div>
</>
)
}
46 changes: 46 additions & 0 deletions apps/frontend/app/(main)/settings/_components/NameSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import type { SettingsFormat } from '@/types/type'
import type { FieldErrors, UseFormRegister } from 'react-hook-form'

interface NameSectionProps {
isLoading: boolean
updateNow: boolean
defaultProfileValues: { userProfile?: { realName?: string } }
register: UseFormRegister<SettingsFormat>
errors: FieldErrors<SettingsFormat>
realName: string
}

export default function NameSection({
isLoading,
updateNow,
defaultProfileValues,
register,
errors,
realName
}: NameSectionProps) {
return (
<>
<label className="-mb-4 text-xs">Name</label>
<Input
placeholder={
isLoading
? 'Loading...'
: defaultProfileValues.userProfile?.realName || 'Enter your name'
}
disabled={!!updateNow}
{...register('realName')}
className={cn(
realName && (errors.realName ? 'border-red-500' : 'border-primary'),
'placeholder:text-neutral-400 focus-visible:ring-0 disabled:bg-neutral-200'
)}
/>
{realName && errors.realName && (
<div className="-mt-4 inline-flex items-center text-xs text-red-500">
{errors.realName.message}
</div>
)}
</>
)
}
Loading

0 comments on commit b22bca3

Please sign in to comment.