User profile update

parent 5eda1d85
......@@ -5,6 +5,7 @@ import Landing from "./views/Landing.view";
import Home from "./views/Home.view";
import Settings from "./views/Settings.view";
import Applications from "./views/Applications.view";
import DialogBox from "./components/Modals/DialogBox";
import AppState from "./components/AppState";
......@@ -29,6 +30,7 @@ function App() {
</Route>
</Routes>
<AppState />
<DialogBox />
</Router>
);
}
......
......@@ -30,3 +30,17 @@ p {
}
}
}
.hidden-input {
display: none;
}
.profile-card {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
h5 {
margin-top: 10px;
}
}
......@@ -30,3 +30,8 @@ export const updateApplication = (
payload,
success,
});
export const setDialogBox = (payload: string | null) => ({
type: ACTIONS.SET_DIALOG,
payload,
});
......@@ -16,4 +16,5 @@ export enum ACTIONS {
CREATE_JOB = "CREATE_JOB",
UPDATE_JOB = "UPDATE_JOB",
UPDATE_APPLICATION = "UPDATE_APPLICATION",
SET_DIALOG = "SET_DIALOG",
}
import { ACTIONS } from ".";
import { CandidateType } from "../types";
export const updateCandidate = (payload: Partial<CandidateType>) => ({
export const updateCandidate = (
payload: Partial<CandidateType>,
success?: (candidate: CandidateType) => void
) => ({
type: ACTIONS.UPDATE_CANDIDATE,
payload,
success,
});
export const applyForJob = (payload: string) => ({
......
import { Area } from "react-easy-crop";
export const createImage = (url: string) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
export function getRadianAngle(degreeValue: number) {
return (degreeValue * Math.PI) / 180;
}
/**
* Returns the new bounding area of a rotated rectangle.
*/
export function rotateSize(width: number, height: number, rotation: number) {
const rotRad = getRadianAngle(rotation);
return {
width:
Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
height:
Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
};
}
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
*/
export default async function getCroppedImg(
imageSrc: string,
pixelCrop: Area,
rotation = 0,
flip = { horizontal: false, vertical: false }
) {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
return null;
}
const rotRad = getRadianAngle(rotation);
// calculate bounding box of the rotated image
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
image.width,
image.height,
rotation
);
// set canvas size to match the bounding box
canvas.width = bBoxWidth;
canvas.height = bBoxHeight;
// translate canvas context to a central location to allow rotating and flipping around the center
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
ctx.rotate(rotRad);
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
ctx.translate(-image.width / 2, -image.height / 2);
// draw rotated image
ctx.drawImage(image, 0, 0);
// croppedAreaPixels values are bounding box relative
// extract the cropped image using these values
const data = ctx.getImageData(
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height
);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image at the top left corner
ctx.putImageData(data, 0, 0);
// As Base64 string
// return canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve, reject) => {
canvas.toBlob((file: any) => {
file.name = "cropped.jpeg";
resolve({ file: file, url: URL.createObjectURL(file) });
}, "image/jpeg");
});
}
......@@ -64,7 +64,7 @@ export const getStatusColor = (status?: string) => {
return color;
};
export const getAddress = (address: AddressType) => {
export const getAddress = (address?: AddressType) => {
if (!address) return "";
return `${address.addressLine}, ${address.city}, ${address.country}`;
};
......
......@@ -3,6 +3,7 @@ import { CommonReducer } from "../types";
const INITIAL_STATE: CommonReducer = {
jobs: [],
dialogAlert: null,
};
const commonReducer = (
......@@ -13,6 +14,9 @@ const commonReducer = (
case ACTIONS.SET_JOBS:
return { ...state, jobs: payload };
case ACTIONS.SET_DIALOG:
return { ...state, dialogAlert: payload };
case ACTIONS.SIGN_OUT_USER:
return INITIAL_STATE;
......
......@@ -7,9 +7,11 @@ import { APP_STATE, CandidateType, JobType, Reducers } from "../types";
function* updateCandidate({
payload,
success,
}: {
type: typeof ACTIONS.UPDATE_CANDIDATE;
payload: Partial<CandidateType>;
success?: (candidate?: CandidateType | null) => void;
}) {
try {
yield put({
......@@ -18,20 +20,23 @@ function* updateCandidate({
});
yield call(UserAPI.updateCandidate, payload);
const candidate: CandidateType = yield select(getProfile);
const newCandidate = { ...candidate, ...payload };
yield put({
type: ACTIONS.SET_AUTH,
payload: { candidate: { ...candidate, ...payload } },
payload: { candidate: newCandidate },
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
success?.(newCandidate);
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED },
});
success?.(null);
}
}
......@@ -89,6 +94,7 @@ function* applyForJob({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
window.location.reload();
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
......
......@@ -9,6 +9,12 @@ export enum APP_STATE {
SUCCESS = "success",
}
export enum OnboardSteps {
RESUME = "RESUME",
SELF_INTRO = "SELF_INTRO",
COMPLETE = "COMPLETE",
}
export type KeyDetails = {
key: any;
code: any;
......@@ -250,6 +256,7 @@ export type AuthReducer = {
export type CommonReducer = {
jobs: JobType[] | JobPayloadType[];
dialogAlert: string | null;
};
export type Reducers = {
......
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import React, { useState, useEffect, ChangeEvent } from "react";
import React, { useState, useEffect, ChangeEvent, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ACTIONS } from "../common/actions";
import { updateCandidate } from "../common/actions/user";
import { fileStorage } from "../common/config";
import { getProfile } from "../common/lib/util";
import { APP_STATE, CandidateType } from "../common/types";
import { APP_STATE, CandidateType, OnboardSteps } from "../common/types";
import Avatar from "./Avatar";
import Progress from "./Progress";
import Onboarder from "./Onboarder";
import ProfilePicCrop from "./Modals/ProfilePicCrop";
const CandidateProfile = () => {
const dispath = useDispatch();
......@@ -16,10 +18,18 @@ const CandidateProfile = () => {
const [resume, setResume] = useState<File | null>(null);
const [selfIntro, setSelfIntro] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(0);
const [onboardStep, setOnboardStep] = useState<OnboardSteps>(
OnboardSteps.COMPLETE
);
const [profilePicPrev, setprofilePicPrev] = useState<string | null>(null);
const _resumeInput = useRef<HTMLInputElement>(null);
const _introInput = useRef<HTMLInputElement>(null);
const _profilePicInput = useRef<HTMLInputElement>(null);
useEffect(() => {
if (candidate) {
setProfile(candidate);
checkOnboardingStatus(candidate);
}
}, [candidate]);
......@@ -50,8 +60,11 @@ const CandidateProfile = () => {
});
};
const onUpdateCandidate = (payload: Partial<CandidateType>) => {
dispath(updateCandidate(payload));
const onUpdateCandidate = (
payload: Partial<CandidateType>,
callBack?: (candidate: CandidateType) => void
) => {
dispath(updateCandidate(payload, callBack));
};
const uploadSingleFile = (path: string, file: File) => {
......@@ -131,6 +144,60 @@ const CandidateProfile = () => {
}
};
const onSelectProfilePic = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setprofilePicPrev(URL.createObjectURL(e.target.files[0]));
}
};
const onPressOnboard = async () => {
const update: any = {};
if (onboardStep === OnboardSteps.RESUME && resume) {
const url = await uploadSingleFile(`/resumes/${profile._id}.pdf`, resume);
update.resume = url;
}
if (onboardStep === OnboardSteps.SELF_INTRO && selfIntro) {
const url = await uploadSingleFile(
`/selfIntros/${profile._id}.mp4`,
selfIntro
);
update.selfIntro = url;
}
onUpdateCandidate(update, checkOnboardingStatus);
};
const checkOnboardingStatus = (_candidate: CandidateType) => {
if (_candidate) {
if (!_candidate?.resume) {
setOnboardStep(OnboardSteps.RESUME);
} else if (!_candidate?.selfIntro) {
setOnboardStep(OnboardSteps.SELF_INTRO);
} else {
setOnboardStep(OnboardSteps.COMPLETE);
}
}
};
const onPressUploadProfilePic = () => {
_profilePicInput?.current?.click();
};
const onPressUploadResume = () => {
_resumeInput?.current?.click();
};
const onPressUploadSelfIntro = () => {
_introInput?.current?.click();
};
const onRequestCloseProfilePic = () => {
setprofilePicPrev(null);
};
const isOnboarded = onboardStep === OnboardSteps.COMPLETE;
return (
<div className="card p-4 mb-3">
<h5>Edit/Update profile</h5>
......@@ -218,28 +285,47 @@ const CandidateProfile = () => {
target="_blank"
rel="noreferrer"
>
Preview
Preview resume
</a>
)}
<input
type="file"
className="form-control"
onChange={onSelectResume}
accept="application/pdf"
/>
<div className="d-md-block">
<button
type="button"
className="btn btn-warning btn-sm"
style={{ width: "100%" }}
onClick={onPressUploadResume}
>
Select Resume
</button>
</div>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="row">
<label className="col-sm-3 col-form-label">Self Intro</label>
<div className="col-sm-9">
<input
type="file"
className="form-control"
accept="video/mp4"
onChange={onSelectSelfIntro}
/>
<label className="col-sm-4 col-form-label">Self Intro</label>
<div className="col-sm-8">
{profile?.selfIntro && (
<a
className="btn btn-secondary mb-2"
href={profile.selfIntro}
target="_blank"
rel="noreferrer"
>
Preview video
</a>
)}
<div className="d-md-block">
<button
type="button"
className="btn btn-warning btn-sm"
style={{ width: "100%" }}
onClick={onPressUploadSelfIntro}
>
Select Video
</button>
</div>
</div>
</div>
</div>
......@@ -260,6 +346,7 @@ const CandidateProfile = () => {
type="button"
className="btn btn-link"
style={{ padding: "0" }}
onClick={onPressUploadProfilePic}
>
Upload picture
</button>
......@@ -279,6 +366,43 @@ const CandidateProfile = () => {
</button>
</div>
<Progress progress={progress} />
<input
type="file"
className="form-control hidden-input"
onChange={onSelectResume}
accept="application/pdf"
ref={_resumeInput}
/>
<input
type="file"
className="form-control hidden-input"
accept="video/mp4"
onChange={onSelectSelfIntro}
ref={_introInput}
/>
<input
type="file"
className="form-control hidden-input"
accept="image/*"
onChange={onSelectProfilePic}
ref={_profilePicInput}
/>
<Onboarder
isOpen={!isOnboarded}
onboardStep={onboardStep}
onPressUploadResume={onPressUploadResume}
onPressUploadSelfIntro={onPressUploadSelfIntro}
progress={progress}
onPressOnboard={onPressOnboard}
/>
<ProfilePicCrop
onRequestClose={onRequestCloseProfilePic}
profilePicPrev={profilePicPrev}
userId={profile._id}
onUpdateCandidate={onUpdateCandidate}
/>
</div>
);
};
......
import React from "react";
import Modal from "./Modal";
import { useDispatch, useSelector } from "react-redux";
import { Reducers } from "../../common/types";
import { setDialogBox } from "../../common/actions/common";
const DialogBox = () => {
const dispatch = useDispatch();
const dialogString = useSelector(
(state: Reducers) => state.common.dialogAlert
);
if (!dialogString) return null;
const onPressCancel = () => {
dispatch(setDialogBox(null));
};
const isOpen = dialogString !== null;
return (
<Modal isOpen={isOpen} onRequestClose={onPressCancel}>
<p>{dialogString}</p>
<div style={{ display: "block" }}>
<button
type="button"
className="btn btn-primary btn-sm mt-3"
style={{ float: "right" }}
onClick={onPressCancel}
>
Got it
</button>
</div>
</Modal>
);
};
export default DialogBox;
......@@ -16,7 +16,7 @@ const CustomModal = ({
size = "md",
shouldCloseOnOverlayClick = true,
}: OwnProps) => {
const width = size === "md" ? "400px" : "500px";
const width = size === "md" ? "400px" : "600px";
return (
<Modal
isOpen={isOpen}
......
import React, { useRef, useState } from "react";
import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage";
import Cropper, { Area } from "react-easy-crop";
import getCroppedImg from "../../common/lib/crop";
import { fileStorage } from "../../common/config";
type OwnProps = {
onRequestClose: () => void;
profilePicPrev: string | null;
userId?: string;
onUpdateCandidate: (update: any) => void;
};
const ProfilePicCrop = ({
onRequestClose,
profilePicPrev,
userId,
onUpdateCandidate,
}: OwnProps) => {
const [progress, setprogress] = useState(0);
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const _croppedAreaPixels = useRef<Area>();
if (!profilePicPrev) return null;
const onCropComplete = (croppedArea: Area, croppedAreaPixels: Area) => {
_croppedAreaPixels.current = croppedAreaPixels;
};
const onChangeZoom = (e: React.ChangeEvent<HTMLInputElement>) => {
setZoom(Number(e.target.value));
};
const onPressSelect = async () => {
if (_croppedAreaPixels.current) {
const { file, _ }: any = await getCroppedImg(
profilePicPrev,
_croppedAreaPixels.current
);
const extension = file.name.split(".")[1];
const uploadRef = ref(fileStorage, `/resumes/${userId}.${extension}`);
const uploadTask = uploadBytesResumable(uploadRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setprogress(percent);
},
(err) => {},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
setprogress(0);
onUpdateCandidate({ profilePicture: url });
onRequestClose();
});
}
);
}
};
return (
<div
style={{
position: "absolute",
top: "0",
left: "0",
right: "0",
bottom: "0",
}}
>
<Cropper
image={profilePicPrev}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
style={{
containerStyle: {
backgroundColor: "white",
borderRadius: "15px",
},
}}
/>
<div
style={{
position: "absolute",
bottom: "0",
height: "80px",
background: "white",
width: "100%",
borderBottomLeftRadius: "15px",
borderBottomRightRadius: "15px",
padding: "0 20px",
}}
>
{progress > 0 && (
<div className="progress ">
<div
className="progress-bar progress-bar-striped"
style={{ width: `${progress}%` }}
></div>
</div>
)}
<div className="row mt-4">
<div className="col-6">
<input
type="range"
min={1}
max={10}
onChange={onChangeZoom}
className="form-range"
/>
</div>
<div className="col-auto">
<button
type="button"
className="btn btn-warning btn-sm"
onClick={onPressSelect}
>
Crop & select picture
</button>
</div>
<div className="col-auto">
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={onRequestClose}
>
Cancel
</button>
</div>
</div>
</div>
</div>
);
};
export default ProfilePicCrop;
......@@ -28,13 +28,7 @@ const NavBar = () => {
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<a className="nav-link" href="/">
Link
</a>
</li>
</ul>
<ul className="navbar-nav me-auto mb-2 mb-lg-0"></ul>
<nav className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
......@@ -50,11 +44,6 @@ const NavBar = () => {
/>
</a>
<ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="/">
Action
</a>
</li>
<li>
<a className="dropdown-item" href="/settings">
Settings
......
import React from "react";
import CustomModal from "./Modals/Modal";
import { OnboardSteps } from "../common/types";
import Progress from "./Progress";
type OwnProps = {
isOpen: boolean;
onboardStep: OnboardSteps;
onPressUploadResume: () => void;
onPressUploadSelfIntro: () => void;
progress: number;
onPressOnboard: () => void;
};
const Onboarder = ({
isOpen,
onboardStep,
onPressUploadResume,
onPressUploadSelfIntro,
progress,
onPressOnboard,
}: OwnProps) => {
return (
<CustomModal
isOpen={isOpen}
onRequestClose={() => {}}
shouldCloseOnOverlayClick={false}
size="lg"
>
<h5 className="mb-3">Complete your profile</h5>
{onboardStep === OnboardSteps.RESUME ? (
<div className="row">
<label className="col-sm-6 col-form-label">
<h6>1. Upload your resume</h6>
</label>
<div className="col-sm-6">
<button
type="button"
className="btn btn-primary"
style={{ width: "200px" }}
onClick={onPressUploadResume}
>
Choose resume
</button>
</div>
</div>
) : (
<div className="row">
<label className="col-sm-6 col-form-label">
<h6>2. Upload a self introduction video</h6>
</label>
<div className="col-sm-6">
<button
type="button"
className="btn btn-primary"
style={{ width: "200px" }}
onClick={onPressUploadSelfIntro}
>
Choose video
</button>
</div>
</div>
)}
<Progress progress={progress} />
<hr />
<div className="d-md-block">
<button
type="button"
className="btn btn-warning btn-sm"
style={{ width: "100px", float: "right" }}
onClick={onPressOnboard}
>
Next
</button>
</div>
</CustomModal>
);
};
export default Onboarder;
import React from "react";
import Avatar from "./Avatar";
import { getAddress } from "../common/lib/util";
import { AddressType } from "../common/types";
const Profile = () => {
return <div className="card p-4">Profile</div>;
type OwnProps = {
name: string;
profilePic?: string;
email: string;
address?: AddressType;
};
const Profile = ({ name, profilePic, email, address }: OwnProps) => {
const addressString = getAddress(address);
return (
<div className="card p-4 profile-card">
<Avatar size="lg" name={name} url={profilePic} />
<h5>{name}</h5>
<p>{email}</p>
<p>{addressString}</p>
<a
className="btn btn-primary mt-3"
href="/settings"
style={{ width: "100%" }}
>
Edit profile
</a>
</div>
);
};
export default Profile;
import React from "react";
import React, { CSSProperties } from "react";
type OwnProps = {
progress?: number;
style?: CSSProperties;
};
const Progress = ({ progress }: OwnProps) => {
const Progress = ({ progress, style }: OwnProps) => {
if (!progress || progress === 0) return null;
return (
<div className="progress mt-3">
<div className="progress mt-3" style={style}>
<div
className="progress-bar progress-bar-striped"
style={{ width: `${progress}%` }}
......
......@@ -25,9 +25,9 @@ const ResultTable = ({ data }: { data?: Stat | null }) => {
</tr>
<tr>
<th className="text-right">Score/Distance</th>
<td>{(data.standard.inRangePercent.full as number).toFixed(2)}</td>
<td>{(data.standard.inRangePercent.full as number)?.toFixed(2)}</td>
<td>
{(data.fullStandard.normedDistance.full as number).toFixed(2)}
{(data.fullStandard.normedDistance.full as number)?.toFixed(2)}
</td>
</tr>
<tr>
......
import React, { useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { applyForJob } from "../common/actions/user";
import { getStatusColor, getUserId } from "../common/lib/util";
import { JobPayloadType, JobType, Reducers, USER_TYPE } from "../common/types";
import { getProfile, getStatusColor, getUserId } from "../common/lib/util";
import {
CandidateType,
JobPayloadType,
JobType,
OrganizationType,
Reducers,
USER_TYPE,
} from "../common/types";
import Jobs from "../components/Jobs";
import Layout from "../Layouts/Layout";
import CreateUpdateJob from "../components/Modals/CreateUpdateJob";
import Profile from "../components/Profile";
import { setDialogBox } from "../common/actions/common";
const CandidateHome = () => {
const dispatch = useDispatch();
const candidate = useSelector(getProfile) as CandidateType;
const [selectedJob, setSelectedJob] = useState<number>(0);
const userId = useSelector(getUserId);
const jobs = useSelector(
......@@ -40,13 +49,24 @@ const CandidateHome = () => {
}, [jobs, selectedJob, userId]);
const onApply = () => {
if (!candidate?.resume || !candidate?.selfIntro) {
dispatch(
setDialogBox("Your resume and self introduction video is required!")
);
} else {
dispatch(applyForJob(jobs[selectedJob]._id || ""));
}
};
return (
<div className="row">
<div className="col-3 ">
<Profile />
<Profile
name={candidate.name}
email={candidate.contacts.email}
profilePic={candidate.profilePicture}
address={candidate.contacts.address}
/>
</div>
<div className="col">
<Jobs
......@@ -95,6 +115,9 @@ const CandidateHome = () => {
const OrganizationHome = () => {
const [selectedJob, setSelectedJob] = useState<number>(0);
const organization: OrganizationType = useSelector(
(state: Reducers) => state.auth.organization
) as OrganizationType;
const jobs = useSelector((state: Reducers) => state.common.jobs) as JobType[];
const job = jobs.length > 0 ? jobs[selectedJob] : null;
......@@ -107,7 +130,12 @@ const OrganizationHome = () => {
return (
<div className="row">
<div className="col-3 ">
<Profile />
<Profile
name={organization.name}
profilePic={organization.profilePicture}
email={organization.contacts.email}
address={organization.contacts.address}
/>
<CreateUpdateJob mode="create" />
</div>
<div className="col">
......
......@@ -32,7 +32,7 @@ const Landing = () => {
className="btn btn-link"
onClick={() => setForm("sign-in")}
>
Sing in
Sign in
</button>
</p>
</section>
......@@ -48,7 +48,7 @@ const Landing = () => {
className="btn btn-link"
onClick={() => setForm("sign-up")}
>
Sing up
Sign up
</button>
</p>
</section>
......
......@@ -7107,6 +7107,11 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
normalize-wheel@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
......@@ -8253,6 +8258,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-easy-crop@^4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-4.7.4.tgz#3106bfb3de371d9393aedc0b9ae1fb97c2b9f722"
integrity sha512-oDi1375Jo/zuPUvo3oauxnNbfy8L4wsbmHD1KB2vT55fdgu+q8/K0w/rDWzy9jz4jfQ94Q9+3Yu366sDDFVmiA==
dependencies:
normalize-wheel "^1.0.1"
tslib "2.0.1"
react-error-overlay@^6.0.11:
version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
......@@ -9462,6 +9475,11 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment