mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-31 14:12:37 +08:00
[questions][feat] sort answers, comments (#457)
Co-authored-by: Jeff Sieu <jeffsy00@gmail.com>
This commit is contained in:
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `location` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "location",
|
||||||
|
ADD COLUMN "cityId" TEXT,
|
||||||
|
ADD COLUMN "countryId" TEXT,
|
||||||
|
ADD COLUMN "stateId" TEXT,
|
||||||
|
ALTER COLUMN "companyId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswer_updatedAt_id_idx" ON "QuestionsAnswer"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswer_upvotes_id_idx" ON "QuestionsAnswer"("upvotes", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswerComment_updatedAt_id_idx" ON "QuestionsAnswerComment"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswerComment_upvotes_id_idx" ON "QuestionsAnswerComment"("upvotes", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsQuestionComment_updatedAt_id_idx" ON "QuestionsQuestionComment"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsQuestionComment_upvotes_id_idx" ON "QuestionsQuestionComment"("upvotes", "id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "State"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "QuestionsQuestionType" ADD VALUE 'THEORY';
|
@ -107,27 +107,30 @@ model Company {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Country {
|
model Country {
|
||||||
id String @id
|
id String @id
|
||||||
name String @unique
|
name String @unique
|
||||||
code String @unique
|
code String @unique
|
||||||
states State[]
|
states State[]
|
||||||
|
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model State {
|
model State {
|
||||||
id String @id
|
id String @id
|
||||||
name String
|
name String
|
||||||
countryId String
|
countryId String
|
||||||
cities City[]
|
cities City[]
|
||||||
country Country @relation(fields: [countryId], references: [id])
|
country Country @relation(fields: [countryId], references: [id])
|
||||||
|
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||||
|
|
||||||
@@unique([name, countryId])
|
@@unique([name, countryId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model City {
|
model City {
|
||||||
id String @id
|
id String @id
|
||||||
name String
|
name String
|
||||||
stateId String
|
stateId String
|
||||||
state State @relation(fields: [stateId], references: [id])
|
state State @relation(fields: [stateId], references: [id])
|
||||||
|
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||||
|
|
||||||
@@unique([name, stateId])
|
@@unique([name, stateId])
|
||||||
}
|
}
|
||||||
@ -423,6 +426,7 @@ enum QuestionsQuestionType {
|
|||||||
CODING
|
CODING
|
||||||
SYSTEM_DESIGN
|
SYSTEM_DESIGN
|
||||||
BEHAVIORAL
|
BEHAVIORAL
|
||||||
|
THEORY
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuestionsQuestion {
|
model QuestionsQuestion {
|
||||||
@ -435,12 +439,12 @@ model QuestionsQuestion {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
encounters QuestionsQuestionEncounter[]
|
encounters QuestionsQuestionEncounter[]
|
||||||
votes QuestionsQuestionVote[]
|
votes QuestionsQuestionVote[]
|
||||||
comments QuestionsQuestionComment[]
|
comments QuestionsQuestionComment[]
|
||||||
answers QuestionsAnswer[]
|
answers QuestionsAnswer[]
|
||||||
QuestionsListQuestionEntry QuestionsListQuestionEntry[]
|
questionsListQuestionEntries QuestionsListQuestionEntry[]
|
||||||
|
|
||||||
@@index([lastSeenAt, id])
|
@@index([lastSeenAt, id])
|
||||||
@@index([upvotes, id])
|
@@index([upvotes, id])
|
||||||
@ -450,14 +454,18 @@ model QuestionsQuestionEncounter {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
questionId String
|
questionId String
|
||||||
userId String?
|
userId String?
|
||||||
// TODO: sync with models (location, role)
|
companyId String?
|
||||||
companyId String
|
countryId String?
|
||||||
location String @db.Text
|
stateId String?
|
||||||
|
cityId String?
|
||||||
role String @db.Text
|
role String @db.Text
|
||||||
seenAt DateTime
|
seenAt DateTime
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
country Country? @relation(fields: [countryId], references: [id], onDelete: SetNull)
|
||||||
|
state State? @relation(fields: [stateId], references: [id], onDelete: SetNull)
|
||||||
|
city City? @relation(fields: [cityId], references: [id], onDelete: SetNull)
|
||||||
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||||
@ -489,6 +497,9 @@ model QuestionsQuestionComment {
|
|||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||||
votes QuestionsQuestionCommentVote[]
|
votes QuestionsQuestionCommentVote[]
|
||||||
|
|
||||||
|
@@index([updatedAt, id])
|
||||||
|
@@index([upvotes, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuestionsQuestionCommentVote {
|
model QuestionsQuestionCommentVote {
|
||||||
@ -518,6 +529,9 @@ model QuestionsAnswer {
|
|||||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||||
votes QuestionsAnswerVote[]
|
votes QuestionsAnswerVote[]
|
||||||
comments QuestionsAnswerComment[]
|
comments QuestionsAnswerComment[]
|
||||||
|
|
||||||
|
@@index([updatedAt, id])
|
||||||
|
@@index([upvotes, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuestionsAnswerVote {
|
model QuestionsAnswerVote {
|
||||||
@ -546,6 +560,9 @@ model QuestionsAnswerComment {
|
|||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade)
|
answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade)
|
||||||
votes QuestionsAnswerCommentVote[]
|
votes QuestionsAnswerCommentVote[]
|
||||||
|
|
||||||
|
@@index([updatedAt, id])
|
||||||
|
@@index([upvotes, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuestionsAnswerCommentVote {
|
model QuestionsAnswerCommentVote {
|
||||||
|
@ -28,54 +28,56 @@ export default function ContributeQuestionCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className="w-full">
|
||||||
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
|
<button
|
||||||
type="button"
|
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
|
||||||
onClick={handleOpenContribute}>
|
type="button"
|
||||||
<TextInput
|
onClick={handleOpenContribute}>
|
||||||
disabled={true}
|
<TextInput
|
||||||
isLabelHidden={true}
|
disabled={true}
|
||||||
label="Question"
|
isLabelHidden={true}
|
||||||
placeholder="Contribute a question"
|
label="Question"
|
||||||
onChange={handleOpenContribute}
|
placeholder="Contribute a question"
|
||||||
/>
|
onChange={handleOpenContribute}
|
||||||
<div className="flex flex-wrap items-end justify-center gap-x-2">
|
/>
|
||||||
<div className="min-w-[150px] flex-1">
|
<div className="flex flex-wrap items-end justify-center gap-x-2">
|
||||||
<TextInput
|
<div className="min-w-[150px] flex-1">
|
||||||
disabled={true}
|
<TextInput
|
||||||
label="Company"
|
disabled={true}
|
||||||
startAddOn={BuildingOffice2Icon}
|
label="Company"
|
||||||
startAddOnType="icon"
|
startAddOn={BuildingOffice2Icon}
|
||||||
onChange={handleOpenContribute}
|
startAddOnType="icon"
|
||||||
/>
|
onChange={handleOpenContribute}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-[150px] flex-1">
|
||||||
|
<TextInput
|
||||||
|
disabled={true}
|
||||||
|
label="Question type"
|
||||||
|
startAddOn={QuestionMarkCircleIcon}
|
||||||
|
startAddOnType="icon"
|
||||||
|
onChange={handleOpenContribute}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-[150px] flex-1">
|
||||||
|
<TextInput
|
||||||
|
disabled={true}
|
||||||
|
label="Date"
|
||||||
|
startAddOn={CalendarDaysIcon}
|
||||||
|
startAddOnType="icon"
|
||||||
|
onChange={handleOpenContribute}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className="bg-primary-600 hover:bg-primary-700 rounded-full px-3 py-2 text-white">
|
||||||
|
Contribute
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-[150px] flex-1">
|
<ContributeQuestionDialog
|
||||||
<TextInput
|
show={showDraftDialog}
|
||||||
disabled={true}
|
onCancel={handleDraftDialogCancel}
|
||||||
label="Question type"
|
onSubmit={onSubmit}
|
||||||
startAddOn={QuestionMarkCircleIcon}
|
/>
|
||||||
startAddOnType="icon"
|
</button>
|
||||||
onChange={handleOpenContribute}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-[150px] flex-1">
|
|
||||||
<TextInput
|
|
||||||
disabled={true}
|
|
||||||
label="Date"
|
|
||||||
startAddOn={CalendarDaysIcon}
|
|
||||||
startAddOnType="icon"
|
|
||||||
onChange={handleOpenContribute}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1 className="bg-primary-600 hover:bg-primary-700 rounded-full px-3 py-2 text-white">
|
|
||||||
Contribute
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<ContributeQuestionDialog
|
|
||||||
show={showDraftDialog}
|
|
||||||
onCancel={handleDraftDialogCancel}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import type { UseInfiniteQueryResult } from 'react-query';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
export type PaginationLoadMoreButtonProps = {
|
||||||
|
query: UseInfiniteQueryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PaginationLoadMoreButton(
|
||||||
|
props: PaginationLoadMoreButtonProps,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
query: { hasNextPage, isFetchingNextPage, fetchNextPage },
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={() => {
|
||||||
|
fetchNextPage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -2,40 +2,19 @@ import {
|
|||||||
AdjustmentsHorizontalIcon,
|
AdjustmentsHorizontalIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { Button, Select, TextInput } from '@tih/ui';
|
import { Button, TextInput } from '@tih/ui';
|
||||||
|
|
||||||
export type SortOption<Value> = {
|
import type { SortOptionsSelectProps } from './SortOptionsSelect';
|
||||||
label: string;
|
import SortOptionsSelect from './SortOptionsSelect';
|
||||||
value: Value;
|
|
||||||
|
export type QuestionSearchBarProps = SortOptionsSelectProps & {
|
||||||
|
onFilterOptionsToggle: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SortOrderProps<SortOrder> = {
|
export default function QuestionSearchBar({
|
||||||
onSortOrderChange?: (sortValue: SortOrder) => void;
|
|
||||||
sortOrderOptions: ReadonlyArray<SortOption<SortOrder>>;
|
|
||||||
sortOrderValue: SortOrder;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SortTypeProps<SortType> = {
|
|
||||||
onSortTypeChange?: (sortType: SortType) => void;
|
|
||||||
sortTypeOptions: ReadonlyArray<SortOption<SortType>>;
|
|
||||||
sortTypeValue: SortType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuestionSearchBarProps<SortType, SortOrder> =
|
|
||||||
SortOrderProps<SortOrder> &
|
|
||||||
SortTypeProps<SortType> & {
|
|
||||||
onFilterOptionsToggle: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function QuestionSearchBar<SortType, SortOrder>({
|
|
||||||
onSortOrderChange,
|
|
||||||
sortOrderOptions,
|
|
||||||
sortOrderValue,
|
|
||||||
onSortTypeChange,
|
|
||||||
sortTypeOptions,
|
|
||||||
sortTypeValue,
|
|
||||||
onFilterOptionsToggle,
|
onFilterOptionsToggle,
|
||||||
}: QuestionSearchBarProps<SortType, SortOrder>) {
|
...sortOptionsSelectProps
|
||||||
|
}: QuestionSearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end">
|
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end">
|
||||||
<div className="flex-1 ">
|
<div className="flex-1 ">
|
||||||
@ -48,38 +27,7 @@ export default function QuestionSearchBar<SortType, SortOrder>({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-end gap-4">
|
<div className="flex items-end justify-end gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<SortOptionsSelect {...sortOptionsSelectProps} />
|
||||||
<Select
|
|
||||||
display="inline"
|
|
||||||
label="Sort by"
|
|
||||||
options={sortTypeOptions}
|
|
||||||
value={sortTypeValue}
|
|
||||||
onChange={(value) => {
|
|
||||||
const chosenOption = sortTypeOptions.find(
|
|
||||||
(option) => String(option.value) === value,
|
|
||||||
);
|
|
||||||
if (chosenOption) {
|
|
||||||
onSortTypeChange?.(chosenOption.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Select
|
|
||||||
display="inline"
|
|
||||||
label="Order by"
|
|
||||||
options={sortOrderOptions}
|
|
||||||
value={sortOrderValue}
|
|
||||||
onChange={(value) => {
|
|
||||||
const chosenOption = sortOrderOptions.find(
|
|
||||||
(option) => String(option.value) === value,
|
|
||||||
);
|
|
||||||
if (chosenOption) {
|
|
||||||
onSortOrderChange?.(chosenOption.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
<Button
|
<Button
|
||||||
addonPosition="start"
|
addonPosition="start"
|
||||||
|
69
apps/portal/src/components/questions/SortOptionsSelect.tsx
Normal file
69
apps/portal/src/components/questions/SortOptionsSelect.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Select } from '~/../../../packages/ui/dist';
|
||||||
|
import { SORT_ORDERS, SORT_TYPES } from '~/utils/questions/constants';
|
||||||
|
|
||||||
|
import type { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
|
export type SortOption<Value> = {
|
||||||
|
label: string;
|
||||||
|
value: Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortTypeOptions = SORT_TYPES;
|
||||||
|
const sortOrderOptions = SORT_ORDERS;
|
||||||
|
|
||||||
|
type SortOrderProps<Order> = {
|
||||||
|
onSortOrderChange?: (sortValue: Order) => void;
|
||||||
|
sortOrderValue: Order;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortTypeProps<Type> = {
|
||||||
|
onSortTypeChange?: (sortType: Type) => void;
|
||||||
|
sortTypeValue: Type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortOptionsSelectProps = SortOrderProps<SortOrder> &
|
||||||
|
SortTypeProps<SortType>;
|
||||||
|
|
||||||
|
export default function SortOptionsSelect({
|
||||||
|
onSortOrderChange,
|
||||||
|
sortOrderValue,
|
||||||
|
onSortTypeChange,
|
||||||
|
sortTypeValue,
|
||||||
|
}: SortOptionsSelectProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-end justify-end gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Sort by"
|
||||||
|
options={sortTypeOptions}
|
||||||
|
value={sortTypeValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortTypeOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortTypeChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Order by"
|
||||||
|
options={sortOrderOptions}
|
||||||
|
value={sortOrderValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortOrderOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortOrderChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ChatBubbleBottomCenterTextIcon,
|
ChatBubbleBottomCenterTextIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
@ -18,6 +18,8 @@ import QuestionAggregateBadge from '../../QuestionAggregateBadge';
|
|||||||
import QuestionTypeBadge from '../../QuestionTypeBadge';
|
import QuestionTypeBadge from '../../QuestionTypeBadge';
|
||||||
import VotingButtons from '../../VotingButtons';
|
import VotingButtons from '../../VotingButtons';
|
||||||
|
|
||||||
|
import type { CountryInfo } from '~/types/questions';
|
||||||
|
|
||||||
type UpvoteProps =
|
type UpvoteProps =
|
||||||
| {
|
| {
|
||||||
showVoteButtons: true;
|
showVoteButtons: true;
|
||||||
@ -51,13 +53,13 @@ type AnswerStatisticsProps =
|
|||||||
type AggregateStatisticsProps =
|
type AggregateStatisticsProps =
|
||||||
| {
|
| {
|
||||||
companies: Record<string, number>;
|
companies: Record<string, number>;
|
||||||
locations: Record<string, number>;
|
countries: Record<string, CountryInfo>;
|
||||||
roles: Record<string, number>;
|
roles: Record<string, number>;
|
||||||
showAggregateStatistics: true;
|
showAggregateStatistics: true;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
companies?: never;
|
companies?: never;
|
||||||
locations?: never;
|
countries?: never;
|
||||||
roles?: never;
|
roles?: never;
|
||||||
showAggregateStatistics?: false;
|
showAggregateStatistics?: false;
|
||||||
};
|
};
|
||||||
@ -136,7 +138,7 @@ export default function BaseQuestionCard({
|
|||||||
upvoteCount,
|
upvoteCount,
|
||||||
timestamp,
|
timestamp,
|
||||||
roles,
|
roles,
|
||||||
locations,
|
countries,
|
||||||
showHover,
|
showHover,
|
||||||
onReceivedSubmit,
|
onReceivedSubmit,
|
||||||
showDeleteButton,
|
showDeleteButton,
|
||||||
@ -147,6 +149,22 @@ export default function BaseQuestionCard({
|
|||||||
const [showReceivedForm, setShowReceivedForm] = useState(false);
|
const [showReceivedForm, setShowReceivedForm] = useState(false);
|
||||||
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
|
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
|
||||||
const hoverClass = showHover ? 'hover:bg-slate-50' : '';
|
const hoverClass = showHover ? 'hover:bg-slate-50' : '';
|
||||||
|
|
||||||
|
const locations = useMemo(() => {
|
||||||
|
if (countries === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCount: Record<string, number> = {};
|
||||||
|
// Decompose countries
|
||||||
|
for (const country of Object.keys(countries)) {
|
||||||
|
const { total } = countries[country];
|
||||||
|
countryCount[country] = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return countryCount;
|
||||||
|
}, [countries]);
|
||||||
|
|
||||||
const cardContent = (
|
const cardContent = (
|
||||||
<>
|
<>
|
||||||
{showVoteButtons && (
|
{showVoteButtons && (
|
||||||
@ -168,7 +186,7 @@ export default function BaseQuestionCard({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
/>
|
/>
|
||||||
<QuestionAggregateBadge
|
<QuestionAggregateBadge
|
||||||
statistics={locations}
|
statistics={locations!}
|
||||||
variant="success"
|
variant="success"
|
||||||
/>
|
/>
|
||||||
<QuestionAggregateBadge statistics={roles} variant="danger" />
|
<QuestionAggregateBadge statistics={roles} variant="danger" />
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { startOfMonth } from 'date-fns';
|
import { startOfMonth } from 'date-fns';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import type { QuestionsQuestionType } from '@prisma/client';
|
import type { QuestionsQuestionType } from '@prisma/client';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
|
import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
|
||||||
|
|
||||||
import { LOCATIONS, QUESTION_TYPES, ROLES } from '~/utils/questions/constants';
|
import { QUESTION_TYPES } from '~/utils/questions/constants';
|
||||||
import {
|
import {
|
||||||
useFormRegister,
|
useFormRegister,
|
||||||
useSelectRegister,
|
useSelectRegister,
|
||||||
@ -15,14 +16,16 @@ import RoleTypeahead from '../typeahead/RoleTypeahead';
|
|||||||
import type { Month } from '../../shared/MonthYearPicker';
|
import type { Month } from '../../shared/MonthYearPicker';
|
||||||
import MonthYearPicker from '../../shared/MonthYearPicker';
|
import MonthYearPicker from '../../shared/MonthYearPicker';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions';
|
||||||
|
|
||||||
export type ContributeQuestionData = {
|
export type ContributeQuestionData = {
|
||||||
company: string;
|
company: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
location: string;
|
location: Location & TypeaheadOption;
|
||||||
position: string;
|
position: string;
|
||||||
questionContent: string;
|
questionContent: string;
|
||||||
questionType: QuestionsQuestionType;
|
questionType: QuestionsQuestionType;
|
||||||
role: string;
|
role: TypeaheadOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContributeQuestionFormProps = {
|
export type ContributeQuestionFormProps = {
|
||||||
@ -79,15 +82,12 @@ export default function ContributeQuestionForm({
|
|||||||
name="location"
|
name="location"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<LocationTypeahead
|
<LocationTypeahead
|
||||||
|
{...field}
|
||||||
required={true}
|
required={true}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
// @ts-ignore TODO(questions): handle potentially null value.
|
// @ts-ignore TODO(questions): handle potentially null value.
|
||||||
field.onChange(option.value);
|
field.onChange(option);
|
||||||
}}
|
}}
|
||||||
{...field}
|
|
||||||
value={LOCATIONS.find(
|
|
||||||
(location) => location.value === field.value,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -117,8 +117,9 @@ export default function ContributeQuestionForm({
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="company"
|
name="company"
|
||||||
render={({ field }) => (
|
render={({ field: { value: _, ...field } }) => (
|
||||||
<CompanyTypeahead
|
<CompanyTypeahead
|
||||||
|
{...field}
|
||||||
required={true}
|
required={true}
|
||||||
// @ts-ignore TODO(questions): handle potentially null value.
|
// @ts-ignore TODO(questions): handle potentially null value.
|
||||||
onSelect={({ id }) => {
|
onSelect={({ id }) => {
|
||||||
@ -134,13 +135,12 @@ export default function ContributeQuestionForm({
|
|||||||
name="role"
|
name="role"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RoleTypeahead
|
<RoleTypeahead
|
||||||
|
{...field}
|
||||||
required={true}
|
required={true}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
// @ts-ignore TODO(questions): handle potentially null value.
|
// @ts-ignore TODO(questions): handle potentially null value.
|
||||||
field.onChange(option.value);
|
field.onChange(option);
|
||||||
}}
|
}}
|
||||||
{...field}
|
|
||||||
value={ROLES.find((role) => role.value === field.value)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -9,11 +9,15 @@ import CompanyTypeahead from '../typeahead/CompanyTypeahead';
|
|||||||
import LocationTypeahead from '../typeahead/LocationTypeahead';
|
import LocationTypeahead from '../typeahead/LocationTypeahead';
|
||||||
import RoleTypeahead from '../typeahead/RoleTypeahead';
|
import RoleTypeahead from '../typeahead/RoleTypeahead';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions';
|
||||||
|
|
||||||
export type CreateQuestionEncounterData = {
|
export type CreateQuestionEncounterData = {
|
||||||
|
cityId?: string;
|
||||||
company: string;
|
company: string;
|
||||||
location: string;
|
countryId: string;
|
||||||
role: string;
|
role: string;
|
||||||
seenAt: Date;
|
seenAt: Date;
|
||||||
|
stateId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateQuestionEncounterFormProps = {
|
export type CreateQuestionEncounterFormProps = {
|
||||||
@ -28,7 +32,9 @@ export default function CreateQuestionEncounterForm({
|
|||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
const [selectedCompany, setSelectedCompany] = useState<string | null>(null);
|
const [selectedCompany, setSelectedCompany] = useState<string | null>(null);
|
||||||
const [selectedLocation, setSelectedLocation] = useState<string | null>(null);
|
const [selectedLocation, setSelectedLocation] = useState<Location | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [selectedRole, setSelectedRole] = useState<string | null>(null);
|
const [selectedRole, setSelectedRole] = useState<string | null>(null);
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(
|
const [selectedDate, setSelectedDate] = useState<Date>(
|
||||||
startOfMonth(new Date()),
|
startOfMonth(new Date()),
|
||||||
@ -61,10 +67,10 @@ export default function CreateQuestionEncounterForm({
|
|||||||
placeholder="Other location"
|
placeholder="Other location"
|
||||||
suggestedCount={3}
|
suggestedCount={3}
|
||||||
// @ts-ignore TODO(questions): handle potentially null value.
|
// @ts-ignore TODO(questions): handle potentially null value.
|
||||||
onSelect={({ value: location }) => {
|
onSelect={(location) => {
|
||||||
setSelectedLocation(location);
|
setSelectedLocation(location);
|
||||||
}}
|
}}
|
||||||
onSuggestionClick={({ value: location }) => {
|
onSuggestionClick={(location) => {
|
||||||
setSelectedLocation(location);
|
setSelectedLocation(location);
|
||||||
setStep(step + 1);
|
setStep(step + 1);
|
||||||
}}
|
}}
|
||||||
@ -130,11 +136,14 @@ export default function CreateQuestionEncounterForm({
|
|||||||
selectedRole &&
|
selectedRole &&
|
||||||
selectedDate
|
selectedDate
|
||||||
) {
|
) {
|
||||||
|
const { cityId, stateId, countryId } = selectedLocation;
|
||||||
onSubmit({
|
onSubmit({
|
||||||
|
cityId,
|
||||||
company: selectedCompany,
|
company: selectedCompany,
|
||||||
location: selectedLocation,
|
countryId,
|
||||||
role: selectedRole,
|
role: selectedRole,
|
||||||
seenAt: selectedDate,
|
seenAt: selectedDate,
|
||||||
|
stateId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -8,13 +8,16 @@ import type { RequireAllOrNone } from '~/utils/questions/RequireAllOrNone';
|
|||||||
type TypeaheadProps = ComponentProps<typeof Typeahead>;
|
type TypeaheadProps = ComponentProps<typeof Typeahead>;
|
||||||
type TypeaheadOption = TypeaheadProps['options'][number];
|
type TypeaheadOption = TypeaheadProps['options'][number];
|
||||||
|
|
||||||
export type ExpandedTypeaheadProps = RequireAllOrNone<{
|
export type ExpandedTypeaheadProps = Omit<TypeaheadProps, 'onSelect'> &
|
||||||
clearOnSelect?: boolean;
|
RequireAllOrNone<{
|
||||||
filterOption: (option: TypeaheadOption) => boolean;
|
clearOnSelect?: boolean;
|
||||||
onSuggestionClick: (option: TypeaheadOption) => void;
|
filterOption: (option: TypeaheadOption) => boolean;
|
||||||
suggestedCount: number;
|
onSuggestionClick: (option: TypeaheadOption) => void;
|
||||||
}> &
|
suggestedCount: number;
|
||||||
TypeaheadProps;
|
}> & {
|
||||||
|
onChange?: unknown; // Workaround: This prop is here just to absorb the onChange returned react-hook-form
|
||||||
|
onSelect: (option: TypeaheadOption) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ExpandedTypeahead({
|
export default function ExpandedTypeahead({
|
||||||
suggestedCount = 0,
|
suggestedCount = 0,
|
||||||
@ -23,6 +26,7 @@ export default function ExpandedTypeahead({
|
|||||||
clearOnSelect = false,
|
clearOnSelect = false,
|
||||||
options,
|
options,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onChange: _,
|
||||||
...typeaheadProps
|
...typeaheadProps
|
||||||
}: ExpandedTypeaheadProps) {
|
}: ExpandedTypeaheadProps) {
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
@ -55,7 +59,8 @@ export default function ExpandedTypeahead({
|
|||||||
if (clearOnSelect) {
|
if (clearOnSelect) {
|
||||||
setKey((key + 1) % 2);
|
setKey((key + 1) % 2);
|
||||||
}
|
}
|
||||||
onSelect(option);
|
// TODO: Remove onSelect null coercion once onSelect prop is refactored
|
||||||
|
onSelect(option!);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,71 @@
|
|||||||
import { LOCATIONS } from '~/utils/questions/constants';
|
import { useMemo, useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
import ExpandedTypeahead from './ExpandedTypeahead';
|
import ExpandedTypeahead from './ExpandedTypeahead';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions';
|
||||||
|
|
||||||
export type LocationTypeaheadProps = Omit<
|
export type LocationTypeaheadProps = Omit<
|
||||||
ExpandedTypeaheadProps,
|
ExpandedTypeaheadProps,
|
||||||
'label' | 'onQueryChange' | 'options'
|
'label' | 'onQueryChange' | 'onSelect' | 'onSuggestionClick' | 'options'
|
||||||
>;
|
> & {
|
||||||
|
onSelect: (option: Location & TypeaheadOption) => void;
|
||||||
|
onSuggestionClick?: (option: Location) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LocationTypeahead({
|
||||||
|
onSelect,
|
||||||
|
onSuggestionClick,
|
||||||
|
...restProps
|
||||||
|
}: LocationTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const { data: locations } = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const locationOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
locations?.map(({ id, name, state }) => ({
|
||||||
|
cityId: id,
|
||||||
|
countryId: state.country.id,
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state.name}, ${state.country.name}`,
|
||||||
|
stateId: state.id,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [locations]);
|
||||||
|
|
||||||
export default function LocationTypeahead(props: LocationTypeaheadProps) {
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...({
|
||||||
|
onSuggestionClick: onSuggestionClick
|
||||||
|
? (option: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find(
|
||||||
|
(locationOption) => locationOption.id === option.id,
|
||||||
|
)!;
|
||||||
|
onSuggestionClick({
|
||||||
|
...location,
|
||||||
|
...option,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
...restProps,
|
||||||
|
} as ExpandedTypeaheadProps)}
|
||||||
label="Location"
|
label="Location"
|
||||||
options={LOCATIONS}
|
options={locationOptions}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
onQueryChange={setQuery}
|
||||||
onQueryChange={() => {}}
|
onSelect={({ id }: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find((option) => option.id === id)!;
|
||||||
|
onSelect(location);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
@ -17,13 +19,16 @@ const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...(props as ExpandedTypeaheadProps)}
|
||||||
label="Role"
|
label="Role"
|
||||||
options={ROLES}
|
options={ROLES.filter((option) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
option.label.toLowerCase().includes(query.toLowerCase()),
|
||||||
onQueryChange={() => {}}
|
)}
|
||||||
|
onQueryChange={setQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, Select, TextArea } from '@tih/ui';
|
import { Button, TextArea } from '@tih/ui';
|
||||||
|
|
||||||
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
||||||
import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
|
import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
|
||||||
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
||||||
|
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||||
|
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
|
||||||
|
|
||||||
import { APP_TITLE } from '~/utils/questions/constants';
|
import { APP_TITLE } from '~/utils/questions/constants';
|
||||||
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
export type AnswerCommentData = {
|
export type AnswerCommentData = {
|
||||||
commentContent: string;
|
commentContent: string;
|
||||||
};
|
};
|
||||||
@ -19,6 +24,13 @@ export type AnswerCommentData = {
|
|||||||
export default function QuestionPage() {
|
export default function QuestionPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [commentSortOrder, setCommentSortOrder] = useState<SortOrder>(
|
||||||
|
SortOrder.DESC,
|
||||||
|
);
|
||||||
|
const [commentSortType, setCommentSortType] = useState<SortType>(
|
||||||
|
SortType.NEW,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register: comRegister,
|
register: comRegister,
|
||||||
reset: resetComment,
|
reset: resetComment,
|
||||||
@ -36,10 +48,23 @@ export default function QuestionPage() {
|
|||||||
{ answerId: answerId as string },
|
{ answerId: answerId as string },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { data: comments } = trpc.useQuery([
|
const answerCommentInfiniteQuery = trpc.useInfiniteQuery(
|
||||||
'questions.answers.comments.getAnswerComments',
|
[
|
||||||
{ answerId: answerId as string },
|
'questions.answers.comments.getAnswerComments',
|
||||||
]);
|
{
|
||||||
|
answerId: answerId as string,
|
||||||
|
limit: 5,
|
||||||
|
sortOrder: commentSortOrder,
|
||||||
|
sortType: commentSortType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: answerCommentsData } = answerCommentInfiniteQuery;
|
||||||
|
|
||||||
const { mutate: addComment } = trpc.useMutation(
|
const { mutate: addComment } = trpc.useMutation(
|
||||||
'questions.answers.comments.user.create',
|
'questions.answers.comments.user.create',
|
||||||
@ -47,7 +72,11 @@ export default function QuestionPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.invalidateQueries([
|
utils.invalidateQueries([
|
||||||
'questions.answers.comments.getAnswerComments',
|
'questions.answers.comments.getAnswerComments',
|
||||||
{ answerId: answerId as string },
|
{
|
||||||
|
answerId: answerId as string,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
sortType: SortType.NEW,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -108,32 +137,6 @@ export default function QuestionPage() {
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<div className="my-3 flex justify-between">
|
<div className="my-3 flex justify-between">
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span aria-hidden={true} className="text-sm">
|
|
||||||
Sort by:
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
display="inline"
|
|
||||||
isLabelHidden={true}
|
|
||||||
label="Sort by"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: 'Most recent',
|
|
||||||
value: 'most-recent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Most upvotes',
|
|
||||||
value: 'most-upvotes',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value="most-recent"
|
|
||||||
onChange={(value) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!isCommentDirty || !isCommentValid}
|
disabled={!isCommentDirty || !isCommentValid}
|
||||||
label="Post"
|
label="Post"
|
||||||
@ -142,18 +145,35 @@ export default function QuestionPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
{(comments ?? []).map((comment) => (
|
<div className="flex items-center justify-between gap-2">
|
||||||
<AnswerCommentListItem
|
<p className="text-lg">Comments</p>
|
||||||
key={comment.id}
|
<div className="flex items-end gap-2">
|
||||||
answerCommentId={comment.id}
|
<SortOptionsSelect
|
||||||
authorImageUrl={comment.userImage}
|
sortOrderValue={commentSortOrder}
|
||||||
authorName={comment.user}
|
sortTypeValue={commentSortType}
|
||||||
content={comment.content}
|
onSortOrderChange={setCommentSortOrder}
|
||||||
createdAt={comment.createdAt}
|
onSortTypeChange={setCommentSortType}
|
||||||
upvoteCount={comment.numVotes}
|
/>
|
||||||
/>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
{/* TODO: Allow to load more pages */}
|
||||||
|
{(answerCommentsData?.pages ?? []).flatMap(
|
||||||
|
({ processedQuestionAnswerCommentsData: comments }) =>
|
||||||
|
comments.map((comment) => (
|
||||||
|
<AnswerCommentListItem
|
||||||
|
key={comment.id}
|
||||||
|
answerCommentId={comment.id}
|
||||||
|
authorImageUrl={comment.userImage}
|
||||||
|
authorName={comment.user}
|
||||||
|
content={comment.content}
|
||||||
|
createdAt={comment.createdAt}
|
||||||
|
upvoteCount={comment.numVotes}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
<PaginationLoadMoreButton query={answerCommentInfiniteQuery} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, Collapsible, Select, TextArea } from '@tih/ui';
|
import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
|
||||||
|
|
||||||
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
||||||
import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
|
import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
|
||||||
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
|
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
|
||||||
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
||||||
|
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||||
|
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
|
||||||
|
|
||||||
import { APP_TITLE } from '~/utils/questions/constants';
|
import { APP_TITLE } from '~/utils/questions/constants';
|
||||||
import createSlug from '~/utils/questions/createSlug';
|
import createSlug from '~/utils/questions/createSlug';
|
||||||
@ -16,6 +18,8 @@ import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregat
|
|||||||
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
export type AnswerQuestionData = {
|
export type AnswerQuestionData = {
|
||||||
answerContent: string;
|
answerContent: string;
|
||||||
};
|
};
|
||||||
@ -26,6 +30,19 @@ export type QuestionCommentData = {
|
|||||||
|
|
||||||
export default function QuestionPage() {
|
export default function QuestionPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [answerSortOrder, setAnswerSortOrder] = useState<SortOrder>(
|
||||||
|
SortOrder.DESC,
|
||||||
|
);
|
||||||
|
const [answerSortType, setAnswerSortType] = useState<SortType>(SortType.NEW);
|
||||||
|
|
||||||
|
const [commentSortOrder, setCommentSortOrder] = useState<SortOrder>(
|
||||||
|
SortOrder.DESC,
|
||||||
|
);
|
||||||
|
const [commentSortType, setCommentSortType] = useState<SortType>(
|
||||||
|
SortType.NEW,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register: ansRegister,
|
register: ansRegister,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -64,10 +81,23 @@ export default function QuestionPage() {
|
|||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const { data: comments } = trpc.useQuery([
|
const commentInfiniteQuery = trpc.useInfiniteQuery(
|
||||||
'questions.questions.comments.getQuestionComments',
|
[
|
||||||
{ questionId: questionId as string },
|
'questions.questions.comments.getQuestionComments',
|
||||||
]);
|
{
|
||||||
|
limit: 5,
|
||||||
|
questionId: questionId as string,
|
||||||
|
sortOrder: commentSortOrder,
|
||||||
|
sortType: commentSortType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: commentData } = commentInfiniteQuery;
|
||||||
|
|
||||||
const { mutate: addComment } = trpc.useMutation(
|
const { mutate: addComment } = trpc.useMutation(
|
||||||
'questions.questions.comments.user.create',
|
'questions.questions.comments.user.create',
|
||||||
@ -80,10 +110,23 @@ export default function QuestionPage() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: answers } = trpc.useQuery([
|
const answerInfiniteQuery = trpc.useInfiniteQuery(
|
||||||
'questions.answers.getAnswers',
|
[
|
||||||
{ questionId: questionId as string },
|
'questions.answers.getAnswers',
|
||||||
]);
|
{
|
||||||
|
limit: 5,
|
||||||
|
questionId: questionId as string,
|
||||||
|
sortOrder: answerSortOrder,
|
||||||
|
sortType: answerSortType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: answerData } = answerInfiniteQuery;
|
||||||
|
|
||||||
const { mutate: addAnswer } = trpc.useMutation(
|
const { mutate: addAnswer } = trpc.useMutation(
|
||||||
'questions.answers.user.create',
|
'questions.answers.user.create',
|
||||||
@ -144,12 +187,12 @@ export default function QuestionPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
|
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
|
||||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||||
<FullQuestionCard
|
<FullQuestionCard
|
||||||
{...question}
|
{...question}
|
||||||
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
||||||
locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
|
countries={relabeledAggregatedEncounters?.countryCounts ?? {}}
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
receivedCount={undefined}
|
receivedCount={undefined}
|
||||||
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
|
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
|
||||||
@ -160,78 +203,74 @@ export default function QuestionPage() {
|
|||||||
upvoteCount={question.numVotes}
|
upvoteCount={question.numVotes}
|
||||||
onReceivedSubmit={(data) => {
|
onReceivedSubmit={(data) => {
|
||||||
addEncounter({
|
addEncounter({
|
||||||
|
cityId: data.cityId,
|
||||||
companyId: data.company,
|
companyId: data.company,
|
||||||
location: data.location,
|
countryId: data.countryId,
|
||||||
questionId: questionId as string,
|
questionId: questionId as string,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
seenAt: data.seenAt,
|
seenAt: data.seenAt,
|
||||||
|
stateId: data.stateId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mx-2">
|
<div className="mx-2">
|
||||||
<Collapsible label={`${(comments ?? []).length} comment(s)`}>
|
<Collapsible label={`${question.numComments} comment(s)`}>
|
||||||
<form
|
<div className="mt-4 px-4">
|
||||||
className="mb-2"
|
<form
|
||||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
className="mb-2"
|
||||||
<TextArea
|
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||||
{...commentRegister('commentContent', {
|
<TextArea
|
||||||
minLength: 1,
|
{...commentRegister('commentContent', {
|
||||||
required: true,
|
minLength: 1,
|
||||||
})}
|
required: true,
|
||||||
label="Post a comment"
|
})}
|
||||||
required={true}
|
label="Post a comment"
|
||||||
resize="vertical"
|
required={true}
|
||||||
rows={2}
|
resize="vertical"
|
||||||
/>
|
rows={2}
|
||||||
<div className="my-3 flex justify-between">
|
/>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="my-3 flex justify-between">
|
||||||
<span aria-hidden={true} className="text-sm">
|
<Button
|
||||||
Sort by:
|
disabled={!isCommentDirty || !isCommentValid}
|
||||||
</span>
|
label="Post"
|
||||||
<Select
|
type="submit"
|
||||||
display="inline"
|
variant="primary"
|
||||||
isLabelHidden={true}
|
|
||||||
label="Sort by"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: 'Most recent',
|
|
||||||
value: 'most-recent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Most upvotes',
|
|
||||||
value: 'most-upvotes',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value="most-recent"
|
|
||||||
onChange={(value) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<Button
|
{/* TODO: Add button to load more */}
|
||||||
disabled={!isCommentDirty || !isCommentValid}
|
<div className="flex flex-col gap-2">
|
||||||
label="Post"
|
<div className="flex items-center justify-between gap-2">
|
||||||
type="submit"
|
<p className="text-lg">Comments</p>
|
||||||
variant="primary"
|
<div className="flex items-end gap-2">
|
||||||
/>
|
<SortOptionsSelect
|
||||||
|
sortOrderValue={commentSortOrder}
|
||||||
|
sortTypeValue={commentSortType}
|
||||||
|
onSortOrderChange={setCommentSortOrder}
|
||||||
|
onSortTypeChange={setCommentSortType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(commentData?.pages ?? []).flatMap(
|
||||||
|
({ processedQuestionCommentsData: comments }) =>
|
||||||
|
comments.map((comment) => (
|
||||||
|
<AnswerCommentListItem
|
||||||
|
key={comment.id}
|
||||||
|
answerCommentId={comment.id}
|
||||||
|
authorImageUrl={comment.userImage}
|
||||||
|
authorName={comment.user}
|
||||||
|
content={comment.content}
|
||||||
|
createdAt={comment.createdAt}
|
||||||
|
upvoteCount={comment.numVotes}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
<PaginationLoadMoreButton query={commentInfiniteQuery} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
{(comments ?? []).map((comment) => (
|
|
||||||
<AnswerCommentListItem
|
|
||||||
key={comment.id}
|
|
||||||
answerCommentId={comment.id}
|
|
||||||
authorImageUrl={comment.userImage}
|
|
||||||
authorName={comment.user}
|
|
||||||
content={comment.content}
|
|
||||||
createdAt={comment.createdAt}
|
|
||||||
upvoteCount={comment.numVotes}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
<HorizontalDivider />
|
||||||
<form onSubmit={handleSubmit(handleSubmitAnswer)}>
|
<form onSubmit={handleSubmit(handleSubmitAnswer)}>
|
||||||
<TextArea
|
<TextArea
|
||||||
{...answerRegister('answerContent', {
|
{...answerRegister('answerContent', {
|
||||||
@ -244,34 +283,6 @@ export default function QuestionPage() {
|
|||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 mb-1 flex justify-between">
|
<div className="mt-3 mb-1 flex justify-between">
|
||||||
<div className="flex items-baseline justify-start gap-2">
|
|
||||||
<p>{(answers ?? []).length} answers</p>
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span aria-hidden={true} className="text-sm">
|
|
||||||
Sort by:
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
display="inline"
|
|
||||||
isLabelHidden={true}
|
|
||||||
label="Sort by"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: 'Most recent',
|
|
||||||
value: 'most-recent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Most upvotes',
|
|
||||||
value: 'most-upvotes',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value="most-recent"
|
|
||||||
onChange={(value) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!isDirty || !isValid}
|
disabled={!isDirty || !isValid}
|
||||||
label="Contribute"
|
label="Contribute"
|
||||||
@ -280,21 +291,37 @@ export default function QuestionPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{(answers ?? []).map((answer) => (
|
<div className="flex items-center justify-between gap-2">
|
||||||
<QuestionAnswerCard
|
<p className="text-xl">{question.numAnswers} answers</p>
|
||||||
key={answer.id}
|
<div className="flex items-end gap-2">
|
||||||
answerId={answer.id}
|
<SortOptionsSelect
|
||||||
authorImageUrl={answer.userImage}
|
sortOrderValue={answerSortOrder}
|
||||||
authorName={answer.user}
|
sortTypeValue={answerSortType}
|
||||||
commentCount={answer.numComments}
|
onSortOrderChange={setAnswerSortOrder}
|
||||||
content={answer.content}
|
onSortTypeChange={setAnswerSortType}
|
||||||
createdAt={answer.createdAt}
|
/>
|
||||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
</div>
|
||||||
answer.content,
|
</div>
|
||||||
)}`}
|
{/* TODO: Add button to load more */}
|
||||||
upvoteCount={answer.numVotes}
|
{(answerData?.pages ?? []).flatMap(
|
||||||
/>
|
({ processedAnswersData: answers }) =>
|
||||||
))}
|
answers.map((answer) => (
|
||||||
|
<QuestionAnswerCard
|
||||||
|
key={answer.id}
|
||||||
|
answerId={answer.id}
|
||||||
|
authorImageUrl={answer.userImage}
|
||||||
|
authorName={answer.user}
|
||||||
|
commentCount={answer.numComments}
|
||||||
|
content={answer.content}
|
||||||
|
createdAt={answer.createdAt}
|
||||||
|
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||||
|
answer.content,
|
||||||
|
)}`}
|
||||||
|
upvoteCount={answer.numVotes}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,11 +5,13 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { Bars3BottomLeftIcon } from '@heroicons/react/20/solid';
|
import { Bars3BottomLeftIcon } from '@heroicons/react/20/solid';
|
||||||
import { NoSymbolIcon } from '@heroicons/react/24/outline';
|
import { NoSymbolIcon } from '@heroicons/react/24/outline';
|
||||||
import type { QuestionsQuestionType } from '@prisma/client';
|
import type { QuestionsQuestionType } from '@prisma/client';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
import { Button, SlideOut } from '@tih/ui';
|
import { Button, SlideOut } from '@tih/ui';
|
||||||
|
|
||||||
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
|
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
|
||||||
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
|
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
|
||||||
import FilterSection from '~/components/questions/filter/FilterSection';
|
import FilterSection from '~/components/questions/filter/FilterSection';
|
||||||
|
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||||
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
|
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
|
||||||
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
|
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
|
||||||
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
|
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
|
||||||
@ -17,8 +19,6 @@ import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
|
|||||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
import type { QuestionAge } from '~/utils/questions/constants';
|
import type { QuestionAge } from '~/utils/questions/constants';
|
||||||
import { SORT_TYPES } from '~/utils/questions/constants';
|
|
||||||
import { SORT_ORDERS } from '~/utils/questions/constants';
|
|
||||||
import { APP_TITLE } from '~/utils/questions/constants';
|
import { APP_TITLE } from '~/utils/questions/constants';
|
||||||
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
|
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
|
||||||
import createSlug from '~/utils/questions/createSlug';
|
import createSlug from '~/utils/questions/createSlug';
|
||||||
@ -29,14 +29,29 @@ import {
|
|||||||
} from '~/utils/questions/useSearchParam';
|
} from '~/utils/questions/useSearchParam';
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions.d';
|
||||||
import { SortType } from '~/types/questions.d';
|
import { SortType } from '~/types/questions.d';
|
||||||
import { SortOrder } from '~/types/questions.d';
|
import { SortOrder } from '~/types/questions.d';
|
||||||
|
|
||||||
|
function locationToSlug(value: Location & TypeaheadOption): string {
|
||||||
|
return [
|
||||||
|
value.countryId,
|
||||||
|
value.stateId,
|
||||||
|
value.cityId,
|
||||||
|
value.id,
|
||||||
|
value.label,
|
||||||
|
value.value,
|
||||||
|
].join('-');
|
||||||
|
}
|
||||||
|
|
||||||
export default function QuestionsBrowsePage() {
|
export default function QuestionsBrowsePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
|
const [
|
||||||
useSearchParam('companies');
|
selectedCompanySlugs,
|
||||||
|
setSelectedCompanySlugs,
|
||||||
|
areCompaniesInitialized,
|
||||||
|
] = useSearchParam('companies');
|
||||||
const [
|
const [
|
||||||
selectedQuestionTypes,
|
selectedQuestionTypes,
|
||||||
setSelectedQuestionTypes,
|
setSelectedQuestionTypes,
|
||||||
@ -70,7 +85,13 @@ export default function QuestionsBrowsePage() {
|
|||||||
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
|
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
|
||||||
useSearchParam('roles');
|
useSearchParam('roles');
|
||||||
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
|
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
|
||||||
useSearchParam('locations');
|
useSearchParam<Location & TypeaheadOption>('locations', {
|
||||||
|
paramToString: locationToSlug,
|
||||||
|
stringToParam: (param) => {
|
||||||
|
const [countryId, stateId, cityId, id, label, value] = param.split('-');
|
||||||
|
return { cityId, countryId, id, label, stateId, value };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [sortOrder, setSortOrder, isSortOrderInitialized] =
|
const [sortOrder, setSortOrder, isSortOrderInitialized] =
|
||||||
useSearchParamSingle<SortOrder>('sortOrder', {
|
useSearchParamSingle<SortOrder>('sortOrder', {
|
||||||
@ -122,13 +143,13 @@ export default function QuestionsBrowsePage() {
|
|||||||
|
|
||||||
const hasFilters = useMemo(
|
const hasFilters = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedCompanies.length > 0 ||
|
selectedCompanySlugs.length > 0 ||
|
||||||
selectedQuestionTypes.length > 0 ||
|
selectedQuestionTypes.length > 0 ||
|
||||||
selectedQuestionAge !== 'all' ||
|
selectedQuestionAge !== 'all' ||
|
||||||
selectedRoles.length > 0 ||
|
selectedRoles.length > 0 ||
|
||||||
selectedLocations.length > 0,
|
selectedLocations.length > 0,
|
||||||
[
|
[
|
||||||
selectedCompanies,
|
selectedCompanySlugs,
|
||||||
selectedQuestionTypes,
|
selectedQuestionTypes,
|
||||||
selectedQuestionAge,
|
selectedQuestionAge,
|
||||||
selectedRoles,
|
selectedRoles,
|
||||||
@ -147,24 +168,24 @@ export default function QuestionsBrowsePage() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}, [selectedQuestionAge]);
|
}, [selectedQuestionAge]);
|
||||||
|
|
||||||
const {
|
const questionsInfiniteQuery = trpc.useInfiniteQuery(
|
||||||
data: questionsQueryData,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
} = trpc.useInfiniteQuery(
|
|
||||||
[
|
[
|
||||||
'questions.questions.getQuestionsByFilter',
|
'questions.questions.getQuestionsByFilter',
|
||||||
{
|
{
|
||||||
companyNames: selectedCompanies,
|
// TODO: Enable filtering by countryIds and stateIds
|
||||||
|
cityIds: selectedLocations
|
||||||
|
.map(({ cityId }) => cityId)
|
||||||
|
.filter((id) => id !== undefined) as Array<string>,
|
||||||
|
companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]),
|
||||||
|
countryIds: [],
|
||||||
endDate: today,
|
endDate: today,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
locations: selectedLocations,
|
|
||||||
questionTypes: selectedQuestionTypes,
|
questionTypes: selectedQuestionTypes,
|
||||||
roles: selectedRoles,
|
roles: selectedRoles,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
sortType,
|
sortType,
|
||||||
startDate,
|
startDate,
|
||||||
|
stateIds: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
@ -173,6 +194,8 @@ export default function QuestionsBrowsePage() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: questionsQueryData } = questionsInfiniteQuery;
|
||||||
|
|
||||||
const questionCount = useMemo(() => {
|
const questionCount = useMemo(() => {
|
||||||
if (!questionsQueryData) {
|
if (!questionsQueryData) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -239,8 +262,8 @@ export default function QuestionsBrowsePage() {
|
|||||||
Router.replace({
|
Router.replace({
|
||||||
pathname,
|
pathname,
|
||||||
query: {
|
query: {
|
||||||
companies: selectedCompanies,
|
companies: selectedCompanySlugs,
|
||||||
locations: selectedLocations,
|
locations: selectedLocations.map(locationToSlug),
|
||||||
questionAge: selectedQuestionAge,
|
questionAge: selectedQuestionAge,
|
||||||
questionTypes: selectedQuestionTypes,
|
questionTypes: selectedQuestionTypes,
|
||||||
roles: selectedRoles,
|
roles: selectedRoles,
|
||||||
@ -255,7 +278,7 @@ export default function QuestionsBrowsePage() {
|
|||||||
areSearchOptionsInitialized,
|
areSearchOptionsInitialized,
|
||||||
loaded,
|
loaded,
|
||||||
pathname,
|
pathname,
|
||||||
selectedCompanies,
|
selectedCompanySlugs,
|
||||||
selectedRoles,
|
selectedRoles,
|
||||||
selectedLocations,
|
selectedLocations,
|
||||||
selectedQuestionAge,
|
selectedQuestionAge,
|
||||||
@ -265,13 +288,16 @@ export default function QuestionsBrowsePage() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const selectedCompanyOptions = useMemo(() => {
|
const selectedCompanyOptions = useMemo(() => {
|
||||||
return selectedCompanies.map((company) => ({
|
return selectedCompanySlugs.map((company) => {
|
||||||
checked: true,
|
const [id, label] = company.split('_');
|
||||||
id: company,
|
return {
|
||||||
label: company,
|
checked: true,
|
||||||
value: company,
|
id,
|
||||||
}));
|
label,
|
||||||
}, [selectedCompanies]);
|
value: id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [selectedCompanySlugs]);
|
||||||
|
|
||||||
const selectedRoleOptions = useMemo(() => {
|
const selectedRoleOptions = useMemo(() => {
|
||||||
return selectedRoles.map((role) => ({
|
return selectedRoles.map((role) => ({
|
||||||
@ -285,9 +311,7 @@ export default function QuestionsBrowsePage() {
|
|||||||
const selectedLocationOptions = useMemo(() => {
|
const selectedLocationOptions = useMemo(() => {
|
||||||
return selectedLocations.map((location) => ({
|
return selectedLocations.map((location) => ({
|
||||||
checked: true,
|
checked: true,
|
||||||
id: location,
|
...location,
|
||||||
label: location,
|
|
||||||
value: location,
|
|
||||||
}));
|
}));
|
||||||
}, [selectedLocations]);
|
}, [selectedLocations]);
|
||||||
|
|
||||||
@ -305,7 +329,7 @@ export default function QuestionsBrowsePage() {
|
|||||||
label="Clear filters"
|
label="Clear filters"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedCompanies([]);
|
setSelectedCompanySlugs([]);
|
||||||
setSelectedQuestionTypes([]);
|
setSelectedQuestionTypes([]);
|
||||||
setSelectedQuestionAge('all');
|
setSelectedQuestionAge('all');
|
||||||
setSelectedRoles([]);
|
setSelectedRoles([]);
|
||||||
@ -320,8 +344,8 @@ export default function QuestionsBrowsePage() {
|
|||||||
{...field}
|
{...field}
|
||||||
clearOnSelect={true}
|
clearOnSelect={true}
|
||||||
filterOption={(option) => {
|
filterOption={(option) => {
|
||||||
return !selectedCompanies.some((company) => {
|
return !selectedCompanySlugs.some((companySlug) => {
|
||||||
return company === option.value;
|
return companySlug === `${option.id}_${option.label}`;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
@ -337,10 +361,15 @@ export default function QuestionsBrowsePage() {
|
|||||||
)}
|
)}
|
||||||
onOptionChange={(option) => {
|
onOptionChange={(option) => {
|
||||||
if (option.checked) {
|
if (option.checked) {
|
||||||
setSelectedCompanies([...selectedCompanies, option.label]);
|
setSelectedCompanySlugs([
|
||||||
|
...selectedCompanySlugs,
|
||||||
|
`${option.id}_${option.label}`,
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedCompanies(
|
setSelectedCompanySlugs(
|
||||||
selectedCompanies.filter((company) => company !== option.label),
|
selectedCompanySlugs.filter(
|
||||||
|
(companySlug) => companySlug !== `${option.id}_${option.label}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -348,7 +377,10 @@ export default function QuestionsBrowsePage() {
|
|||||||
<FilterSection
|
<FilterSection
|
||||||
label="Roles"
|
label="Roles"
|
||||||
options={selectedRoleOptions}
|
options={selectedRoleOptions}
|
||||||
renderInput={({ onOptionChange, field: { ref: _, ...field } }) => (
|
renderInput={({
|
||||||
|
onOptionChange,
|
||||||
|
field: { ref: _, onChange: __, ...field },
|
||||||
|
}) => (
|
||||||
<RoleTypeahead
|
<RoleTypeahead
|
||||||
{...field}
|
{...field}
|
||||||
clearOnSelect={true}
|
clearOnSelect={true}
|
||||||
@ -406,13 +438,16 @@ export default function QuestionsBrowsePage() {
|
|||||||
<FilterSection
|
<FilterSection
|
||||||
label="Locations"
|
label="Locations"
|
||||||
options={selectedLocationOptions}
|
options={selectedLocationOptions}
|
||||||
renderInput={({ onOptionChange, field: { ref: _, ...field } }) => (
|
renderInput={({
|
||||||
|
onOptionChange,
|
||||||
|
field: { ref: _, onChange: __, ...field },
|
||||||
|
}) => (
|
||||||
<LocationTypeahead
|
<LocationTypeahead
|
||||||
{...field}
|
{...field}
|
||||||
clearOnSelect={true}
|
clearOnSelect={true}
|
||||||
filterOption={(option) => {
|
filterOption={(option) => {
|
||||||
return !selectedLocations.some((location) => {
|
return !selectedLocations.some((location) => {
|
||||||
return location === option.value;
|
return location.id === option.id;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
@ -428,10 +463,14 @@ export default function QuestionsBrowsePage() {
|
|||||||
)}
|
)}
|
||||||
onOptionChange={(option) => {
|
onOptionChange={(option) => {
|
||||||
if (option.checked) {
|
if (option.checked) {
|
||||||
setSelectedLocations([...selectedLocations, option.value]);
|
// TODO: Fix type inference, then remove the `as` cast.
|
||||||
|
setSelectedLocations([
|
||||||
|
...selectedLocations,
|
||||||
|
option as unknown as Location & TypeaheadOption,
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedLocations(
|
setSelectedLocations(
|
||||||
selectedLocations.filter((role) => role !== option.value),
|
selectedLocations.filter((location) => location.id !== option.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -450,21 +489,22 @@ export default function QuestionsBrowsePage() {
|
|||||||
<div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
|
<div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
|
||||||
<ContributeQuestionCard
|
<ContributeQuestionCard
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
|
const { cityId, countryId, stateId } = data.location;
|
||||||
createQuestion({
|
createQuestion({
|
||||||
|
cityId,
|
||||||
companyId: data.company,
|
companyId: data.company,
|
||||||
content: data.questionContent,
|
content: data.questionContent,
|
||||||
location: data.location,
|
countryId,
|
||||||
questionType: data.questionType,
|
questionType: data.questionType,
|
||||||
role: data.role,
|
role: data.role.value,
|
||||||
seenAt: data.date,
|
seenAt: data.date,
|
||||||
|
stateId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
|
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
|
||||||
<QuestionSearchBar
|
<QuestionSearchBar
|
||||||
sortOrderOptions={SORT_ORDERS}
|
|
||||||
sortOrderValue={sortOrder}
|
sortOrderValue={sortOrder}
|
||||||
sortTypeOptions={SORT_TYPES}
|
|
||||||
sortTypeValue={sortType}
|
sortTypeValue={sortType}
|
||||||
onFilterOptionsToggle={() => {
|
onFilterOptionsToggle={() => {
|
||||||
setFilterDrawerOpen(!filterDrawerOpen);
|
setFilterDrawerOpen(!filterDrawerOpen);
|
||||||
@ -477,7 +517,7 @@ export default function QuestionsBrowsePage() {
|
|||||||
{(questionsQueryData?.pages ?? []).flatMap(
|
{(questionsQueryData?.pages ?? []).flatMap(
|
||||||
({ data: questions }) =>
|
({ data: questions }) =>
|
||||||
questions.map((question) => {
|
questions.map((question) => {
|
||||||
const { companyCounts, locationCounts, roleCounts } =
|
const { companyCounts, countryCounts, roleCounts } =
|
||||||
relabelQuestionAggregates(
|
relabelQuestionAggregates(
|
||||||
question.aggregatedQuestionEncounters,
|
question.aggregatedQuestionEncounters,
|
||||||
);
|
);
|
||||||
@ -488,10 +528,10 @@ export default function QuestionsBrowsePage() {
|
|||||||
answerCount={question.numAnswers}
|
answerCount={question.numAnswers}
|
||||||
companies={companyCounts}
|
companies={companyCounts}
|
||||||
content={question.content}
|
content={question.content}
|
||||||
|
countries={countryCounts}
|
||||||
href={`/questions/${question.id}/${createSlug(
|
href={`/questions/${question.id}/${createSlug(
|
||||||
question.content,
|
question.content,
|
||||||
)}`}
|
)}`}
|
||||||
locations={locationCounts}
|
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
receivedCount={question.receivedCount}
|
receivedCount={question.receivedCount}
|
||||||
roles={roleCounts}
|
roles={roleCounts}
|
||||||
@ -508,15 +548,7 @@ export default function QuestionsBrowsePage() {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
<Button
|
<PaginationLoadMoreButton query={questionsInfiniteQuery} />
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
isLoading={isFetchingNextPage}
|
|
||||||
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={() => {
|
|
||||||
fetchNextPage();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{questionCount === 0 && (
|
{questionCount === 0 && (
|
||||||
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">
|
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">
|
||||||
<NoSymbolIcon className="h-6 w-6" />
|
<NoSymbolIcon className="h-6 w-6" />
|
||||||
|
@ -174,7 +174,7 @@ export default function ListPage() {
|
|||||||
<div className="flex flex-col gap-4 pb-4">
|
<div className="flex flex-col gap-4 pb-4">
|
||||||
{lists[selectedListIndex].questionEntries.map(
|
{lists[selectedListIndex].questionEntries.map(
|
||||||
({ question, id: entryId }) => {
|
({ question, id: entryId }) => {
|
||||||
const { companyCounts, locationCounts, roleCounts } =
|
const { companyCounts, countryCounts, roleCounts } =
|
||||||
relabelQuestionAggregates(
|
relabelQuestionAggregates(
|
||||||
question.aggregatedQuestionEncounters,
|
question.aggregatedQuestionEncounters,
|
||||||
);
|
);
|
||||||
@ -184,10 +184,10 @@ export default function ListPage() {
|
|||||||
key={question.id}
|
key={question.id}
|
||||||
companies={companyCounts}
|
companies={companyCounts}
|
||||||
content={question.content}
|
content={question.content}
|
||||||
|
countries={countryCounts}
|
||||||
href={`/questions/${question.id}/${createSlug(
|
href={`/questions/${question.id}/${createSlug(
|
||||||
question.content,
|
question.content,
|
||||||
)}`}
|
)}`}
|
||||||
locations={locationCounts}
|
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
receivedCount={question.receivedCount}
|
receivedCount={question.receivedCount}
|
||||||
roles={roleCounts}
|
roles={roleCounts}
|
||||||
|
@ -19,9 +19,11 @@ export const locationsRouter = createRouter()
|
|||||||
select: {
|
select: {
|
||||||
country: {
|
country: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -4,16 +4,43 @@ import { Vote } from '@prisma/client';
|
|||||||
import { createRouter } from '../context';
|
import { createRouter } from '../context';
|
||||||
|
|
||||||
import type { AnswerComment } from '~/types/questions';
|
import type { AnswerComment } from '~/types/questions';
|
||||||
|
import { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
export const questionsAnswerCommentRouter = createRouter().query(
|
export const questionsAnswerCommentRouter = createRouter().query(
|
||||||
'getAnswerComments',
|
'getAnswerComments',
|
||||||
{
|
{
|
||||||
input: z.object({
|
input: z.object({
|
||||||
answerId: z.string(),
|
answerId: z.string(),
|
||||||
|
cursor: z.string().nullish(),
|
||||||
|
limit: z.number().min(1).default(50),
|
||||||
|
sortOrder: z.nativeEnum(SortOrder),
|
||||||
|
sortType: z.nativeEnum(SortType),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
|
const { answerId, cursor } = input;
|
||||||
|
|
||||||
|
const sortCondition =
|
||||||
|
input.sortType === SortType.TOP
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
upvotes: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
updatedAt: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const questionAnswerCommentsData =
|
const questionAnswerCommentsData =
|
||||||
await ctx.prisma.questionsAnswerComment.findMany({
|
await ctx.prisma.questionsAnswerComment.findMany({
|
||||||
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
@ -23,14 +50,13 @@ export const questionsAnswerCommentRouter = createRouter().query(
|
|||||||
},
|
},
|
||||||
votes: true,
|
votes: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: sortCondition,
|
||||||
createdAt: 'desc',
|
take: input.limit + 1,
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
answerId: input.answerId,
|
answerId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return questionAnswerCommentsData.map((data) => {
|
const processedQuestionAnswerCommentsData = questionAnswerCommentsData.map((data) => {
|
||||||
const votes: number = data.votes.reduce(
|
const votes: number = data.votes.reduce(
|
||||||
(previousValue: number, currentValue) => {
|
(previousValue: number, currentValue) => {
|
||||||
let result: number = previousValue;
|
let result: number = previousValue;
|
||||||
@ -59,6 +85,22 @@ export const questionsAnswerCommentRouter = createRouter().query(
|
|||||||
};
|
};
|
||||||
return answerComment;
|
return answerComment;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nextCursor: typeof cursor | undefined = undefined;
|
||||||
|
|
||||||
|
if (questionAnswerCommentsData.length > input.limit) {
|
||||||
|
const nextItem = questionAnswerCommentsData.pop()!;
|
||||||
|
processedQuestionAnswerCommentsData.pop();
|
||||||
|
|
||||||
|
const nextIdCursor: string | undefined = nextItem.id;
|
||||||
|
|
||||||
|
nextCursor = nextIdCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextCursor,
|
||||||
|
processedQuestionAnswerCommentsData,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -39,7 +39,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (answerCommentToUpdate?.id !== userId) {
|
if (answerCommentToUpdate?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -71,7 +71,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (answerCommentToDelete?.id !== userId) {
|
if (answerCommentToDelete?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -100,27 +100,237 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mutation('createVote', {
|
.mutation('setUpVote', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
answerCommentId: z.string(),
|
answerCommentId: z.string(),
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerCommentId } = input;
|
||||||
|
|
||||||
const { answerCommentId, vote } = input;
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerCommentToUpdate =
|
||||||
|
await tx.questionsAnswerComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
if (answerCommentToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer Comment do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [answerCommentVote] = await ctx.prisma.$transaction([
|
const vote = await tx.questionsAnswerCommentVote.findUnique({
|
||||||
ctx.prisma.questionsAnswerCommentVote.create({
|
where: {
|
||||||
data: {
|
answerCommentId_userId: { answerCommentId, userId },
|
||||||
answerCommentId,
|
|
||||||
userId,
|
|
||||||
vote,
|
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
ctx.prisma.questionsAnswerComment.update({
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsAnswerCommentVote.create({
|
||||||
|
data: {
|
||||||
|
answerCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswerComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.UPVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.DOWNVOTE) {
|
||||||
|
const updatedVote = await tx.questionsAnswerCommentVote.update({
|
||||||
|
data: {
|
||||||
|
answerCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswerComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setDownVote', {
|
||||||
|
input: z.object({
|
||||||
|
answerCommentId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerCommentId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerCommentToUpdate =
|
||||||
|
await tx.questionsAnswerComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answerCommentToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer Comment do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsAnswerCommentVote.findUnique({
|
||||||
|
where: {
|
||||||
|
answerCommentId_userId: { answerCommentId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsAnswerCommentVote.create({
|
||||||
|
data: {
|
||||||
|
answerCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswerComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.DOWNVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.UPVOTE) {
|
||||||
|
const updatedVote = await tx.questionsAnswerCommentVote.update({
|
||||||
|
data: {
|
||||||
|
answerCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswerComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setNoVote', {
|
||||||
|
input: z.object({
|
||||||
|
answerCommentId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerCommentId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerCommentToUpdate =
|
||||||
|
await tx.questionsAnswerComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answerCommentToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer Comment do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteToDelete = await tx.questionsAnswerCommentVote.findUnique({
|
||||||
|
where: {
|
||||||
|
answerCommentId_userId: { answerCommentId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (voteToDelete === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voteToDelete!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
|
||||||
|
|
||||||
|
await tx.questionsAnswerCommentVote.delete({
|
||||||
|
where: {
|
||||||
|
id: voteToDelete.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswerComment.update({
|
||||||
data: {
|
data: {
|
||||||
upvotes: {
|
upvotes: {
|
||||||
increment: incrementValue,
|
increment: incrementValue,
|
||||||
@ -129,101 +339,9 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
|||||||
where: {
|
where: {
|
||||||
id: answerCommentId,
|
id: answerCommentId,
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return answerCommentVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('updateVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { id, vote } = input;
|
|
||||||
|
|
||||||
const voteToUpdate =
|
|
||||||
await ctx.prisma.questionsAnswerCommentVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (voteToUpdate?.userId !== userId) {
|
return voteToDelete;
|
||||||
throw new TRPCError({
|
});
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
|
|
||||||
|
|
||||||
const [answerCommentVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsAnswerCommentVote.update({
|
|
||||||
data: {
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsAnswerComment.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToUpdate.answerCommentId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return answerCommentVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const voteToDelete =
|
|
||||||
await ctx.prisma.questionsAnswerCommentVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
|
|
||||||
|
|
||||||
const [answerCommentVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsAnswerCommentVote.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsAnswerComment.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToDelete.answerCommentId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return answerCommentVote;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -5,16 +5,42 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { createRouter } from '../context';
|
import { createRouter } from '../context';
|
||||||
|
|
||||||
import type { Answer } from '~/types/questions';
|
import type { Answer } from '~/types/questions';
|
||||||
|
import { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
export const questionsAnswerRouter = createRouter()
|
export const questionsAnswerRouter = createRouter()
|
||||||
.query('getAnswers', {
|
.query('getAnswers', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
|
cursor: z.string().nullish(),
|
||||||
|
limit: z.number().min(1).default(50),
|
||||||
questionId: z.string(),
|
questionId: z.string(),
|
||||||
|
sortOrder: z.nativeEnum(SortOrder),
|
||||||
|
sortType: z.nativeEnum(SortType),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const { questionId } = input;
|
const { questionId, cursor } = input;
|
||||||
|
|
||||||
|
const sortCondition =
|
||||||
|
input.sortType === SortType.TOP
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
upvotes: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
updatedAt: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
const answersData = await ctx.prisma.questionsAnswer.findMany({
|
const answersData = await ctx.prisma.questionsAnswer.findMany({
|
||||||
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
@ -29,14 +55,14 @@ export const questionsAnswerRouter = createRouter()
|
|||||||
},
|
},
|
||||||
votes: true,
|
votes: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: sortCondition,
|
||||||
createdAt: 'desc',
|
take: input.limit + 1,
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
questionId,
|
questionId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return answersData.map((data) => {
|
|
||||||
|
const processedAnswersData = answersData.map((data) => {
|
||||||
const votes: number = data.votes.reduce(
|
const votes: number = data.votes.reduce(
|
||||||
(previousValue: number, currentValue) => {
|
(previousValue: number, currentValue) => {
|
||||||
let result: number = previousValue;
|
let result: number = previousValue;
|
||||||
@ -65,6 +91,22 @@ export const questionsAnswerRouter = createRouter()
|
|||||||
};
|
};
|
||||||
return answer;
|
return answer;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nextCursor: typeof cursor | undefined = undefined;
|
||||||
|
|
||||||
|
if (answersData.length > input.limit) {
|
||||||
|
const nextItem = answersData.pop()!;
|
||||||
|
processedAnswersData.pop();
|
||||||
|
|
||||||
|
const nextIdCursor: string | undefined = nextItem.id;
|
||||||
|
|
||||||
|
nextCursor = nextIdCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextCursor,
|
||||||
|
processedAnswersData,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.query('getAnswerById', {
|
.query('getAnswerById', {
|
||||||
|
@ -39,7 +39,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (answerToUpdate?.id !== userId) {
|
if (answerToUpdate?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -69,7 +69,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (answerToDelete?.id !== userId) {
|
if (answerToDelete?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -98,27 +98,234 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mutation('createVote', {
|
.mutation('setUpVote', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
answerId: z.string(),
|
answerId: z.string(),
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerId } = input;
|
||||||
|
|
||||||
const { answerId, vote } = input;
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerToUpdate = await tx.questionsAnswer.findUnique({
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
where: {
|
||||||
|
id: answerId,
|
||||||
const [answerVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsAnswerVote.create({
|
|
||||||
data: {
|
|
||||||
answerId,
|
|
||||||
userId,
|
|
||||||
vote,
|
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
ctx.prisma.questionsAnswer.update({
|
|
||||||
|
if (answerToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsAnswerVote.findUnique({
|
||||||
|
where: {
|
||||||
|
answerId_userId: { answerId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsAnswerVote.create({
|
||||||
|
data: {
|
||||||
|
answerId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswer.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.UPVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.DOWNVOTE) {
|
||||||
|
const updatedVote = await tx.questionsAnswerVote.update({
|
||||||
|
data: {
|
||||||
|
answerId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswer.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setDownVote', {
|
||||||
|
input: z.object({
|
||||||
|
answerId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerToUpdate = await tx.questionsAnswer.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answerToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsAnswerVote.findUnique({
|
||||||
|
where: {
|
||||||
|
answerId_userId: { answerId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsAnswerVote.create({
|
||||||
|
data: {
|
||||||
|
answerId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswer.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.DOWNVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.UPVOTE) {
|
||||||
|
const updatedVote = await tx.questionsAnswerVote.update({
|
||||||
|
data: {
|
||||||
|
answerId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswer.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setNoVote', {
|
||||||
|
input: z.object({
|
||||||
|
answerId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { answerId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const answerToUpdate = await tx.questionsAnswer.findUnique({
|
||||||
|
where: {
|
||||||
|
id: answerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (answerToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Answer do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteToDelete = await tx.questionsAnswerVote.findUnique({
|
||||||
|
where: {
|
||||||
|
answerId_userId: { answerId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (voteToDelete === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voteToDelete!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
|
||||||
|
|
||||||
|
await tx.questionsAnswerVote.delete({
|
||||||
|
where: {
|
||||||
|
id: voteToDelete.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsAnswer.update({
|
||||||
data: {
|
data: {
|
||||||
upvotes: {
|
upvotes: {
|
||||||
increment: incrementValue,
|
increment: incrementValue,
|
||||||
@ -127,98 +334,9 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
|||||||
where: {
|
where: {
|
||||||
id: answerId,
|
id: answerId,
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return answerVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('updateVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { id, vote } = input;
|
|
||||||
|
|
||||||
const voteToUpdate = await ctx.prisma.questionsAnswerVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToUpdate?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
|
return voteToDelete;
|
||||||
|
|
||||||
const [questionsAnswerVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsAnswerVote.update({
|
|
||||||
data: {
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsAnswer.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToUpdate.answerId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return questionsAnswerVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const voteToDelete = await ctx.prisma.questionsAnswerVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (voteToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
|
|
||||||
|
|
||||||
const [questionsAnswerVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsAnswerVote.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsAnswer.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToDelete.answerId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionsAnswerVote;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -25,10 +25,12 @@ export const questionsListRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
encounters: {
|
encounters: {
|
||||||
select: {
|
select: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
location: true,
|
country: true,
|
||||||
role: true,
|
role: true,
|
||||||
seenAt: true,
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@ -83,10 +85,12 @@ export const questionsListRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
encounters: {
|
encounters: {
|
||||||
select: {
|
select: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
location: true,
|
country: true,
|
||||||
role: true,
|
role: true,
|
||||||
seenAt: true,
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
|
@ -4,17 +4,43 @@ import { Vote } from '@prisma/client';
|
|||||||
import { createRouter } from '../context';
|
import { createRouter } from '../context';
|
||||||
|
|
||||||
import type { QuestionComment } from '~/types/questions';
|
import type { QuestionComment } from '~/types/questions';
|
||||||
|
import { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
export const questionsQuestionCommentRouter = createRouter().query(
|
export const questionsQuestionCommentRouter = createRouter().query(
|
||||||
'getQuestionComments',
|
'getQuestionComments',
|
||||||
{
|
{
|
||||||
input: z.object({
|
input: z.object({
|
||||||
|
cursor: z.string().nullish(),
|
||||||
|
limit: z.number().min(1).default(50),
|
||||||
questionId: z.string(),
|
questionId: z.string(),
|
||||||
|
sortOrder: z.nativeEnum(SortOrder),
|
||||||
|
sortType: z.nativeEnum(SortType),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const { questionId } = input;
|
const { questionId, cursor } = input;
|
||||||
|
|
||||||
|
const sortCondition =
|
||||||
|
input.sortType === SortType.TOP
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
upvotes: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
updatedAt: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const questionCommentsData =
|
const questionCommentsData =
|
||||||
await ctx.prisma.questionsQuestionComment.findMany({
|
await ctx.prisma.questionsQuestionComment.findMany({
|
||||||
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
@ -24,14 +50,13 @@ export const questionsQuestionCommentRouter = createRouter().query(
|
|||||||
},
|
},
|
||||||
votes: true,
|
votes: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: sortCondition,
|
||||||
createdAt: 'desc',
|
take: input.limit + 1,
|
||||||
},
|
|
||||||
where: {
|
where: {
|
||||||
questionId,
|
questionId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return questionCommentsData.map((data) => {
|
const processedQuestionCommentsData = questionCommentsData.map((data) => {
|
||||||
const votes: number = data.votes.reduce(
|
const votes: number = data.votes.reduce(
|
||||||
(previousValue: number, currentValue) => {
|
(previousValue: number, currentValue) => {
|
||||||
let result: number = previousValue;
|
let result: number = previousValue;
|
||||||
@ -59,6 +84,22 @@ export const questionsQuestionCommentRouter = createRouter().query(
|
|||||||
};
|
};
|
||||||
return questionComment;
|
return questionComment;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nextCursor: typeof cursor | undefined = undefined;
|
||||||
|
|
||||||
|
if (questionCommentsData.length > input.limit) {
|
||||||
|
const nextItem = questionCommentsData.pop()!;
|
||||||
|
processedQuestionCommentsData.pop();
|
||||||
|
|
||||||
|
const nextIdCursor: string | undefined = nextItem.id;
|
||||||
|
|
||||||
|
nextCursor = nextIdCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextCursor,
|
||||||
|
processedQuestionCommentsData,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -41,7 +41,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionCommentToUpdate?.id !== userId) {
|
if (questionCommentToUpdate?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -72,7 +72,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionCommentToDelete?.id !== userId) {
|
if (questionCommentToDelete?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -101,26 +101,240 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mutation('createVote', {
|
.mutation('setUpVote', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
questionCommentId: z.string(),
|
questionCommentId: z.string(),
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
const { questionCommentId, vote } = input;
|
const { questionCommentId } = input;
|
||||||
|
|
||||||
const incrementValue: number = vote === Vote.UPVOTE ? 1 : -1;
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionCommentToUpdate =
|
||||||
|
await tx.questionsQuestionComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [questionCommentVote] = await ctx.prisma.$transaction([
|
if (questionCommentToUpdate === null) {
|
||||||
ctx.prisma.questionsQuestionCommentVote.create({
|
throw new TRPCError({
|
||||||
data: {
|
code: 'BAD_REQUEST',
|
||||||
questionCommentId,
|
message: 'Question Comment do not exist.',
|
||||||
userId,
|
});
|
||||||
vote,
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsQuestionCommentVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionCommentId_userId: { questionCommentId, userId },
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
ctx.prisma.questionsQuestionComment.update({
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsQuestionCommentVote.create({
|
||||||
|
data: {
|
||||||
|
questionCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestionComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.UPVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.DOWNVOTE) {
|
||||||
|
const updatedVote = await tx.questionsQuestionCommentVote.update({
|
||||||
|
data: {
|
||||||
|
questionCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestionComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setDownVote', {
|
||||||
|
input: z.object({
|
||||||
|
questionCommentId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { questionCommentId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionCommentToUpdate =
|
||||||
|
await tx.questionsQuestionComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questionCommentToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Question Comment do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsQuestionCommentVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionCommentId_userId: { questionCommentId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsQuestionCommentVote.create({
|
||||||
|
data: {
|
||||||
|
questionCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestionComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.DOWNVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.UPVOTE) {
|
||||||
|
tx.questionsQuestionCommentVote.delete({
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdVote = await tx.questionsQuestionCommentVote.create({
|
||||||
|
data: {
|
||||||
|
questionCommentId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestionComment.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setNoVote', {
|
||||||
|
input: z.object({
|
||||||
|
questionCommentId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { questionCommentId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionCommentToUpdate =
|
||||||
|
await tx.questionsQuestionComment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questionCommentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questionCommentToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Question Comment do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteToDelete = await tx.questionsQuestionCommentVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionCommentId_userId: { questionCommentId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (voteToDelete === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voteToDelete!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
|
||||||
|
|
||||||
|
await tx.questionsQuestionCommentVote.delete({
|
||||||
|
where: {
|
||||||
|
id: voteToDelete.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestionComment.update({
|
||||||
data: {
|
data: {
|
||||||
upvotes: {
|
upvotes: {
|
||||||
increment: incrementValue,
|
increment: incrementValue,
|
||||||
@ -129,100 +343,9 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
|||||||
where: {
|
where: {
|
||||||
id: questionCommentId,
|
id: questionCommentId,
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionCommentVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('updateVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { id, vote } = input;
|
|
||||||
|
|
||||||
const voteToUpdate =
|
|
||||||
await ctx.prisma.questionsQuestionCommentVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (voteToUpdate?.userId !== userId) {
|
return voteToDelete;
|
||||||
throw new TRPCError({
|
});
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
|
|
||||||
|
|
||||||
const [questionCommentVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionCommentVote.update({
|
|
||||||
data: {
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestionComment.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToUpdate.questionCommentId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return questionCommentVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const voteToDelete =
|
|
||||||
await ctx.prisma.questionsQuestionCommentVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
|
|
||||||
|
|
||||||
const [questionCommentVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionCommentVote.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestionComment.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToDelete.questionCommentId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionCommentVote;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createRouter } from '../context';
|
import { createAggregatedQuestionEncounter } from '~/utils/questions/server/aggregate-encounters';
|
||||||
|
|
||||||
import type { AggregatedQuestionEncounter } from '~/types/questions';
|
import { createRouter } from '../context';
|
||||||
|
|
||||||
export const questionsQuestionEncounterRouter = createRouter().query(
|
export const questionsQuestionEncounterRouter = createRouter().query(
|
||||||
'getAggregatedEncounters',
|
'getAggregatedEncounters',
|
||||||
@ -14,48 +14,17 @@ export const questionsQuestionEncounterRouter = createRouter().query(
|
|||||||
const questionEncountersData =
|
const questionEncountersData =
|
||||||
await ctx.prisma.questionsQuestionEncounter.findMany({
|
await ctx.prisma.questionsQuestionEncounter.findMany({
|
||||||
include: {
|
include: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
|
country: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
...input,
|
...input,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const companyCounts: Record<string, number> = {};
|
return createAggregatedQuestionEncounter(questionEncountersData);
|
||||||
const locationCounts: Record<string, number> = {};
|
|
||||||
const roleCounts: Record<string, number> = {};
|
|
||||||
|
|
||||||
let latestSeenAt = questionEncountersData[0].seenAt;
|
|
||||||
|
|
||||||
for (let i = 0; i < questionEncountersData.length; i++) {
|
|
||||||
const encounter = questionEncountersData[i];
|
|
||||||
|
|
||||||
latestSeenAt =
|
|
||||||
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
|
|
||||||
|
|
||||||
if (!(encounter.company!.name in companyCounts)) {
|
|
||||||
companyCounts[encounter.company!.name] = 1;
|
|
||||||
}
|
|
||||||
companyCounts[encounter.company!.name] += 1;
|
|
||||||
|
|
||||||
if (!(encounter.location in locationCounts)) {
|
|
||||||
locationCounts[encounter.location] = 1;
|
|
||||||
}
|
|
||||||
locationCounts[encounter.location] += 1;
|
|
||||||
|
|
||||||
if (!(encounter.role in roleCounts)) {
|
|
||||||
roleCounts[encounter.role] = 1;
|
|
||||||
}
|
|
||||||
roleCounts[encounter.role] += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionEncounter: AggregatedQuestionEncounter = {
|
|
||||||
companyCounts,
|
|
||||||
latestSeenAt,
|
|
||||||
locationCounts,
|
|
||||||
roleCounts,
|
|
||||||
};
|
|
||||||
return questionEncounter;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,38 +1,22 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { createAggregatedQuestionEncounter } from '~/utils/questions/server/aggregate-encounters';
|
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
import { createProtectedRouter } from '../context';
|
import { createProtectedRouter } from '../context';
|
||||||
|
|
||||||
import { SortOrder } from '~/types/questions.d';
|
import { SortOrder } from '~/types/questions.d';
|
||||||
|
|
||||||
export const questionsQuestionEncounterUserRouter = createProtectedRouter()
|
export const questionsQuestionEncounterUserRouter = createProtectedRouter()
|
||||||
.query('getAggregatedEncounters', {
|
|
||||||
input: z.object({
|
|
||||||
questionId: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const questionEncountersData =
|
|
||||||
await ctx.prisma.questionsQuestionEncounter.findMany({
|
|
||||||
include: {
|
|
||||||
company: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
...input,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createAggregatedQuestionEncounter(questionEncountersData);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('create', {
|
.mutation('create', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
|
cityId: z.string().nullish(),
|
||||||
companyId: z.string(),
|
companyId: z.string(),
|
||||||
location: z.string(),
|
countryId: z.string(),
|
||||||
questionId: z.string(),
|
questionId: z.string(),
|
||||||
role: z.string(),
|
role: z.nativeEnum(JobTitleLabels),
|
||||||
seenAt: z.date(),
|
seenAt: z.date(),
|
||||||
|
stateId: z.string().nullish(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
@ -94,7 +78,7 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionEncounterToUpdate?.id !== userId) {
|
if (questionEncounterToUpdate?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -157,7 +141,7 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionEncounterToDelete?.id !== userId) {
|
if (questionEncounterToDelete?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
|
@ -11,22 +11,18 @@ import { SortOrder, SortType } from '~/types/questions.d';
|
|||||||
export const questionsQuestionRouter = createRouter()
|
export const questionsQuestionRouter = createRouter()
|
||||||
.query('getQuestionsByFilter', {
|
.query('getQuestionsByFilter', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
companyNames: z.string().array(),
|
cityIds: z.string().array(),
|
||||||
cursor: z
|
companyIds: z.string().array(),
|
||||||
.object({
|
countryIds: z.string().array(),
|
||||||
idCursor: z.string().optional(),
|
cursor: z.string().nullish(),
|
||||||
lastSeenCursor: z.date().nullish().optional(),
|
|
||||||
upvoteCursor: z.number().optional(),
|
|
||||||
})
|
|
||||||
.nullish(),
|
|
||||||
endDate: z.date().default(new Date()),
|
endDate: z.date().default(new Date()),
|
||||||
limit: z.number().min(1).default(50),
|
limit: z.number().min(1).default(50),
|
||||||
locations: z.string().array(),
|
|
||||||
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
|
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
|
||||||
roles: z.string().array(),
|
roles: z.string().array(),
|
||||||
sortOrder: z.nativeEnum(SortOrder),
|
sortOrder: z.nativeEnum(SortOrder),
|
||||||
sortType: z.nativeEnum(SortType),
|
sortType: z.nativeEnum(SortType),
|
||||||
startDate: z.date().optional(),
|
startDate: z.date().optional(),
|
||||||
|
stateIds: z.string().array(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const { cursor } = input;
|
const { cursor } = input;
|
||||||
@ -51,12 +47,7 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
];
|
];
|
||||||
|
|
||||||
const questionsData = await ctx.prisma.questionsQuestion.findMany({
|
const questionsData = await ctx.prisma.questionsQuestion.findMany({
|
||||||
cursor:
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
cursor !== undefined
|
|
||||||
? {
|
|
||||||
id: cursor ? cursor!.idCursor : undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
@ -66,10 +57,12 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
},
|
},
|
||||||
encounters: {
|
encounters: {
|
||||||
select: {
|
select: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
location: true,
|
country: true,
|
||||||
role: true,
|
role: true,
|
||||||
seenAt: true,
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@ -95,19 +88,39 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
gte: input.startDate,
|
gte: input.startDate,
|
||||||
lte: input.endDate,
|
lte: input.endDate,
|
||||||
},
|
},
|
||||||
...(input.companyNames.length > 0
|
...(input.companyIds.length > 0
|
||||||
? {
|
? {
|
||||||
company: {
|
company: {
|
||||||
name: {
|
id: {
|
||||||
in: input.companyNames,
|
in: input.companyIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(input.locations.length > 0
|
...(input.cityIds.length > 0
|
||||||
? {
|
? {
|
||||||
location: {
|
city: {
|
||||||
in: input.locations,
|
id: {
|
||||||
|
in: input.cityIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.countryIds.length > 0
|
||||||
|
? {
|
||||||
|
country: {
|
||||||
|
id: {
|
||||||
|
in: input.countryIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.stateIds.length > 0
|
||||||
|
? {
|
||||||
|
state: {
|
||||||
|
id: {
|
||||||
|
in: input.stateIds,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@ -134,16 +147,8 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
processedQuestionsData.pop();
|
processedQuestionsData.pop();
|
||||||
|
|
||||||
const nextIdCursor: string | undefined = nextItem.id;
|
const nextIdCursor: string | undefined = nextItem.id;
|
||||||
const nextLastSeenCursor =
|
|
||||||
input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined;
|
|
||||||
const nextUpvoteCursor =
|
|
||||||
input.sortType === SortType.TOP ? nextItem.upvotes : undefined;
|
|
||||||
|
|
||||||
nextCursor = {
|
nextCursor = nextIdCursor;
|
||||||
idCursor: nextIdCursor,
|
|
||||||
lastSeenCursor: nextLastSeenCursor,
|
|
||||||
upvoteCursor: nextUpvoteCursor,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -167,10 +172,12 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
},
|
},
|
||||||
encounters: {
|
encounters: {
|
||||||
select: {
|
select: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
location: true,
|
country: true,
|
||||||
role: true,
|
role: true,
|
||||||
seenAt: true,
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@ -201,21 +208,23 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const escapeChars = /[()|&:*!]/g;
|
const escapeChars = /[()|&:*!]/g;
|
||||||
|
|
||||||
const query =
|
const query = input.content
|
||||||
input.content
|
.replace(escapeChars, ' ')
|
||||||
.replace(escapeChars, " ")
|
.trim()
|
||||||
.trim()
|
.split(/\s+/)
|
||||||
.split(/\s+/)
|
.join(' | ');
|
||||||
.join(" | ");
|
|
||||||
|
|
||||||
const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw`
|
const relatedQuestionsId: Array<{ id: string }> = await ctx.prisma
|
||||||
|
.$queryRaw`
|
||||||
SELECT id FROM "QuestionsQuestion"
|
SELECT id FROM "QuestionsQuestion"
|
||||||
WHERE
|
WHERE
|
||||||
to_tsvector("content") @@ to_tsquery('english', ${query})
|
to_tsvector("content") @@ to_tsquery('english', ${query})
|
||||||
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
|
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id);
|
const relatedQuestionsIdArray = relatedQuestionsId.map(
|
||||||
|
(current) => current.id,
|
||||||
|
);
|
||||||
|
|
||||||
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
|
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
|
||||||
include: {
|
include: {
|
||||||
@ -227,10 +236,12 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
},
|
},
|
||||||
encounters: {
|
encounters: {
|
||||||
select: {
|
select: {
|
||||||
|
city: true,
|
||||||
company: true,
|
company: true,
|
||||||
location: true,
|
country: true,
|
||||||
role: true,
|
role: true,
|
||||||
seenAt: true,
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@ -241,9 +252,9 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
votes: true,
|
votes: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id : {
|
id: {
|
||||||
in : relatedQuestionsIdArray,
|
in: relatedQuestionsIdArray,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -252,5 +263,5 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
);
|
);
|
||||||
|
|
||||||
return processedQuestionsData;
|
return processedQuestionsData;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
@ -7,12 +7,14 @@ import { createProtectedRouter } from '../context';
|
|||||||
export const questionsQuestionUserRouter = createProtectedRouter()
|
export const questionsQuestionUserRouter = createProtectedRouter()
|
||||||
.mutation('create', {
|
.mutation('create', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
|
cityId: z.string().nullish(),
|
||||||
companyId: z.string(),
|
companyId: z.string(),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
location: z.string(),
|
countryId: z.string(),
|
||||||
questionType: z.nativeEnum(QuestionsQuestionType),
|
questionType: z.nativeEnum(QuestionsQuestionType),
|
||||||
role: z.string(),
|
role: z.string(),
|
||||||
seenAt: z.date(),
|
seenAt: z.date(),
|
||||||
|
stateId: z.string().nullish(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
@ -22,14 +24,34 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
|||||||
content: input.content,
|
content: input.content,
|
||||||
encounters: {
|
encounters: {
|
||||||
create: {
|
create: {
|
||||||
|
city:
|
||||||
|
input.cityId !== null
|
||||||
|
? {
|
||||||
|
connect: {
|
||||||
|
id: input.cityId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
company: {
|
company: {
|
||||||
connect: {
|
connect: {
|
||||||
id: input.companyId,
|
id: input.companyId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
location: input.location,
|
country: {
|
||||||
|
connect: {
|
||||||
|
id: input.countryId,
|
||||||
|
},
|
||||||
|
},
|
||||||
role: input.role,
|
role: input.role,
|
||||||
seenAt: input.seenAt,
|
seenAt: input.seenAt,
|
||||||
|
state:
|
||||||
|
input.stateId !== null
|
||||||
|
? {
|
||||||
|
connect: {
|
||||||
|
id: input.stateId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
id: userId,
|
id: userId,
|
||||||
@ -59,7 +81,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionToUpdate?.id !== userId) {
|
if (questionToUpdate?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -93,7 +115,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (questionToDelete?.id !== userId) {
|
if (questionToDelete?.userId !== userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
message: 'User have no authorization to record.',
|
message: 'User have no authorization to record.',
|
||||||
@ -123,26 +145,234 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mutation('createVote', {
|
.mutation('setUpVote', {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
questionId: z.string(),
|
questionId: z.string(),
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const userId = ctx.session?.user?.id;
|
const userId = ctx.session?.user?.id;
|
||||||
const { questionId, vote } = input;
|
const { questionId } = input;
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionToUpdate = await tx.questionsQuestion.findUnique({
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
where: {
|
||||||
ctx.prisma.questionsQuestionVote.create({
|
id: questionId,
|
||||||
data: {
|
|
||||||
questionId,
|
|
||||||
userId,
|
|
||||||
vote,
|
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
|
if (questionToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Question do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsQuestionVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionId_userId: { questionId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsQuestionVote.create({
|
||||||
|
data: {
|
||||||
|
questionId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestion.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.vote === Vote.UPVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.DOWNVOTE) {
|
||||||
|
const updatedVote = await tx.questionsQuestionVote.update({
|
||||||
|
data: {
|
||||||
|
questionId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.UPVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestion.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setDownVote', {
|
||||||
|
input: z.object({
|
||||||
|
questionId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { questionId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionToUpdate = await tx.questionsQuestion.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questionToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Question do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await tx.questionsQuestionVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionId_userId: { questionId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vote === null) {
|
||||||
|
const createdVote = await tx.questionsQuestionVote.create({
|
||||||
|
data: {
|
||||||
|
questionId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestion.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.DOWNVOTE) {
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vote.vote === Vote.UPVOTE) {
|
||||||
|
const updatedVote = await tx.questionsQuestionVote.update({
|
||||||
|
data: {
|
||||||
|
questionId,
|
||||||
|
userId,
|
||||||
|
vote: Vote.DOWNVOTE,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: vote.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestion.update({
|
||||||
|
data: {
|
||||||
|
upvotes: {
|
||||||
|
increment: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedVote;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation('setNoVote', {
|
||||||
|
input: z.object({
|
||||||
|
questionId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const userId = ctx.session?.user?.id;
|
||||||
|
const { questionId } = input;
|
||||||
|
|
||||||
|
return await ctx.prisma.$transaction(async (tx) => {
|
||||||
|
const questionToUpdate = await tx.questionsQuestion.findUnique({
|
||||||
|
where: {
|
||||||
|
id: questionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (questionToUpdate === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Question do not exist.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteToDelete = await tx.questionsQuestionVote.findUnique({
|
||||||
|
where: {
|
||||||
|
questionId_userId: { questionId, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (voteToDelete === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voteToDelete!.userId !== userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User have no authorization to record.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
|
||||||
|
|
||||||
|
await tx.questionsQuestionVote.delete({
|
||||||
|
where: {
|
||||||
|
id: voteToDelete.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.questionsQuestion.update({
|
||||||
data: {
|
data: {
|
||||||
upvotes: {
|
upvotes: {
|
||||||
increment: incrementValue,
|
increment: incrementValue,
|
||||||
@ -151,98 +381,9 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
|||||||
where: {
|
where: {
|
||||||
id: questionId,
|
id: questionId,
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('updateVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { id, vote } = input;
|
|
||||||
|
|
||||||
const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToUpdate?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
|
return voteToDelete;
|
||||||
|
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionVote.update({
|
|
||||||
data: {
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToUpdate.questionId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return questionVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (voteToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
|
|
||||||
|
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionVote.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToDelete.questionId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionVote;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
32
apps/portal/src/types/questions.d.ts
vendored
32
apps/portal/src/types/questions.d.ts
vendored
@ -14,10 +14,40 @@ export type Question = {
|
|||||||
user: string;
|
user: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StateInfo = {
|
||||||
|
cityCounts: Record<string, number>;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CountryInfo = {
|
||||||
|
stateInfos: Record<string, StateInfo>;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CityLocation = {
|
||||||
|
cityId: string;
|
||||||
|
countryId: string;
|
||||||
|
stateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StateLocation = {
|
||||||
|
cityId?: never;
|
||||||
|
countryId: string;
|
||||||
|
stateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CountryLocation = {
|
||||||
|
cityId?: never;
|
||||||
|
countryId: string;
|
||||||
|
stateId?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Location = CityLocation | CountryLocation | StateLocation;
|
||||||
|
|
||||||
export type AggregatedQuestionEncounter = {
|
export type AggregatedQuestionEncounter = {
|
||||||
companyCounts: Record<string, number>;
|
companyCounts: Record<string, number>;
|
||||||
|
countryCounts: Record<string, CountryInfo>;
|
||||||
latestSeenAt: Date;
|
latestSeenAt: Date;
|
||||||
locationCounts: Record<string, number>;
|
|
||||||
roleCounts: Record<string, number>;
|
roleCounts: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,47 +63,6 @@ export const QUESTION_AGES: FilterChoices<QuestionAge> = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const LOCATIONS: FilterChoices = [
|
|
||||||
{
|
|
||||||
id: 'Singapore',
|
|
||||||
label: 'Singapore',
|
|
||||||
value: 'Singapore',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Menlo Park',
|
|
||||||
label: 'Menlo Park',
|
|
||||||
value: 'Menlo Park',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'California',
|
|
||||||
label: 'California',
|
|
||||||
value: 'California',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Hong Kong',
|
|
||||||
label: 'Hong Kong',
|
|
||||||
value: 'Hong Kong',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Taiwan',
|
|
||||||
label: 'Taiwan',
|
|
||||||
value: 'Taiwan',
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const ROLES: FilterChoices = [
|
|
||||||
{
|
|
||||||
id: 'Software Engineer',
|
|
||||||
label: 'Software Engineer',
|
|
||||||
value: 'Software Engineer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Software Engineer Intern',
|
|
||||||
label: 'Software Engineer Intern',
|
|
||||||
value: 'Software Engineer Intern',
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const SORT_ORDERS = [
|
export const SORT_ORDERS = [
|
||||||
{
|
{
|
||||||
label: 'Ascending',
|
label: 'Ascending',
|
||||||
|
@ -3,10 +3,8 @@ import { JobTitleLabels } from '~/components/shared/JobTitles';
|
|||||||
import type { AggregatedQuestionEncounter } from '~/types/questions';
|
import type { AggregatedQuestionEncounter } from '~/types/questions';
|
||||||
|
|
||||||
export default function relabelQuestionAggregates({
|
export default function relabelQuestionAggregates({
|
||||||
locationCounts,
|
|
||||||
companyCounts,
|
|
||||||
roleCounts,
|
roleCounts,
|
||||||
latestSeenAt,
|
...rest
|
||||||
}: AggregatedQuestionEncounter) {
|
}: AggregatedQuestionEncounter) {
|
||||||
const newRoleCounts = Object.fromEntries(
|
const newRoleCounts = Object.fromEntries(
|
||||||
Object.entries(roleCounts).map(([roleId, count]) => [
|
Object.entries(roleCounts).map(([roleId, count]) => [
|
||||||
@ -16,10 +14,8 @@ export default function relabelQuestionAggregates({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const relabeledAggregate: AggregatedQuestionEncounter = {
|
const relabeledAggregate: AggregatedQuestionEncounter = {
|
||||||
companyCounts,
|
|
||||||
latestSeenAt,
|
|
||||||
locationCounts,
|
|
||||||
roleCounts: newRoleCounts,
|
roleCounts: newRoleCounts,
|
||||||
|
...rest,
|
||||||
};
|
};
|
||||||
|
|
||||||
return relabeledAggregate;
|
return relabeledAggregate;
|
||||||
|
@ -1,17 +1,26 @@
|
|||||||
import type {
|
import type {
|
||||||
|
City,
|
||||||
Company,
|
Company,
|
||||||
|
Country,
|
||||||
QuestionsQuestion,
|
QuestionsQuestion,
|
||||||
QuestionsQuestionVote,
|
QuestionsQuestionVote,
|
||||||
|
State,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { Vote } from '@prisma/client';
|
import { Vote } from '@prisma/client';
|
||||||
|
|
||||||
import type { AggregatedQuestionEncounter, Question } from '~/types/questions';
|
import type {
|
||||||
|
AggregatedQuestionEncounter,
|
||||||
|
CountryInfo,
|
||||||
|
Question,
|
||||||
|
} from '~/types/questions';
|
||||||
|
|
||||||
type AggregatableEncounters = Array<{
|
type AggregatableEncounters = Array<{
|
||||||
|
city: City | null;
|
||||||
company: Company | null;
|
company: Company | null;
|
||||||
location: string;
|
country: Country | null;
|
||||||
role: string;
|
role: string;
|
||||||
seenAt: Date;
|
seenAt: Date;
|
||||||
|
state: State | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type QuestionWithAggregatableData = QuestionsQuestion & {
|
type QuestionWithAggregatableData = QuestionsQuestion & {
|
||||||
@ -67,8 +76,8 @@ export function createQuestionWithAggregateData(
|
|||||||
export function createAggregatedQuestionEncounter(
|
export function createAggregatedQuestionEncounter(
|
||||||
encounters: AggregatableEncounters,
|
encounters: AggregatableEncounters,
|
||||||
): AggregatedQuestionEncounter {
|
): AggregatedQuestionEncounter {
|
||||||
|
const countryCounts: Record<string, CountryInfo> = {};
|
||||||
const companyCounts: Record<string, number> = {};
|
const companyCounts: Record<string, number> = {};
|
||||||
const locationCounts: Record<string, number> = {};
|
|
||||||
const roleCounts: Record<string, number> = {};
|
const roleCounts: Record<string, number> = {};
|
||||||
|
|
||||||
let latestSeenAt = encounters[0].seenAt;
|
let latestSeenAt = encounters[0].seenAt;
|
||||||
@ -77,15 +86,47 @@ export function createAggregatedQuestionEncounter(
|
|||||||
latestSeenAt =
|
latestSeenAt =
|
||||||
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
|
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
|
||||||
|
|
||||||
if (!(encounter.company!.name in companyCounts)) {
|
if (encounter.company !== null) {
|
||||||
companyCounts[encounter.company!.name] = 0;
|
if (!(encounter.company.name in companyCounts)) {
|
||||||
|
companyCounts[encounter.company!.name] = 0;
|
||||||
|
}
|
||||||
|
companyCounts[encounter.company!.name] += 1;
|
||||||
}
|
}
|
||||||
companyCounts[encounter.company!.name] += 1;
|
|
||||||
|
|
||||||
if (!(encounter.location in locationCounts)) {
|
if (encounter.country !== null) {
|
||||||
locationCounts[encounter.location] = 0;
|
if (!(encounter.country.name in countryCounts)) {
|
||||||
|
countryCounts[encounter.country.name] = {
|
||||||
|
stateInfos: {},
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const countryInfo = countryCounts[encounter.country.name];
|
||||||
|
|
||||||
|
countryInfo.total += 1;
|
||||||
|
|
||||||
|
const countryStateInfo = countryInfo.stateInfos;
|
||||||
|
|
||||||
|
if (encounter.state !== null) {
|
||||||
|
if (!(encounter.state.name in countryStateInfo)) {
|
||||||
|
countryStateInfo[encounter.state.name] = {
|
||||||
|
cityCounts: {},
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const stateInfo = countryStateInfo[encounter.state.name];
|
||||||
|
|
||||||
|
stateInfo.total += 1;
|
||||||
|
|
||||||
|
const { cityCounts } = stateInfo;
|
||||||
|
|
||||||
|
if (encounter.city !== null) {
|
||||||
|
if (!(encounter.city.name in cityCounts)) {
|
||||||
|
cityCounts[encounter.city.name] = 0;
|
||||||
|
}
|
||||||
|
cityCounts[encounter.city.name] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
locationCounts[encounter.location] += 1;
|
|
||||||
|
|
||||||
if (!(encounter.role in roleCounts)) {
|
if (!(encounter.role in roleCounts)) {
|
||||||
roleCounts[encounter.role] = 0;
|
roleCounts[encounter.role] = 0;
|
||||||
@ -95,8 +136,8 @@ export function createAggregatedQuestionEncounter(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
companyCounts,
|
companyCounts,
|
||||||
|
countryCounts,
|
||||||
latestSeenAt,
|
latestSeenAt,
|
||||||
locationCounts,
|
|
||||||
roleCounts,
|
roleCounts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,31 @@
|
|||||||
import type { FilterChoice } from '~/components/questions/filter/FilterSection';
|
import type { FilterChoice } from '~/components/questions/filter/FilterSection';
|
||||||
|
|
||||||
import { LOCATIONS } from './constants';
|
import { trpc } from '../trpc';
|
||||||
|
|
||||||
export default function useDefaultLocation(): FilterChoice | undefined {
|
import type { Location } from '~/types/questions';
|
||||||
return LOCATIONS[0];
|
|
||||||
|
export default function useDefaultLocation():
|
||||||
|
| (FilterChoice & Location)
|
||||||
|
| undefined {
|
||||||
|
const { data: locations } = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: 'singapore',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (locations === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, name, state } = locations[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
cityId: id,
|
||||||
|
countryId: state.country.id,
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state.name}, ${state.country.name}`,
|
||||||
|
stateId: state.id,
|
||||||
|
value: id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import type { Vote } from '@prisma/client';
|
|||||||
import { trpc } from '../trpc';
|
import { trpc } from '../trpc';
|
||||||
|
|
||||||
type UseVoteOptions = {
|
type UseVoteOptions = {
|
||||||
createVote: (opts: { vote: Vote }) => void;
|
setDownVote: () => void;
|
||||||
deleteVote: (opts: { id: string }) => void;
|
setNoVote: () => void;
|
||||||
updateVote: (opts: BackendVote) => void;
|
setUpVote: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BackendVote = {
|
type BackendVote = {
|
||||||
@ -19,47 +19,23 @@ const createVoteCallbacks = (
|
|||||||
vote: BackendVote | null,
|
vote: BackendVote | null,
|
||||||
opts: UseVoteOptions,
|
opts: UseVoteOptions,
|
||||||
) => {
|
) => {
|
||||||
const { createVote, updateVote, deleteVote } = opts;
|
const { setDownVote, setNoVote, setUpVote } = opts;
|
||||||
|
|
||||||
const handleUpvote = () => {
|
const handleUpvote = () => {
|
||||||
// Either upvote or remove upvote
|
// Either upvote or remove upvote
|
||||||
if (vote) {
|
if (vote && vote.vote === 'UPVOTE') {
|
||||||
if (vote.vote === 'DOWNVOTE') {
|
setNoVote();
|
||||||
updateVote({
|
|
||||||
id: vote.id,
|
|
||||||
vote: 'UPVOTE',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deleteVote({
|
|
||||||
id: vote.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Update vote to an upvote
|
|
||||||
} else {
|
} else {
|
||||||
createVote({
|
setUpVote();
|
||||||
vote: 'UPVOTE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownvote = () => {
|
const handleDownvote = () => {
|
||||||
// Either downvote or remove downvote
|
// Either downvote or remove downvote
|
||||||
if (vote) {
|
if (vote && vote.vote === 'DOWNVOTE') {
|
||||||
if (vote.vote === 'UPVOTE') {
|
setNoVote();
|
||||||
updateVote({
|
|
||||||
id: vote.id,
|
|
||||||
vote: 'DOWNVOTE',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deleteVote({
|
|
||||||
id: vote.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Update vote to an upvote
|
|
||||||
} else {
|
} else {
|
||||||
createVote({
|
setDownVote();
|
||||||
vote: 'DOWNVOTE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,61 +47,61 @@ type QueryKey = Parameters<typeof trpc.useQuery>[0][0];
|
|||||||
|
|
||||||
export const useQuestionVote = (id: string) => {
|
export const useQuestionVote = (id: string) => {
|
||||||
return useVote(id, {
|
return useVote(id, {
|
||||||
create: 'questions.questions.user.createVote',
|
|
||||||
deleteKey: 'questions.questions.user.deleteVote',
|
|
||||||
idKey: 'questionId',
|
idKey: 'questionId',
|
||||||
invalidateKeys: [
|
invalidateKeys: [
|
||||||
'questions.questions.getQuestionsByFilter',
|
'questions.questions.getQuestionsByFilter',
|
||||||
'questions.questions.getQuestionById',
|
'questions.questions.getQuestionById',
|
||||||
],
|
],
|
||||||
query: 'questions.questions.user.getVote',
|
query: 'questions.questions.user.getVote',
|
||||||
update: 'questions.questions.user.updateVote',
|
setDownVoteKey: 'questions.questions.user.setDownVote',
|
||||||
|
setNoVoteKey: 'questions.questions.user.setNoVote',
|
||||||
|
setUpVoteKey: 'questions.questions.user.setUpVote',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAnswerVote = (id: string) => {
|
export const useAnswerVote = (id: string) => {
|
||||||
return useVote(id, {
|
return useVote(id, {
|
||||||
create: 'questions.answers.user.createVote',
|
|
||||||
deleteKey: 'questions.answers.user.deleteVote',
|
|
||||||
idKey: 'answerId',
|
idKey: 'answerId',
|
||||||
invalidateKeys: [
|
invalidateKeys: [
|
||||||
'questions.answers.getAnswers',
|
'questions.answers.getAnswers',
|
||||||
'questions.answers.getAnswerById',
|
'questions.answers.getAnswerById',
|
||||||
],
|
],
|
||||||
query: 'questions.answers.user.getVote',
|
query: 'questions.answers.user.getVote',
|
||||||
update: 'questions.answers.user.updateVote',
|
setDownVoteKey: 'questions.answers.user.setDownVote',
|
||||||
|
setNoVoteKey: 'questions.answers.user.setNoVote',
|
||||||
|
setUpVoteKey: 'questions.answers.user.setUpVote',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQuestionCommentVote = (id: string) => {
|
export const useQuestionCommentVote = (id: string) => {
|
||||||
return useVote(id, {
|
return useVote(id, {
|
||||||
create: 'questions.questions.comments.user.createVote',
|
|
||||||
deleteKey: 'questions.questions.comments.user.deleteVote',
|
|
||||||
idKey: 'questionCommentId',
|
idKey: 'questionCommentId',
|
||||||
invalidateKeys: ['questions.questions.comments.getQuestionComments'],
|
invalidateKeys: ['questions.questions.comments.getQuestionComments'],
|
||||||
query: 'questions.questions.comments.user.getVote',
|
query: 'questions.questions.comments.user.getVote',
|
||||||
update: 'questions.questions.comments.user.updateVote',
|
setDownVoteKey: 'questions.questions.comments.user.setDownVote',
|
||||||
|
setNoVoteKey: 'questions.questions.comments.user.setNoVote',
|
||||||
|
setUpVoteKey: 'questions.questions.comments.user.setUpVote',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAnswerCommentVote = (id: string) => {
|
export const useAnswerCommentVote = (id: string) => {
|
||||||
return useVote(id, {
|
return useVote(id, {
|
||||||
create: 'questions.answers.comments.user.createVote',
|
|
||||||
deleteKey: 'questions.answers.comments.user.deleteVote',
|
|
||||||
idKey: 'answerCommentId',
|
idKey: 'answerCommentId',
|
||||||
invalidateKeys: ['questions.answers.comments.getAnswerComments'],
|
invalidateKeys: ['questions.answers.comments.getAnswerComments'],
|
||||||
query: 'questions.answers.comments.user.getVote',
|
query: 'questions.answers.comments.user.getVote',
|
||||||
update: 'questions.answers.comments.user.updateVote',
|
setDownVoteKey: 'questions.answers.comments.user.setDownVote',
|
||||||
|
setNoVoteKey: 'questions.answers.comments.user.setNoVote',
|
||||||
|
setUpVoteKey: 'questions.answers.comments.user.setUpVote',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
|
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
|
||||||
create: MutationKey;
|
|
||||||
deleteKey: MutationKey;
|
|
||||||
idKey: string;
|
idKey: string;
|
||||||
invalidateKeys: Array<VoteQueryKey>;
|
invalidateKeys: Array<VoteQueryKey>;
|
||||||
query: VoteQueryKey;
|
query: VoteQueryKey;
|
||||||
update: MutationKey;
|
setDownVoteKey: MutationKey;
|
||||||
|
setNoVoteKey: MutationKey;
|
||||||
|
setUpVoteKey: MutationKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseVoteMutationContext = {
|
type UseVoteMutationContext = {
|
||||||
@ -137,7 +113,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
|||||||
id: string,
|
id: string,
|
||||||
opts: VoteProps<VoteQueryKey>,
|
opts: VoteProps<VoteQueryKey>,
|
||||||
) => {
|
) => {
|
||||||
const { create, deleteKey, query, update, idKey, invalidateKeys } = opts;
|
const {
|
||||||
|
idKey,
|
||||||
|
invalidateKeys,
|
||||||
|
query,
|
||||||
|
setDownVoteKey,
|
||||||
|
setNoVoteKey,
|
||||||
|
setUpVoteKey,
|
||||||
|
} = opts;
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const onVoteUpdate = useCallback(() => {
|
const onVoteUpdate = useCallback(() => {
|
||||||
@ -157,8 +140,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
|||||||
|
|
||||||
const backendVote = data as BackendVote;
|
const backendVote = data as BackendVote;
|
||||||
|
|
||||||
const { mutate: createVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
const { mutate: setUpVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||||
create,
|
setUpVoteKey,
|
||||||
{
|
{
|
||||||
onError: (err, variables, context) => {
|
onError: (err, variables, context) => {
|
||||||
if (context !== undefined) {
|
if (context !== undefined) {
|
||||||
@ -185,8 +168,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
|||||||
onSettled: onVoteUpdate,
|
onSettled: onVoteUpdate,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { mutate: updateVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
const { mutate: setDownVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||||
update,
|
setDownVoteKey,
|
||||||
{
|
{
|
||||||
onError: (error, variables, context) => {
|
onError: (error, variables, context) => {
|
||||||
if (context !== undefined) {
|
if (context !== undefined) {
|
||||||
@ -214,8 +197,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutate: deleteVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
const { mutate: setNoVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||||
deleteKey,
|
setNoVoteKey,
|
||||||
{
|
{
|
||||||
onError: (err, variables, context) => {
|
onError: (err, variables, context) => {
|
||||||
if (context !== undefined) {
|
if (context !== undefined) {
|
||||||
@ -242,14 +225,21 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
|||||||
const { handleDownvote, handleUpvote } = createVoteCallbacks(
|
const { handleDownvote, handleUpvote } = createVoteCallbacks(
|
||||||
backendVote ?? null,
|
backendVote ?? null,
|
||||||
{
|
{
|
||||||
createVote: ({ vote }) => {
|
setDownVote: () => {
|
||||||
createVote({
|
setDownVote({
|
||||||
[idKey]: id,
|
[idKey]: id,
|
||||||
vote,
|
});
|
||||||
} as any);
|
},
|
||||||
|
setNoVote: () => {
|
||||||
|
setNoVote({
|
||||||
|
[idKey]: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setUpVote: () => {
|
||||||
|
setUpVote({
|
||||||
|
[idKey]: id,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
deleteVote,
|
|
||||||
updateVote,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user