[resumes][feat] Fetch resume details from database (#322)

* [resumes][feat] Add resume details router

* [resumes][feat] Change review page to dynamic routing

* [resumes][feat] Toggle resume star button

* [resumes][refactor] Revert routers to User model
This commit is contained in:
Peirong
2022-10-07 23:33:24 +08:00
committed by GitHub
parent 5507c6a9d2
commit 2e947f5fb0
12 changed files with 371 additions and 187 deletions

View File

@ -22,6 +22,7 @@
"@trpc/react": "^9.27.2", "@trpc/react": "^9.27.2",
"@trpc/server": "^9.27.2", "@trpc/server": "^9.27.2",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"date-fns": "^2.29.3",
"next": "12.3.1", "next": "12.3.1",
"next-auth": "~4.10.3", "next-auth": "~4.10.3",
"react": "18.2.0", "react": "18.2.0",

View File

@ -0,0 +1,73 @@
/*
Warnings:
- You are about to drop the column `resumesProfileId` on the `ResumesComment` table. All the data in the column will be lost.
- You are about to drop the column `resumesProfileId` on the `ResumesCommentVote` table. All the data in the column will be lost.
- You are about to drop the column `resumesProfileId` on the `ResumesResume` table. All the data in the column will be lost.
- You are about to drop the column `resumesProfileId` on the `ResumesStar` table. All the data in the column will be lost.
- You are about to drop the `ResumesProfile` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[userId,commentId]` on the table `ResumesCommentVote` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[userId,resumeId]` on the table `ResumesStar` will be added. If there are existing duplicate values, this will fail.
- Added the required column `userId` to the `ResumesComment` table without a default value. This is not possible if the table is not empty.
- Added the required column `userId` to the `ResumesCommentVote` table without a default value. This is not possible if the table is not empty.
- Added the required column `userId` to the `ResumesResume` table without a default value. This is not possible if the table is not empty.
- Added the required column `userId` to the `ResumesStar` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "ResumesComment" DROP CONSTRAINT "ResumesComment_resumesProfileId_fkey";
-- DropForeignKey
ALTER TABLE "ResumesCommentVote" DROP CONSTRAINT "ResumesCommentVote_resumesProfileId_fkey";
-- DropForeignKey
ALTER TABLE "ResumesProfile" DROP CONSTRAINT "ResumesProfile_userId_fkey";
-- DropForeignKey
ALTER TABLE "ResumesResume" DROP CONSTRAINT "ResumesResume_resumesProfileId_fkey";
-- DropForeignKey
ALTER TABLE "ResumesStar" DROP CONSTRAINT "ResumesStar_resumesProfileId_fkey";
-- DropIndex
DROP INDEX "ResumesCommentVote_commentId_resumesProfileId_key";
-- DropIndex
DROP INDEX "ResumesStar_resumeId_resumesProfileId_key";
-- AlterTable
ALTER TABLE "ResumesComment" DROP COLUMN "resumesProfileId",
ADD COLUMN "userId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ResumesCommentVote" DROP COLUMN "resumesProfileId",
ADD COLUMN "userId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ResumesResume" DROP COLUMN "resumesProfileId",
ADD COLUMN "userId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ResumesStar" DROP COLUMN "resumesProfileId",
ADD COLUMN "userId" TEXT NOT NULL;
-- DropTable
DROP TABLE "ResumesProfile";
-- CreateIndex
CREATE UNIQUE INDEX "ResumesCommentVote_userId_commentId_key" ON "ResumesCommentVote"("userId", "commentId");
-- CreateIndex
CREATE UNIQUE INDEX "ResumesStar_userId_resumeId_key" ON "ResumesStar"("userId", "resumeId");
-- AddForeignKey
ALTER TABLE "ResumesResume" ADD CONSTRAINT "ResumesResume_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ResumesStar" ADD CONSTRAINT "ResumesStar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ResumesCommentVote" ADD CONSTRAINT "ResumesCommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -37,15 +37,18 @@ model Session {
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
name String? name String?
email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
image String? image String?
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]
todos Todo[] todos Todo[]
resumesProfile ResumesProfile? resumesResumes ResumesResume[]
resumesStars ResumesStar[]
resumesComments ResumesComment[]
resumesCommentVotes ResumesCommentVote[]
} }
model VerificationToken { model VerificationToken {
@ -85,56 +88,45 @@ model Company {
// Add Resumes project models here, prefix all models with "Resumes", // Add Resumes project models here, prefix all models with "Resumes",
// use camelCase for field names, and try to name them consistently // use camelCase for field names, and try to name them consistently
// across all models in this file. // across all models in this file.
model ResumesProfile {
id String @id @default(cuid())
userId String @unique
resumesResumes ResumesResume[]
resumesStars ResumesStar[]
resumesComments ResumesComment[]
resumesCommentVotes ResumesCommentVote[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model ResumesResume { model ResumesResume {
id String @id @default(cuid()) id String @id @default(cuid())
resumesProfileId String userId String
title String @db.Text title String @db.Text
// TODO: Update role, experience, location to use Enums // TODO: Update role, experience, location to use Enums
role String @db.Text role String @db.Text
experience String @db.Text experience String @db.Text
location String @db.Text location String @db.Text
url String url String
additionalInfo String? @db.Text additionalInfo String? @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
stars ResumesStar[] stars ResumesStar[]
comments ResumesComment[] comments ResumesComment[]
} }
model ResumesStar { model ResumesStar {
id String @id @default(cuid()) id String @id @default(cuid())
resumesProfileId String userId String
resumeId String resumeId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade) resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([resumeId, resumesProfileId]) @@unique([userId, resumeId])
} }
model ResumesComment { model ResumesComment {
id String @id @default(cuid()) id String @id @default(cuid())
resumesProfileId String userId String
resumeId String resumeId String
description String @db.Text description String @db.Text
section ResumesSection section ResumesSection
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade) resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) votes ResumesCommentVote[]
votes ResumesCommentVote[] user User @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
enum ResumesSection { enum ResumesSection {
@ -146,16 +138,16 @@ enum ResumesSection {
} }
model ResumesCommentVote { model ResumesCommentVote {
id String @id @default(cuid()) id String @id @default(cuid())
resumesProfileId String userId String
commentId String commentId String
value Int value Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
resumesProfile ResumesProfile @relation(fields: [resumesProfileId], references: [id], onDelete: Cascade) comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade)
comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([commentId, resumesProfileId]) @@unique([userId, commentId])
} }
// End of Resumes project models. // End of Resumes project models.

View File

@ -6,7 +6,11 @@ import { Button, Spinner } from '@tih/ui';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`; pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
export default function ResumePdf() { type Props = Readonly<{
url: string;
}>;
export default function ResumePdf({ url }: Props) {
const [numPages, setNumPages] = useState(0); const [numPages, setNumPages] = useState(0);
const [pageNumber] = useState(1); const [pageNumber] = useState(1);
@ -18,7 +22,7 @@ export default function ResumePdf() {
<div> <div>
<Document <Document
className="h-[calc(100vh-17rem)] overflow-scroll" className="h-[calc(100vh-17rem)] overflow-scroll"
file="/test_resume.pdf" file={url}
loading={<Spinner display="block" label="" size="lg" />} loading={<Spinner display="block" label="" size="lg" />}
onLoadSuccess={onPdfLoadSuccess}> onLoadSuccess={onPdfLoadSuccess}>
<Page pageNumber={pageNumber} /> <Page pageNumber={pageNumber} />

View File

@ -0,0 +1,148 @@
import clsx from 'clsx';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import Error from 'next/error';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useEffect } from 'react';
import {
AcademicCapIcon,
BriefcaseIcon,
CalendarIcon,
InformationCircleIcon,
MapPinIcon,
StarIcon,
} from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui';
import CommentsSection from '~/components/resumes/comments/CommentsSection';
import ResumePdf from '~/components/resumes/ResumePdf';
import { trpc } from '~/utils/trpc';
export default function ResumeReviewPage() {
const ErrorPage = (
<Error statusCode={404} title="Requested resume does not exist." />
);
const { data: session } = useSession();
const router = useRouter();
const { resumeId } = router.query;
const utils = trpc.useContext();
// Safe to assert resumeId type as string because query is only sent if so
const detailsQuery = trpc.useQuery(
['resumes.details.find', { resumeId: resumeId as string }],
{
enabled: typeof resumeId === 'string' && session?.user?.id !== undefined,
},
);
const starMutation = trpc.useMutation('resumes.details.update_star', {
onSuccess() {
utils.invalidateQueries();
},
});
useEffect(() => {
if (detailsQuery.data?.stars.length) {
document.getElementById('star-button')?.focus();
} else {
document.getElementById('star-button')?.blur();
}
}, [detailsQuery.data?.stars]);
const onStarButtonClick = () => {
// Star button only rendered if resume exists
// Star button only clickable if user exists
starMutation.mutate({
resumeId: resumeId as string,
});
};
return (
<>
{detailsQuery.isError && ErrorPage}
{detailsQuery.isLoading && <Spinner display="block" label="" size="lg" />}
{detailsQuery.isFetched && detailsQuery.data && (
<main className="flex-1 p-4">
<div className="flex flex-row md:space-x-8">
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{detailsQuery.data.title}
</h1>
<button
className="isolate inline-flex max-h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
disabled={session?.user === null}
id="star-button"
type="button"
onClick={onStarButtonClick}>
<span className="relative inline-flex">
<StarIcon
aria-hidden="true"
className={clsx(
detailsQuery.data?.stars.length
? 'text-orange-400'
: 'text-gray-400',
'-ml-1 mr-2 h-5 w-5',
)}
id="star-icon"
/>
Star
</span>
<span className="relative -ml-px inline-flex">
{detailsQuery.data._count.stars}
</span>
</button>
</div>
<div className="flex flex-col pt-1 sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-8">
<div className="mt-2 flex items-center text-sm text-gray-500">
<BriefcaseIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
{detailsQuery.data.role}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
{detailsQuery.data.location}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
{detailsQuery.data.experience}
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<CalendarIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
{`Uploaded ${formatDistanceToNow(
new Date(detailsQuery.data.createdAt),
{ addSuffix: true },
)} by ${detailsQuery.data.user.name}`}
</div>
</div>
{detailsQuery.data.additionalInfo && (
<div className="flex items-center pt-2 text-sm text-gray-500">
<InformationCircleIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
{detailsQuery.data.additionalInfo}
</div>
)}
<div className="flex h-full w-full flex-row py-4">
<div className="w-1/2">
<ResumePdf url={detailsQuery.data.url} />
</div>
<div className="mx-8 w-1/2">
{/* TODO: Update resumeId */}
<CommentsSection resumeId="cl8y6gtez0009yedbne9qp5zi" />
</div>
</div>
</main>
)}
</>
);
}

View File

@ -1,82 +0,0 @@
import {
AcademicCapIcon,
BriefcaseIcon,
CalendarIcon,
InformationCircleIcon,
MapPinIcon,
StarIcon,
} from '@heroicons/react/20/solid';
import CommentsSection from '~/components/resumes/comments/CommentsSection';
import ResumePdf from '~/components/resumes/ResumePdf';
export default function ResumeReviewPage() {
return (
<main className="flex-1 p-4">
<div className="flex flex-row md:space-x-8">
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
Please help moi, applying for medtech startups in Singapore
</h1>
<button
className="isolate inline-flex max-h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
type="button">
<span className="relative inline-flex">
<StarIcon
aria-hidden="true"
className="-ml-1 mr-2 h-5 w-5 text-gray-400"
/>
Star
</span>
<span className="relative -ml-px inline-flex">12k</span>
</button>
</div>
<div className="flex flex-col pt-1 sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-8">
<div className="mt-2 flex items-center text-sm text-gray-500">
<BriefcaseIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
Software Engineer (Backend)
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<MapPinIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
Singapore
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<AcademicCapIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
Fresh Grad
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<CalendarIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
Uploaded 2 days ago by Git Ji Ra
</div>
</div>
<div className="flex items-center pt-2 text-sm text-gray-500">
<InformationCircleIcon
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
Looking to break into SWE roles after doing engineering for the past 2
years
</div>
<div className="flex h-full w-full flex-row py-4">
<div className="w-1/2">
<ResumePdf />
</div>
<div className="mx-8 w-1/2">
{/* TODO: Update resumeId */}
<CommentsSection resumeId="cl8y6gtez0009yedbne9qp5zi" />
</div>
</div>
</main>
);
}

View File

@ -2,6 +2,7 @@ import superjson from 'superjson';
import { createRouter } from './context'; import { createRouter } from './context';
import { protectedExampleRouter } from './protected-example-router'; import { protectedExampleRouter } from './protected-example-router';
import { resumesDetailsRouter } from './resumes-details-router';
import { resumesResumeUserRouter } from './resumes-resume-user-router'; import { resumesResumeUserRouter } from './resumes-resume-user-router';
import { resumeReviewsRouter } from './resumes-reviews-router'; import { resumeReviewsRouter } from './resumes-reviews-router';
import { resumesReviewsUserRouter } from './resumes-reviews-user-router'; import { resumesReviewsUserRouter } from './resumes-reviews-user-router';
@ -16,6 +17,7 @@ export const appRouter = createRouter()
.merge('auth.', protectedExampleRouter) .merge('auth.', protectedExampleRouter)
.merge('todos.', todosRouter) .merge('todos.', todosRouter)
.merge('todos.user.', todosUserRouter) .merge('todos.user.', todosUserRouter)
.merge('resumes.details.', resumesDetailsRouter)
.merge('resumes.resume.user.', resumesResumeUserRouter) .merge('resumes.resume.user.', resumesResumeUserRouter)
.merge('resumes.reviews.', resumeReviewsRouter) .merge('resumes.reviews.', resumeReviewsRouter)
.merge('resumes.reviews.user.', resumesReviewsUserRouter); .merge('resumes.reviews.user.', resumesReviewsUserRouter);

View File

@ -0,0 +1,79 @@
import { z } from 'zod';
import { createRouter } from './context';
export const resumesDetailsRouter = createRouter()
.query('find', {
input: z.object({
resumeId: z.string(),
}),
async resolve({ ctx, input }) {
const { resumeId } = input;
const userId = ctx.session?.user?.id;
// Use the resumeId to query all related information of a single resume
// from Resumesresume:
return await ctx.prisma.resumesResume.findUnique({
include: {
_count: {
select: {
stars: true,
},
},
stars: {
where: {
userId,
},
},
user: {
select: {
name: true,
},
},
},
where: {
id: resumeId,
},
});
},
})
.mutation('update_star', {
input: z.object({
resumeId: z.string(),
}),
async resolve({ ctx, input }) {
const { resumeId } = input;
// Update_star will only be called if user is logged in
const userId = ctx.session!.user!.id;
// Use the resumeId and resumeProfileId to check if star exists
const resumesStar = await ctx.prisma.resumesStar.findUnique({
select: {
id: true,
},
where: {
userId_resumeId: {
resumeId,
userId,
},
},
});
if (resumesStar === null) {
return await ctx.prisma.resumesStar.create({
data: {
resumeId,
userId,
},
});
}
return await ctx.prisma.resumesStar.delete({
where: {
userId_resumeId: {
resumeId,
userId,
},
},
});
},
});

View File

@ -15,23 +15,12 @@ export const resumesResumeUserRouter = createProtectedRouter().mutation(
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user.id; const userId = ctx.session?.user.id;
const resumeProfile = await ctx.prisma.resumesProfile.upsert({
create: {
userId,
},
update: {},
where: {
userId,
},
});
// TODO: Store file in file storage and retrieve URL // TODO: Store file in file storage and retrieve URL
return await ctx.prisma.resumesResume.create({ return await ctx.prisma.resumesResume.create({
data: { data: {
...input, ...input,
resumesProfileId: resumeProfile.id,
url: '', url: '',
userId,
}, },
}); });
}, },

View File

@ -7,18 +7,9 @@ export const resumeReviewsRouter = createRouter().query('list', {
resumeId: z.string(), resumeId: z.string(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { resumeId } = input; const { resumeId } = input;
const { resumesProfileId } =
await ctx.prisma.resumesResume.findUniqueOrThrow({
select: {
resumesProfileId: true,
},
where: {
id: resumeId,
},
});
// For this resume, we retrieve every comment's information, along with: // For this resume, we retrieve every comment's information, along with:
// The user's name and image to render // The user's name and image to render
// Number of votes, and whether the user (if-any) has voted // Number of votes, and whether the user (if-any) has voted
@ -29,20 +20,16 @@ export const resumeReviewsRouter = createRouter().query('list', {
votes: true, votes: true,
}, },
}, },
resumesProfile: { user: {
include: { select: {
user: { image: true,
select: { name: true,
image: true,
name: true,
},
},
}, },
}, },
votes: { votes: {
take: 1, take: 1,
where: { where: {
resumesProfileId, userId,
}, },
}, },
}, },

View File

@ -6,8 +6,8 @@ import { createProtectedRouter } from './context';
type IResumeCommentInput = Readonly<{ type IResumeCommentInput = Readonly<{
description: string; description: string;
resumeId: string; resumeId: string;
resumesProfileId: string;
section: ResumesSection; section: ResumesSection;
userId: string;
}>; }>;
export const resumesReviewsUserRouter = createProtectedRouter().mutation( export const resumesReviewsUserRouter = createProtectedRouter().mutation(
@ -22,19 +22,10 @@ export const resumesReviewsUserRouter = createProtectedRouter().mutation(
skills: z.string(), skills: z.string(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { resumeId, education, experience, general, projects, skills } = const { resumeId, education, experience, general, projects, skills } =
input; input;
const { resumesProfileId } =
await ctx.prisma.resumesResume.findUniqueOrThrow({
select: {
resumesProfileId: true,
},
where: {
id: resumeId,
},
});
// For each section, convert them into ResumesComment model if provided // For each section, convert them into ResumesComment model if provided
const comments: Array<IResumeCommentInput> = [ const comments: Array<IResumeCommentInput> = [
{ description: education, section: ResumesSection.EDUCATION }, { description: education, section: ResumesSection.EDUCATION },
@ -50,8 +41,8 @@ export const resumesReviewsUserRouter = createProtectedRouter().mutation(
return { return {
description, description,
resumeId, resumeId,
resumesProfileId,
section, section,
userId,
}; };
}); });

View File

@ -6049,7 +6049,7 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
date-fns@^2.29.1: date-fns@^2.29.1, date-fns@^2.29.3:
version "2.29.3" version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==