Skip to content

Commit

Permalink
Merge pull request #67 from italosilva01/feat-38
Browse files Browse the repository at this point in the history
Feat 38: Desenvolver componente de Upload de imagem de perfil
  • Loading branch information
mffonseca authored Nov 1, 2024
2 parents ce20d62 + 672f7e0 commit 47fdf47
Show file tree
Hide file tree
Showing 14 changed files with 10,659 additions and 5 deletions.
8 changes: 8 additions & 0 deletions frontend/.hintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": [
"development"
],
"hints": {
"disown-opener": "off"
}
}
15 changes: 15 additions & 0 deletions frontend/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.53.0",
"react-icons": "^5.2.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -45,7 +46,9 @@
"@storybook/nextjs": "^8.2.5",
"@storybook/react": "^8.2.5",
"@storybook/test": "^8.2.5",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.13",
"@types/react": "^18",
Expand Down
Binary file added frontend/public/assets/person-unknow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions frontend/src/app/(mentoring)/mentoring.view.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Link from 'next/link'

import { useMentoringModel } from './mentoring.model'

import { TextInput } from '@/components/form/text-input'
import { AlertBox } from '@/components/ui/alert-box'
import { ErrorMessage } from '@/components/ui/error-message'

import { useMentoringModel } from './mentoring.model'
import { AlertBox } from '@/components/ui/alert-box'

type MentoringViewProps = ReturnType<typeof useMentoringModel>

Expand All @@ -18,7 +19,6 @@ export function MentoringView(props: MentoringViewProps) {
isSubmitting,
submitButtonLabel,
} = props

return (
<main className="flex items-center justify-center bg-gray-100">
<div className="w-full max-w-md bg-white shadow-lg rounded-lg p-6">
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/components/Buttons/ButtonWhiteBlack/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ReactNode } from 'react'

interface ButtonWhiteBlackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
classList?: string
}

export const ButtonWhiteBlack = ({ children, classList, ...rest }: ButtonWhiteBlackProps) => {
return (
<button
className={`text-2xl text-center z-20 flex flex-col justify-center items-center border border-black p-4 w-3 h-3 rounded-full bg-white hover:bg-gray-950 hover:text-white transition duration-300 ease-in-out ${classList}`}
{...rest}
>
{children}
</button>
)
}
15 changes: 15 additions & 0 deletions frontend/src/components/upload/MockFormProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { useForm, FormProvider, UseFormReturn } from 'react-hook-form';

interface MockFormProviderProps {
children: React.ReactNode;
defaultValues?: Record<string, any>;
methods?: Partial<UseFormReturn>;
}

export const MockFormProvider: React.FC<MockFormProviderProps> = ({ children, defaultValues = {}, methods = {} }) => {
const formMethods = useForm({ defaultValues });
const combinedMethods = { ...formMethods, ...methods };

return <FormProvider {...combinedMethods}>{children}</FormProvider>;
};
88 changes: 88 additions & 0 deletions frontend/src/components/upload/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client'
import Image from 'next/image'
import { useRef, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { Controller, useFormContext } from 'react-hook-form'

import { ButtonWhiteBlack } from '../Buttons/ButtonWhiteBlack'

const ZERO = 0

interface UploadProps {
onChange: (...event: any[]) => void,
name:string
}

export const Upload =({name='upload', ...rest}: {name?: string}) =>{
const { control, } = useFormContext()

return(
<Controller
render={({ field: { onChange } })=>(
<UploadInput onChange={(e:any)=>
onChange(e.target.files[ZERO])} name={name} {...rest}/>
)}
name={name}
control={control}
defaultValue=""
/>
)
}

const UploadInput = ({ onChange,name, ...rest
}: UploadProps) => {
const [imgPreviewUrl, setImgPreviewUrl] = useState<string | null>(null)
const inputImageRef = useRef<HTMLInputElement | null>(null);
const { setValue, } = useFormContext()

const handleRemoveImage = () => {
setImgPreviewUrl(null)
if (inputImageRef.current) {
inputImageRef.current.value = '';
setValue(name,'')
}
}
const onDrop = (acceptedFiles: File[]) => {
const file = acceptedFiles[ZERO];
if (file) {
setImgPreviewUrl( URL.createObjectURL(file));
setValue(name,file)
}
};
const { getRootProps, getInputProps } = useDropzone({ onDrop, ...rest
});
return (
<div className="flex flex-col w-fit h-fit relative">
<div className="z-10 w-11/12 flex justify-end absolute">
{imgPreviewUrl && <ButtonWhiteBlack data-testid="custom-element" onClick={handleRemoveImage}>&times;</ButtonWhiteBlack>}
</div>
<div
{...getRootProps()}
className="relative flex flex-col items-center justify-center border border-dashed h-56 w-56 rounded-full overflow-hidden border-cyan-600 z-0"
onClick={()=>{inputImageRef.current && inputImageRef.current.click()}}
>
<label htmlFor="file-uploader" className="hidden">Upload file:</label>
<input
{...getInputProps({ onChange })}
id="file-uploader"
type="file"
className="hidden"
title="Upload your file"
placeholder="Choose a file"
ref={inputImageRef}
/>
{imgPreviewUrl ? (
<Image src={imgPreviewUrl} alt="photo preview" layout="fill" objectFit="cover" sizes="auto auto" />
) : (
<Image
src={'/assets/person-unknow.png'}
alt="person unknow"
layout="fill"
objectFit="cover"
sizes="auto auto"
/>
)}
</div>
</div>
)
}
58 changes: 58 additions & 0 deletions frontend/src/components/upload/upload.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"
import userEvent from '@testing-library/user-event'
import { expect } from "vitest";

import { FormStateChecker, renderWithFormProvider } from "@/test-utils";

import { Upload } from "."


const ONE = 1;
describe('Upload Component',()=>{
beforeEach(()=>{
global.URL.createObjectURL = ()=>'http://localhost:3000/e17fa82b-b0c5-4b24-82fc-132a8659d6adimg/preview/test'
renderWithFormProvider(
<>
<Upload name="upload" />
<FormStateChecker name="upload" />
</>
)
})
it("should render the component without photo",()=>{
const formState = screen.getByTestId("form-state");
expect(screen.getByAltText("person unknow")).toBeInTheDocument();
expect(formState.textContent).toBe('');
})
it("should handle file upload", async()=>{
const inputUpload = screen.getByLabelText<HTMLInputElement>(/upload file/i);
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
expect(inputUpload).toBeInTheDocument();
userEvent.upload(inputUpload,file);
await waitFor(() => {
expect(inputUpload.files?.[0]).toStrictEqual(file);
expect(inputUpload.files?.item(0)).toStrictEqual(file);
expect(inputUpload.files).toHaveLength(ONE)
});
})
it("should handle image removal", async()=>{
const inputUpload = screen.getByLabelText<HTMLInputElement>(/upload file/i);
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
userEvent.upload(inputUpload,file);
await waitFor(()=>{
expect(inputUpload.files?.[0]).toStrictEqual(file);
expect(inputUpload.files?.item(0)).toStrictEqual(file);
expect(inputUpload.files).toHaveLength(ONE)
})
const photoPreview = screen.getByAltText(/photo preview/i);
const buttonRemoveImage = screen.getByTestId('custom-element');
expect(photoPreview).toBeInTheDocument()
expect(buttonRemoveImage).toBeInTheDocument()
userEvent.click(buttonRemoveImage);
await waitFor(()=>{
expect(inputUpload.files?.[0]).toStrictEqual(undefined);
expect(inputUpload.files?.item(0)).toStrictEqual(null);
expect(inputUpload.files).toHaveLength(0)
})
expect(screen.getByAltText("person unknow")).toBeInTheDocument();
})
})
30 changes: 30 additions & 0 deletions frontend/src/components/upload/upload.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import type { Meta, StoryObj } from '@storybook/react'

import { Upload } from "./index";
import { MockFormProvider as FormProvider } from './MockFormProvider';

type Story = StoryObj<typeof Upload>;

const meta: Meta<typeof Upload> = {
component: Upload,
title: 'Components/Upload',
argTypes:{
name:{
control:{type:'text'},
description: 'Field identifier in React hook form'
}
},
parameters: {
layout: 'centered'
}
};
export default meta

export const Default:Story = {
render: () => (
<FormProvider>
<Upload/>
</FormProvider>
)
}
21 changes: 21 additions & 0 deletions frontend/src/test-utils/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render } from "@testing-library/react";
import React, { FC, ReactElement } from "react";
import { FormProvider, useForm, useFormContext } from "react-hook-form"

export const renderWithFormProvider = (ui:ReactElement) =>{
const Wrapper:FC<{ children: React.ReactNode }> = (
{ children }
)=>{
const methods = useForm();
return (<FormProvider {...methods}>
{children}
</FormProvider>)
};

return render(ui,{wrapper:Wrapper})
}

export const FormStateChecker = ({ name }: {name: string }) =>{
const { getValues } = useFormContext();
return <div data-testid="form-state">{getValues(name)}</div>;
};
3 changes: 2 additions & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
Expand Down
Loading

0 comments on commit 47fdf47

Please sign in to comment.