From 469eed56eee692bc51fe452fdfb41cee1051da69 Mon Sep 17 00:00:00 2001 From: Melnyk Mykhailo <91133955+dEdmishka@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:10:15 +0200 Subject: [PATCH] Profile image upload (#94) * 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 --- package-lock.json | 25 ++++ package.json | 1 + project-words.txt | 2 + src/app/auth/page.tsx | 2 +- src/app/profile/page.tsx | 26 +++++ src/components/pages/auth/image-card.tsx | 2 +- src/components/pages/profile/file-input.tsx | 108 ++++++++++++++++++ src/components/pages/profile/form-profile.tsx | 72 ++++++++++++ src/components/ui/icons.tsx | 108 ++++++++++++++++++ src/components/ui/progress.tsx | 27 +++++ src/constants/index.ts | 1 + src/constants/profile.tsx | 2 + 12 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 src/app/profile/page.tsx create mode 100644 src/components/pages/profile/file-input.tsx create mode 100644 src/components/pages/profile/form-profile.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/constants/profile.tsx diff --git a/package-lock.json b/package-lock.json index 333026a..6d2f3be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,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", @@ -1377,6 +1378,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz", + "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", diff --git a/package.json b/package.json index 36ebc3f..892f831 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/project-words.txt b/project-words.txt index fd18891..dcfb3c9 100644 --- a/project-words.txt +++ b/project-words.txt @@ -8,4 +8,6 @@ Roboto subheadline linecap linejoin +mingcute +shadcn clsx diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index 4052d90..ad2fb6a 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -9,7 +9,7 @@ export default function Auth() {
- +
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..fdeb527 --- /dev/null +++ b/src/app/profile/page.tsx @@ -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 ( +
+ +
+ + + +
+
+

Profile

+ +
+ +
+
+
+
+ ) +} diff --git a/src/components/pages/auth/image-card.tsx b/src/components/pages/auth/image-card.tsx index 839a019..05e1269 100644 --- a/src/components/pages/auth/image-card.tsx +++ b/src/components/pages/auth/image-card.tsx @@ -10,7 +10,7 @@ const ImageCard: FC = ({ children, className }) => { return (
diff --git a/src/components/pages/profile/file-input.tsx b/src/components/pages/profile/file-input.tsx new file mode 100644 index 0000000..0e177fd --- /dev/null +++ b/src/components/pages/profile/file-input.tsx @@ -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 = ({ className }) => { + const fileInputRef = useRef(null) + + const [_, setImage] = useState() + + const [preview, setPreview] = useState() + + const [progress, setProgress] = useState(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) => { + const target = event.target as HTMLElement & { + files: FileList + } + + if (target.files.length > 0) { + handleImageLoad(target.files[0]) + } + } + + return ( +
+ {preview ? ( +
+
+ avatar +
+ +
+ ) : ( +
+ +
+ )} +
+ +
+ +
+ +
+ + + File input +
+ ) +} + +export default FileInput diff --git a/src/components/pages/profile/form-profile.tsx b/src/components/pages/profile/form-profile.tsx new file mode 100644 index 0000000..71e5288 --- /dev/null +++ b/src/components/pages/profile/form-profile.tsx @@ -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 = ({ className }) => { + const form = useForm>({ + defaultValues: { + profileName: '', + }, + resolver: zodResolver(formSchema), + }) + + function onSubmit(values: z.infer) { + alert(JSON.stringify(values)) + console.log(values) + } + + return ( +
+ + ( + + + + + + + )} + /> + + + + ) +} + +export default ProfileForm diff --git a/src/components/ui/icons.tsx b/src/components/ui/icons.tsx index dd51db1..4d546d3 100644 --- a/src/components/ui/icons.tsx +++ b/src/components/ui/icons.tsx @@ -25,6 +25,21 @@ export const Icons = { ) }, + cross: (props: IconProps) => ( + + + + ), golub: (props: IconProps) => ( ), + mingcute_camera: (props: IconProps) => ( + + + + + ), + photo_profile: (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + ), } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..4a42cea --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,27 @@ +'use client' + +import { cn } from '@/lib/utils' +import * as ProgressPrimitive from '@radix-ui/react-progress' +import * as React from 'react' + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/constants/index.ts b/src/constants/index.ts index be93b81..164b4ad 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,2 +1,3 @@ export * from './env' +export * from './profile' export * from './auth' diff --git a/src/constants/profile.tsx b/src/constants/profile.tsx new file mode 100644 index 0000000..d7fb582 --- /dev/null +++ b/src/constants/profile.tsx @@ -0,0 +1,2 @@ +export const PROFILE_NAME_LENGTH = 1 +export const PROGRESS_TIMEOUT_DELAY = 500