diff --git a/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql b/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql new file mode 100644 index 00000000..018ad4cd --- /dev/null +++ b/apps/portal/prisma/migrations/20221020101123_add_resume_comment_parent/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ResumesComment" ADD COLUMN "parentId" TEXT; + +-- AddForeignKey +ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ResumesComment"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql new file mode 100644 index 00000000..a6319a17 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `upvotes` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "upvotes" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "netVotes" INTEGER NOT NULL DEFAULT 0; diff --git a/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql new file mode 100644 index 00000000..ef9e4229 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `netVotes` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP DEFAULT, +ALTER COLUMN "upvotes" SET DEFAULT 0; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "netVotes"; diff --git a/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql new file mode 100644 index 00000000..6ae3366a --- /dev/null +++ b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "QuestionsQuestion_lastSeenAt_id_idx" ON "QuestionsQuestion"("lastSeenAt", "id"); + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_upvotes_id_idx" ON "QuestionsQuestion"("upvotes", "id"); diff --git a/apps/portal/prisma/migrations/20221021231817_/migration.sql b/apps/portal/prisma/migrations/20221021231817_/migration.sql new file mode 100644 index 00000000..3820338d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021231817_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD', +ADD COLUMN "baseValue" INTEGER NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql new file mode 100644 index 00000000..089e963d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 48e068ca..fb263f80 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -140,6 +140,7 @@ model ResumesComment { id String @id @default(cuid()) userId String resumeId String + parentId String? description String @db.Text section ResumesSection createdAt DateTime @default(now()) @@ -147,6 +148,8 @@ model ResumesComment { resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) votes ResumesCommentVote[] user User @relation(fields: [userId], references: [id], onDelete: Cascade) + parent ResumesComment? @relation("parentComment", fields: [parentId], references: [id]) + children ResumesComment[] @relation("parentComment") } enum ResumesSection { @@ -202,9 +205,9 @@ model OffersBackground { totalYoe Int specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) offersProfileId String @unique @@ -248,10 +251,16 @@ model OffersExperience { } model OffersCurrency { - id String @id @default(cuid()) - value Int + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + value Float currency String + baseValue Float + baseCurrency String @default("USD") + // Experience OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") @@ -395,6 +404,8 @@ model QuestionsQuestion { userId String? content String @db.Text questionType QuestionsQuestionType + lastSeenAt DateTime + upvotes Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -403,6 +414,9 @@ model QuestionsQuestion { votes QuestionsQuestionVote[] comments QuestionsQuestionComment[] answers QuestionsAnswer[] + + @@index([lastSeenAt, id]) + @@index([upvotes, id]) } model QuestionsQuestionEncounter { diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index 9f46d4d4..d636add7 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -9,6 +9,7 @@ import { YOE_CATEGORY, } from '~/components/offers/table/types'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import { trpc } from '~/utils/trpc'; @@ -25,7 +26,7 @@ export default function OffersTable({ companyFilter, jobTitleFilter, }: OffersTableProps) { - const [currency, setCurrency] = useState('SGD'); // TODO: Detect location + const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [pagination, setPagination] = useState({ currentPage: 0, @@ -44,12 +45,13 @@ export default function OffersTable({ numOfPages: 0, totalItems: 0, }); - }, [selectedTab]); + }, [selectedTab, currency]); const offersQuery = trpc.useQuery( [ 'offers.list', { companyId: companyFilter, + currency, limit: NUMBER_OF_OFFERS_IN_PAGE, location: 'Singapore, Singapore', // TODO: Geolocation offset: pagination.currentPage, diff --git a/apps/portal/src/components/questions/filter/FilterSection.tsx b/apps/portal/src/components/questions/filter/FilterSection.tsx index 9abdb066..7f22a2a9 100644 --- a/apps/portal/src/components/questions/filter/FilterSection.tsx +++ b/apps/portal/src/components/questions/filter/FilterSection.tsx @@ -100,7 +100,8 @@ export default function FilterSection< {isSingleSelect ? (
option.checked)?.value} onChange={(value) => { onOptionChange(value); diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index b0ef8b4d..5726badd 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -42,7 +42,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
{`${resumeInfo.numComments} comment${ - resumeInfo.numComments > 0 ? 's' : '' + resumeInfo.numComments === 1 ? '' : 's' }`}
@@ -51,7 +51,9 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { ) : ( )} - {resumeInfo.numStars} stars + {`${resumeInfo.numStars} star${ + resumeInfo.numStars === 1 ? '' : 's' + }`}
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx index 833c53c2..07d951bb 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx @@ -1,18 +1,11 @@ import clsx from 'clsx'; -import type { Dispatch, SetStateAction } from 'react'; import { useState } from 'react'; -import type { SubmitHandler } from 'react-hook-form'; -import { useForm } from 'react-hook-form'; -import { - ArrowDownCircleIcon, - ArrowUpCircleIcon, -} from '@heroicons/react/20/solid'; +import { ChevronUpIcon } from '@heroicons/react/20/solid'; import { FaceSmileIcon } from '@heroicons/react/24/outline'; -import { Vote } from '@prisma/client'; -import { Button, TextArea } from '@tih/ui'; - -import { trpc } from '~/utils/trpc'; +import ResumeCommentEditForm from './comment/ResumeCommentEditForm'; +import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm'; +import ResumeCommentVoteButtons from './comment/ResumeCommentVoteButtons'; import ResumeUserBadges from '../badges/ResumeUserBadges'; import ResumeExpandableText from '../shared/ResumeExpandableText'; @@ -23,141 +16,55 @@ type ResumeCommentListItemProps = { userId: string | undefined; }; -type ICommentInput = { - description: string; -}; - export default function ResumeCommentListItem({ comment, userId, }: ResumeCommentListItemProps) { const isCommentOwner = userId === comment.user.userId; const [isEditingComment, setIsEditingComment] = useState(false); - - const [upvoteAnimation, setUpvoteAnimation] = useState(false); - const [downvoteAnimation, setDownvoteAnimation] = useState(false); - - const { - register, - handleSubmit, - setValue, - formState: { errors, isDirty }, - reset, - } = useForm({ - defaultValues: { - description: comment.description, - }, - }); - - const trpcContext = trpc.useContext(); - const commentUpdateMutation = trpc.useMutation( - 'resumes.comments.user.update', - { - onSuccess: () => { - // Comment updated, invalidate query to trigger refetch - trpcContext.invalidateQueries(['resumes.comments.list']); - }, - }, - ); - - // COMMENT VOTES - const commentVotesQuery = trpc.useQuery([ - 'resumes.comments.votes.list', - { commentId: comment.id }, - ]); - const commentVotesUpsertMutation = trpc.useMutation( - 'resumes.comments.votes.user.upsert', - { - onSuccess: () => { - // Comment updated, invalidate query to trigger refetch - trpcContext.invalidateQueries(['resumes.comments.votes.list']); - }, - }, - ); - const commentVotesDeleteMutation = trpc.useMutation( - 'resumes.comments.votes.user.delete', - { - onSuccess: () => { - // Comment updated, invalidate query to trigger refetch - trpcContext.invalidateQueries(['resumes.comments.votes.list']); - }, - }, - ); - - // FORM ACTIONS - const onCancel = () => { - reset({ description: comment.description }); - setIsEditingComment(false); - }; - - const onSubmit: SubmitHandler = async (data) => { - const { id } = comment; - return commentUpdateMutation.mutate( - { - id, - ...data, - }, - { - onSuccess: () => { - setIsEditingComment(false); - }, - }, - ); - }; - - const setFormValue = (value: string) => { - setValue('description', value.trim(), { shouldDirty: true }); - }; - - const onVote = async ( - value: Vote, - setAnimation: Dispatch>, - ) => { - setAnimation(true); - - if (commentVotesQuery.data?.userVote?.value === value) { - return commentVotesDeleteMutation.mutate( - { - commentId: comment.id, - }, - { - onSettled: async () => setAnimation(false), - }, - ); - } - return commentVotesUpsertMutation.mutate( - { - commentId: comment.id, - value, - }, - { - onSettled: async () => setAnimation(false), - }, - ); - }; + const [isReplyingComment, setIsReplyingComment] = useState(false); + const [showReplies, setShowReplies] = useState(true); return ( -
+
+ {/* Image Icon */} {comment.user.image ? ( {comment.user.name ) : ( - + )}
{/* Name and creation time */}
-

+

{comment.user.name ?? 'Reviewer ABC'}

-

+

{isCommentOwner ? '(Me)' : ''}

@@ -174,112 +81,92 @@ export default function ResumeCommentListItem({ {/* Description */} {isEditingComment ? ( -
-
-