Skip to content

Commit

Permalink
Profile image upload (#94)
Browse files Browse the repository at this point in the history
* fix theme-toggler arrow functions

* fix form-auth card paddings

* add photo_profile and mingcute_camera icons

* add base profile login page and its file-input component

* bug do not work drag and drop

* add new cross icon

* add new profile name length constant

* add file input component

* add form profile component

* add profile page

* fix code style and add progress bar

* add progress shadcn component

* fix svgs colors display in file input component

* move progress timeout delay to constant

* swap bg colors in progress bar

* fix img tag to next image

* fix specially for shitheads that cant read through blank lines

* remove unnecessary styles from imagecard in auth page

* remove unnecessary styles from imagecard in profile page and move imagecard to profile page
  • Loading branch information
dEdmishka authored Jan 29, 2024
1 parent ada3a24 commit 469eed5
Show file tree
Hide file tree
Showing 12 changed files with 374 additions and 2 deletions.
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
Expand Down
2 changes: 2 additions & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ Roboto
subheadline
linecap
linejoin
mingcute
shadcn
clsx
2 changes: 1 addition & 1 deletion src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Auth() {
<section className="h-screen w-screen bg-gradient-blue-white">
<Content className="flex items-center justify-center dark:bg-none dark:bg-base-gray-8">
<div className="p-7 sm:p-10 flex gap-7 md:gap-[3.75rem] rounded-3xl bg-base-white dark:bg-base-black">
<ImageCard className="-my-[4.375rem] flex-shrink-0 hidden sm:flex items-center justify-center">
<ImageCard className="-my-[4.375rem] flex-shrink-0 hidden sm:flex pt-24 pr-8 pb-36 pl-5">
<Icons.golub />
</ImageCard>
<div>
Expand Down
26 changes: 26 additions & 0 deletions src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ImageCard from '@/components/pages/auth/image-card'
import FileInput from '@/components/pages/profile/file-input'
import FormProfile from '@/components/pages/profile/form-profile'
import Content from '@/components/ui/content'
import { ThemeToggler } from '@/components/ui/theme-toggler'

export default function Profile() {
return (
<section className="h-screen w-screen bg-gradient-blue-white">
<Content className="flex items-center justify-center dark:bg-none dark:bg-base-gray-8">
<div className="p-7 sm:p-10 flex gap-7 md:gap-[3.75rem] rounded-3xl bg-base-white dark:bg-base-black">
<ImageCard className="-my-[4.375rem] flex-shrink-0 hidden sm:flex py-32 px-[3.25rem]">
<FileInput />
</ImageCard>
<div>
<div className="flex justify-between pb-2 mt-8">
<h1 className="title-lg">Profile</h1>
<ThemeToggler />
</div>
<FormProfile className="mt-5" />
</div>
</div>
</Content>
</section>
)
}
2 changes: 1 addition & 1 deletion src/components/pages/auth/image-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const ImageCard: FC<ImageCardProps> = ({ children, className }) => {
return (
<div
className={classnames(
'pt-24 pr-8 pb-36 pl-5 flex items-center justify-center rounded-3xl bg-gradient-purple-blue',
'flex items-center justify-center rounded-3xl bg-gradient-purple-blue',
className
)}
>
Expand Down
108 changes: 108 additions & 0 deletions src/components/pages/profile/file-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use client'

import { Icons } from '@/components/ui/icons'
import { Progress } from '@/components/ui/progress'
import { PROGRESS_TIMEOUT_DELAY } from '@/constants'
import { cn } from '@/lib/utils'
import Image from 'next/image'
import { FC, FormEvent, useRef, useState } from 'react'

type FileInputProps = {
className?: string
}

const FileInput: FC<FileInputProps> = ({ className }) => {
const fileInputRef = useRef<HTMLInputElement>(null)

const [_, setImage] = useState<File | undefined>()

const [preview, setPreview] = useState<string | undefined>()

const [progress, setProgress] = useState<number>(0)

const clearSelectFile = () => {
setPreview(undefined)
}

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

const handleImageLoad = (file: File) => {
setImage(file)

const fileReader = new FileReader()

fileReader.readAsDataURL(file)

fileReader.onloadstart = () => {
setProgress(0)
}
fileReader.onprogress = (progress) => {
setProgress((progress.loaded / progress.total) * 100)
}
fileReader.onload = () => {
setPreview(fileReader.result as string)
setTimeout(() => setProgress(0), PROGRESS_TIMEOUT_DELAY)
}
}

const handleImageChange = (event: FormEvent<HTMLInputElement>) => {
const target = event.target as HTMLElement & {
files: FileList
}

if (target.files.length > 0) {
handleImageLoad(target.files[0])
}
}

return (
<div className={cn('relative', className)}>
{preview ? (
<div>
<div className="relative w-[144px] h-[144px] overflow-hidden rounded-3xl">
<Image alt="avatar" fill src={preview} />
</div>
<Icons.cross
className="absolute z-10 top-4 right-4 text-base-white dark:text-base-black"
onClick={clearSelectFile}
/>
</div>
) : (
<div>
<Icons.photo_profile className="text-base-gray-2 dark:text-base-black" />
</div>
)}
<div
className="absolute -bottom-4 right-1/2 translate-x-1/2"
onClick={handleSelectFile}
>
<Icons.mingcute_camera className="text-white dark:text-bright-indigo" />
</div>

<div
className={cn(
'absolute z-[11] top-1/2 left-1/2 w-full',
!progress && 'w-0'
)}
>
<Progress
className="w-5/6 relative mx-auto transition-[width] duration-500 ease-in-out -left-1/2"
value={progress}
/>
</div>

<input
className="absolute block h-full w-full top-0 opacity-0 rounded-3xl"
name="image"
onChange={handleImageChange}
ref={fileInputRef}
type="file"
/>
<span className="sr-only">File input</span>
</div>
)
}

export default FileInput
72 changes: 72 additions & 0 deletions src/components/pages/profile/form-profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client'

import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { PROFILE_NAME_LENGTH } from '@/constants'
import { classnames } from '@/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import * as z from 'zod'

type FormProfileProps = { className?: string }

const formSchema = z.object({
profileName: z
.string()
.min(PROFILE_NAME_LENGTH, 'The field must not be empty'),
})

const ProfileForm: FC<FormProfileProps> = ({ className }) => {
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
profileName: '',
},
resolver: zodResolver(formSchema),
})

function onSubmit(values: z.infer<typeof formSchema>) {
alert(JSON.stringify(values))
console.log(values)
}

return (
<Form {...form}>
<form
className={classnames('space-y-8 pr-[9.75rem]', className)}
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="profileName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className="sm:min-w-[343px]"
id="profile_name"
placeholder="Profile Name"
type="text"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="!mt-8 w-full" type="submit">
Submit
</Button>
</form>
</Form>
)
}

export default ProfileForm
Loading

0 comments on commit 469eed5

Please sign in to comment.