mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-02-08 04:57:34 +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 {
|
||||
id String @id
|
||||
name String @unique
|
||||
code String @unique
|
||||
states State[]
|
||||
id String @id
|
||||
name String @unique
|
||||
code String @unique
|
||||
states State[]
|
||||
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||
}
|
||||
|
||||
model State {
|
||||
id String @id
|
||||
name String
|
||||
countryId String
|
||||
cities City[]
|
||||
country Country @relation(fields: [countryId], references: [id])
|
||||
id String @id
|
||||
name String
|
||||
countryId String
|
||||
cities City[]
|
||||
country Country @relation(fields: [countryId], references: [id])
|
||||
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||
|
||||
@@unique([name, countryId])
|
||||
}
|
||||
|
||||
model City {
|
||||
id String @id
|
||||
name String
|
||||
stateId String
|
||||
state State @relation(fields: [stateId], references: [id])
|
||||
id String @id
|
||||
name String
|
||||
stateId String
|
||||
state State @relation(fields: [stateId], references: [id])
|
||||
questionsQuestionEncounters QuestionsQuestionEncounter[]
|
||||
|
||||
@@unique([name, stateId])
|
||||
}
|
||||
@@ -423,6 +426,7 @@ enum QuestionsQuestionType {
|
||||
CODING
|
||||
SYSTEM_DESIGN
|
||||
BEHAVIORAL
|
||||
THEORY
|
||||
}
|
||||
|
||||
model QuestionsQuestion {
|
||||
@@ -435,12 +439,12 @@ model QuestionsQuestion {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
encounters QuestionsQuestionEncounter[]
|
||||
votes QuestionsQuestionVote[]
|
||||
comments QuestionsQuestionComment[]
|
||||
answers QuestionsAnswer[]
|
||||
QuestionsListQuestionEntry QuestionsListQuestionEntry[]
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
encounters QuestionsQuestionEncounter[]
|
||||
votes QuestionsQuestionVote[]
|
||||
comments QuestionsQuestionComment[]
|
||||
answers QuestionsAnswer[]
|
||||
questionsListQuestionEntries QuestionsListQuestionEntry[]
|
||||
|
||||
@@index([lastSeenAt, id])
|
||||
@@index([upvotes, id])
|
||||
@@ -450,14 +454,18 @@ model QuestionsQuestionEncounter {
|
||||
id String @id @default(cuid())
|
||||
questionId String
|
||||
userId String?
|
||||
// TODO: sync with models (location, role)
|
||||
companyId String
|
||||
location String @db.Text
|
||||
companyId String?
|
||||
countryId String?
|
||||
stateId String?
|
||||
cityId String?
|
||||
role String @db.Text
|
||||
seenAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
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)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
@@ -489,6 +497,9 @@ model QuestionsQuestionComment {
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
votes QuestionsQuestionCommentVote[]
|
||||
|
||||
@@index([updatedAt, id])
|
||||
@@index([upvotes, id])
|
||||
}
|
||||
|
||||
model QuestionsQuestionCommentVote {
|
||||
@@ -518,6 +529,9 @@ model QuestionsAnswer {
|
||||
question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
votes QuestionsAnswerVote[]
|
||||
comments QuestionsAnswerComment[]
|
||||
|
||||
@@index([updatedAt, id])
|
||||
@@index([upvotes, id])
|
||||
}
|
||||
|
||||
model QuestionsAnswerVote {
|
||||
@@ -546,6 +560,9 @@ model QuestionsAnswerComment {
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade)
|
||||
votes QuestionsAnswerCommentVote[]
|
||||
|
||||
@@index([updatedAt, id])
|
||||
@@index([upvotes, id])
|
||||
}
|
||||
|
||||
model QuestionsAnswerCommentVote {
|
||||
|
||||
@@ -28,54 +28,56 @@ export default function ContributeQuestionCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<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"
|
||||
type="button"
|
||||
onClick={handleOpenContribute}>
|
||||
<TextInput
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label="Question"
|
||||
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">
|
||||
<TextInput
|
||||
disabled={true}
|
||||
label="Company"
|
||||
startAddOn={BuildingOffice2Icon}
|
||||
startAddOnType="icon"
|
||||
onChange={handleOpenContribute}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<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"
|
||||
type="button"
|
||||
onClick={handleOpenContribute}>
|
||||
<TextInput
|
||||
disabled={true}
|
||||
isLabelHidden={true}
|
||||
label="Question"
|
||||
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">
|
||||
<TextInput
|
||||
disabled={true}
|
||||
label="Company"
|
||||
startAddOn={BuildingOffice2Icon}
|
||||
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 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>
|
||||
<ContributeQuestionDialog
|
||||
show={showDraftDialog}
|
||||
onCancel={handleDraftDialogCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</button>
|
||||
<ContributeQuestionDialog
|
||||
show={showDraftDialog}
|
||||
onCancel={handleDraftDialogCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Button, Select, TextInput } from '@tih/ui';
|
||||
import { Button, TextInput } from '@tih/ui';
|
||||
|
||||
export type SortOption<Value> = {
|
||||
label: string;
|
||||
value: Value;
|
||||
import type { SortOptionsSelectProps } from './SortOptionsSelect';
|
||||
import SortOptionsSelect from './SortOptionsSelect';
|
||||
|
||||
export type QuestionSearchBarProps = SortOptionsSelectProps & {
|
||||
onFilterOptionsToggle: () => void;
|
||||
};
|
||||
|
||||
type SortOrderProps<SortOrder> = {
|
||||
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,
|
||||
export default function QuestionSearchBar({
|
||||
onFilterOptionsToggle,
|
||||
}: QuestionSearchBarProps<SortType, SortOrder>) {
|
||||
...sortOptionsSelectProps
|
||||
}: QuestionSearchBarProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-stretch gap-x-4 gap-y-2 lg:flex-row lg:items-end">
|
||||
<div className="flex-1 ">
|
||||
@@ -48,38 +27,7 @@ export default function QuestionSearchBar<SortType, SortOrder>({
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<SortOptionsSelect {...sortOptionsSelectProps} />
|
||||
<div className="lg:hidden">
|
||||
<Button
|
||||
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 { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
CheckIcon,
|
||||
@@ -18,6 +18,8 @@ import QuestionAggregateBadge from '../../QuestionAggregateBadge';
|
||||
import QuestionTypeBadge from '../../QuestionTypeBadge';
|
||||
import VotingButtons from '../../VotingButtons';
|
||||
|
||||
import type { CountryInfo } from '~/types/questions';
|
||||
|
||||
type UpvoteProps =
|
||||
| {
|
||||
showVoteButtons: true;
|
||||
@@ -51,13 +53,13 @@ type AnswerStatisticsProps =
|
||||
type AggregateStatisticsProps =
|
||||
| {
|
||||
companies: Record<string, number>;
|
||||
locations: Record<string, number>;
|
||||
countries: Record<string, CountryInfo>;
|
||||
roles: Record<string, number>;
|
||||
showAggregateStatistics: true;
|
||||
}
|
||||
| {
|
||||
companies?: never;
|
||||
locations?: never;
|
||||
countries?: never;
|
||||
roles?: never;
|
||||
showAggregateStatistics?: false;
|
||||
};
|
||||
@@ -136,7 +138,7 @@ export default function BaseQuestionCard({
|
||||
upvoteCount,
|
||||
timestamp,
|
||||
roles,
|
||||
locations,
|
||||
countries,
|
||||
showHover,
|
||||
onReceivedSubmit,
|
||||
showDeleteButton,
|
||||
@@ -147,6 +149,22 @@ export default function BaseQuestionCard({
|
||||
const [showReceivedForm, setShowReceivedForm] = useState(false);
|
||||
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
|
||||
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 = (
|
||||
<>
|
||||
{showVoteButtons && (
|
||||
@@ -168,7 +186,7 @@ export default function BaseQuestionCard({
|
||||
variant="primary"
|
||||
/>
|
||||
<QuestionAggregateBadge
|
||||
statistics={locations}
|
||||
statistics={locations!}
|
||||
variant="success"
|
||||
/>
|
||||
<QuestionAggregateBadge statistics={roles} variant="danger" />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { startOfMonth } from 'date-fns';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import type { QuestionsQuestionType } from '@prisma/client';
|
||||
import type { TypeaheadOption } 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 {
|
||||
useFormRegister,
|
||||
useSelectRegister,
|
||||
@@ -15,14 +16,16 @@ import RoleTypeahead from '../typeahead/RoleTypeahead';
|
||||
import type { Month } from '../../shared/MonthYearPicker';
|
||||
import MonthYearPicker from '../../shared/MonthYearPicker';
|
||||
|
||||
import type { Location } from '~/types/questions';
|
||||
|
||||
export type ContributeQuestionData = {
|
||||
company: string;
|
||||
date: Date;
|
||||
location: string;
|
||||
location: Location & TypeaheadOption;
|
||||
position: string;
|
||||
questionContent: string;
|
||||
questionType: QuestionsQuestionType;
|
||||
role: string;
|
||||
role: TypeaheadOption;
|
||||
};
|
||||
|
||||
export type ContributeQuestionFormProps = {
|
||||
@@ -79,15 +82,12 @@ export default function ContributeQuestionForm({
|
||||
name="location"
|
||||
render={({ field }) => (
|
||||
<LocationTypeahead
|
||||
{...field}
|
||||
required={true}
|
||||
onSelect={(option) => {
|
||||
// @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
|
||||
control={control}
|
||||
name="company"
|
||||
render={({ field }) => (
|
||||
render={({ field: { value: _, ...field } }) => (
|
||||
<CompanyTypeahead
|
||||
{...field}
|
||||
required={true}
|
||||
// @ts-ignore TODO(questions): handle potentially null value.
|
||||
onSelect={({ id }) => {
|
||||
@@ -134,13 +135,12 @@ export default function ContributeQuestionForm({
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<RoleTypeahead
|
||||
{...field}
|
||||
required={true}
|
||||
onSelect={(option) => {
|
||||
// @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 RoleTypeahead from '../typeahead/RoleTypeahead';
|
||||
|
||||
import type { Location } from '~/types/questions';
|
||||
|
||||
export type CreateQuestionEncounterData = {
|
||||
cityId?: string;
|
||||
company: string;
|
||||
location: string;
|
||||
countryId: string;
|
||||
role: string;
|
||||
seenAt: Date;
|
||||
stateId?: string;
|
||||
};
|
||||
|
||||
export type CreateQuestionEncounterFormProps = {
|
||||
@@ -28,7 +32,9 @@ export default function CreateQuestionEncounterForm({
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
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 [selectedDate, setSelectedDate] = useState<Date>(
|
||||
startOfMonth(new Date()),
|
||||
@@ -61,10 +67,10 @@ export default function CreateQuestionEncounterForm({
|
||||
placeholder="Other location"
|
||||
suggestedCount={3}
|
||||
// @ts-ignore TODO(questions): handle potentially null value.
|
||||
onSelect={({ value: location }) => {
|
||||
onSelect={(location) => {
|
||||
setSelectedLocation(location);
|
||||
}}
|
||||
onSuggestionClick={({ value: location }) => {
|
||||
onSuggestionClick={(location) => {
|
||||
setSelectedLocation(location);
|
||||
setStep(step + 1);
|
||||
}}
|
||||
@@ -130,11 +136,14 @@ export default function CreateQuestionEncounterForm({
|
||||
selectedRole &&
|
||||
selectedDate
|
||||
) {
|
||||
const { cityId, stateId, countryId } = selectedLocation;
|
||||
onSubmit({
|
||||
cityId,
|
||||
company: selectedCompany,
|
||||
location: selectedLocation,
|
||||
countryId,
|
||||
role: selectedRole,
|
||||
seenAt: selectedDate,
|
||||
stateId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -8,13 +8,16 @@ import type { RequireAllOrNone } from '~/utils/questions/RequireAllOrNone';
|
||||
type TypeaheadProps = ComponentProps<typeof Typeahead>;
|
||||
type TypeaheadOption = TypeaheadProps['options'][number];
|
||||
|
||||
export type ExpandedTypeaheadProps = RequireAllOrNone<{
|
||||
clearOnSelect?: boolean;
|
||||
filterOption: (option: TypeaheadOption) => boolean;
|
||||
onSuggestionClick: (option: TypeaheadOption) => void;
|
||||
suggestedCount: number;
|
||||
}> &
|
||||
TypeaheadProps;
|
||||
export type ExpandedTypeaheadProps = Omit<TypeaheadProps, 'onSelect'> &
|
||||
RequireAllOrNone<{
|
||||
clearOnSelect?: boolean;
|
||||
filterOption: (option: TypeaheadOption) => boolean;
|
||||
onSuggestionClick: (option: TypeaheadOption) => void;
|
||||
suggestedCount: number;
|
||||
}> & {
|
||||
onChange?: unknown; // Workaround: This prop is here just to absorb the onChange returned react-hook-form
|
||||
onSelect: (option: TypeaheadOption) => void;
|
||||
};
|
||||
|
||||
export default function ExpandedTypeahead({
|
||||
suggestedCount = 0,
|
||||
@@ -23,6 +26,7 @@ export default function ExpandedTypeahead({
|
||||
clearOnSelect = false,
|
||||
options,
|
||||
onSelect,
|
||||
onChange: _,
|
||||
...typeaheadProps
|
||||
}: ExpandedTypeaheadProps) {
|
||||
const [key, setKey] = useState(0);
|
||||
@@ -55,7 +59,8 @@ export default function ExpandedTypeahead({
|
||||
if (clearOnSelect) {
|
||||
setKey((key + 1) % 2);
|
||||
}
|
||||
onSelect(option);
|
||||
// TODO: Remove onSelect null coercion once onSelect prop is refactored
|
||||
onSelect(option!);
|
||||
}}
|
||||
/>
|
||||
</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 ExpandedTypeahead from './ExpandedTypeahead';
|
||||
|
||||
import type { Location } from '~/types/questions';
|
||||
|
||||
export type LocationTypeaheadProps = Omit<
|
||||
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 (
|
||||
<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"
|
||||
options={LOCATIONS}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onQueryChange={() => {}}
|
||||
options={locationOptions}
|
||||
onQueryChange={setQuery}
|
||||
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 type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||
@@ -17,13 +19,16 @@ const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
|
||||
}),
|
||||
);
|
||||
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
return (
|
||||
<ExpandedTypeahead
|
||||
{...(props as ExpandedTypeaheadProps)}
|
||||
label="Role"
|
||||
options={ROLES}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onQueryChange={() => {}}
|
||||
options={ROLES.filter((option) =>
|
||||
option.label.toLowerCase().includes(query.toLowerCase()),
|
||||
)}
|
||||
onQueryChange={setQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 FullAnswerCard from '~/components/questions/card/FullAnswerCard';
|
||||
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 { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
|
||||
export type AnswerCommentData = {
|
||||
commentContent: string;
|
||||
};
|
||||
@@ -19,6 +24,13 @@ export type AnswerCommentData = {
|
||||
export default function QuestionPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [commentSortOrder, setCommentSortOrder] = useState<SortOrder>(
|
||||
SortOrder.DESC,
|
||||
);
|
||||
const [commentSortType, setCommentSortType] = useState<SortType>(
|
||||
SortType.NEW,
|
||||
);
|
||||
|
||||
const {
|
||||
register: comRegister,
|
||||
reset: resetComment,
|
||||
@@ -36,10 +48,23 @@ export default function QuestionPage() {
|
||||
{ answerId: answerId as string },
|
||||
]);
|
||||
|
||||
const { data: comments } = trpc.useQuery([
|
||||
'questions.answers.comments.getAnswerComments',
|
||||
{ answerId: answerId as string },
|
||||
]);
|
||||
const answerCommentInfiniteQuery = trpc.useInfiniteQuery(
|
||||
[
|
||||
'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(
|
||||
'questions.answers.comments.user.create',
|
||||
@@ -47,7 +72,11 @@ export default function QuestionPage() {
|
||||
onSuccess: () => {
|
||||
utils.invalidateQueries([
|
||||
'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}
|
||||
/>
|
||||
<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
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
@@ -142,18 +145,35 @@ export default function QuestionPage() {
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{(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}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
sortTypeValue={commentSortType}
|
||||
onSortOrderChange={setCommentSortOrder}
|
||||
onSortTypeChange={setCommentSortType}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
|
||||
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
|
||||
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 createSlug from '~/utils/questions/createSlug';
|
||||
@@ -16,6 +18,8 @@ import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregat
|
||||
import { useFormRegister } from '~/utils/questions/useFormRegister';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
|
||||
export type AnswerQuestionData = {
|
||||
answerContent: string;
|
||||
};
|
||||
@@ -26,6 +30,19 @@ export type QuestionCommentData = {
|
||||
|
||||
export default function QuestionPage() {
|
||||
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 {
|
||||
register: ansRegister,
|
||||
handleSubmit,
|
||||
@@ -64,10 +81,23 @@ export default function QuestionPage() {
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { data: comments } = trpc.useQuery([
|
||||
'questions.questions.comments.getQuestionComments',
|
||||
{ questionId: questionId as string },
|
||||
]);
|
||||
const commentInfiniteQuery = trpc.useInfiniteQuery(
|
||||
[
|
||||
'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(
|
||||
'questions.questions.comments.user.create',
|
||||
@@ -80,10 +110,23 @@ export default function QuestionPage() {
|
||||
},
|
||||
);
|
||||
|
||||
const { data: answers } = trpc.useQuery([
|
||||
'questions.answers.getAnswers',
|
||||
{ questionId: questionId as string },
|
||||
]);
|
||||
const answerInfiniteQuery = trpc.useInfiniteQuery(
|
||||
[
|
||||
'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(
|
||||
'questions.answers.user.create',
|
||||
@@ -144,12 +187,12 @@ export default function QuestionPage() {
|
||||
variant="secondary"
|
||||
/>
|
||||
</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">
|
||||
<FullQuestionCard
|
||||
{...question}
|
||||
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
||||
locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
|
||||
countries={relabeledAggregatedEncounters?.countryCounts ?? {}}
|
||||
questionId={question.id}
|
||||
receivedCount={undefined}
|
||||
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
|
||||
@@ -160,78 +203,74 @@ export default function QuestionPage() {
|
||||
upvoteCount={question.numVotes}
|
||||
onReceivedSubmit={(data) => {
|
||||
addEncounter({
|
||||
cityId: data.cityId,
|
||||
companyId: data.company,
|
||||
location: data.location,
|
||||
countryId: data.countryId,
|
||||
questionId: questionId as string,
|
||||
role: data.role,
|
||||
seenAt: data.seenAt,
|
||||
stateId: data.stateId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="mx-2">
|
||||
<Collapsible label={`${(comments ?? []).length} comment(s)`}>
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<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);
|
||||
}}
|
||||
<Collapsible label={`${question.numComments} comment(s)`}>
|
||||
<div className="mt-4 px-4">
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</form>
|
||||
{/* TODO: Add button to load more */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
{(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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
<form onSubmit={handleSubmit(handleSubmitAnswer)}>
|
||||
<TextArea
|
||||
{...answerRegister('answerContent', {
|
||||
@@ -244,34 +283,6 @@ export default function QuestionPage() {
|
||||
rows={5}
|
||||
/>
|
||||
<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
|
||||
disabled={!isDirty || !isValid}
|
||||
label="Contribute"
|
||||
@@ -280,21 +291,37 @@ export default function QuestionPage() {
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
{(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}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xl">{question.numAnswers} answers</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={answerSortOrder}
|
||||
sortTypeValue={answerSortType}
|
||||
onSortOrderChange={setAnswerSortOrder}
|
||||
onSortTypeChange={setAnswerSortType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Add button to load more */}
|
||||
{(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>
|
||||
|
||||
@@ -5,11 +5,13 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { Bars3BottomLeftIcon } from '@heroicons/react/20/solid';
|
||||
import { NoSymbolIcon } from '@heroicons/react/24/outline';
|
||||
import type { QuestionsQuestionType } from '@prisma/client';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { Button, SlideOut } from '@tih/ui';
|
||||
|
||||
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
|
||||
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
|
||||
import FilterSection from '~/components/questions/filter/FilterSection';
|
||||
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
|
||||
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
|
||||
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 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 { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
|
||||
import createSlug from '~/utils/questions/createSlug';
|
||||
@@ -29,14 +29,29 @@ import {
|
||||
} from '~/utils/questions/useSearchParam';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { Location } from '~/types/questions.d';
|
||||
import { SortType } 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() {
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
|
||||
useSearchParam('companies');
|
||||
const [
|
||||
selectedCompanySlugs,
|
||||
setSelectedCompanySlugs,
|
||||
areCompaniesInitialized,
|
||||
] = useSearchParam('companies');
|
||||
const [
|
||||
selectedQuestionTypes,
|
||||
setSelectedQuestionTypes,
|
||||
@@ -70,7 +85,13 @@ export default function QuestionsBrowsePage() {
|
||||
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
|
||||
useSearchParam('roles');
|
||||
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] =
|
||||
useSearchParamSingle<SortOrder>('sortOrder', {
|
||||
@@ -122,13 +143,13 @@ export default function QuestionsBrowsePage() {
|
||||
|
||||
const hasFilters = useMemo(
|
||||
() =>
|
||||
selectedCompanies.length > 0 ||
|
||||
selectedCompanySlugs.length > 0 ||
|
||||
selectedQuestionTypes.length > 0 ||
|
||||
selectedQuestionAge !== 'all' ||
|
||||
selectedRoles.length > 0 ||
|
||||
selectedLocations.length > 0,
|
||||
[
|
||||
selectedCompanies,
|
||||
selectedCompanySlugs,
|
||||
selectedQuestionTypes,
|
||||
selectedQuestionAge,
|
||||
selectedRoles,
|
||||
@@ -147,24 +168,24 @@ export default function QuestionsBrowsePage() {
|
||||
: undefined;
|
||||
}, [selectedQuestionAge]);
|
||||
|
||||
const {
|
||||
data: questionsQueryData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = trpc.useInfiniteQuery(
|
||||
const questionsInfiniteQuery = trpc.useInfiniteQuery(
|
||||
[
|
||||
'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,
|
||||
limit: 10,
|
||||
locations: selectedLocations,
|
||||
questionTypes: selectedQuestionTypes,
|
||||
roles: selectedRoles,
|
||||
sortOrder,
|
||||
sortType,
|
||||
startDate,
|
||||
stateIds: [],
|
||||
},
|
||||
],
|
||||
{
|
||||
@@ -173,6 +194,8 @@ export default function QuestionsBrowsePage() {
|
||||
},
|
||||
);
|
||||
|
||||
const { data: questionsQueryData } = questionsInfiniteQuery;
|
||||
|
||||
const questionCount = useMemo(() => {
|
||||
if (!questionsQueryData) {
|
||||
return undefined;
|
||||
@@ -239,8 +262,8 @@ export default function QuestionsBrowsePage() {
|
||||
Router.replace({
|
||||
pathname,
|
||||
query: {
|
||||
companies: selectedCompanies,
|
||||
locations: selectedLocations,
|
||||
companies: selectedCompanySlugs,
|
||||
locations: selectedLocations.map(locationToSlug),
|
||||
questionAge: selectedQuestionAge,
|
||||
questionTypes: selectedQuestionTypes,
|
||||
roles: selectedRoles,
|
||||
@@ -255,7 +278,7 @@ export default function QuestionsBrowsePage() {
|
||||
areSearchOptionsInitialized,
|
||||
loaded,
|
||||
pathname,
|
||||
selectedCompanies,
|
||||
selectedCompanySlugs,
|
||||
selectedRoles,
|
||||
selectedLocations,
|
||||
selectedQuestionAge,
|
||||
@@ -265,13 +288,16 @@ export default function QuestionsBrowsePage() {
|
||||
]);
|
||||
|
||||
const selectedCompanyOptions = useMemo(() => {
|
||||
return selectedCompanies.map((company) => ({
|
||||
checked: true,
|
||||
id: company,
|
||||
label: company,
|
||||
value: company,
|
||||
}));
|
||||
}, [selectedCompanies]);
|
||||
return selectedCompanySlugs.map((company) => {
|
||||
const [id, label] = company.split('_');
|
||||
return {
|
||||
checked: true,
|
||||
id,
|
||||
label,
|
||||
value: id,
|
||||
};
|
||||
});
|
||||
}, [selectedCompanySlugs]);
|
||||
|
||||
const selectedRoleOptions = useMemo(() => {
|
||||
return selectedRoles.map((role) => ({
|
||||
@@ -285,9 +311,7 @@ export default function QuestionsBrowsePage() {
|
||||
const selectedLocationOptions = useMemo(() => {
|
||||
return selectedLocations.map((location) => ({
|
||||
checked: true,
|
||||
id: location,
|
||||
label: location,
|
||||
value: location,
|
||||
...location,
|
||||
}));
|
||||
}, [selectedLocations]);
|
||||
|
||||
@@ -305,7 +329,7 @@ export default function QuestionsBrowsePage() {
|
||||
label="Clear filters"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setSelectedCompanies([]);
|
||||
setSelectedCompanySlugs([]);
|
||||
setSelectedQuestionTypes([]);
|
||||
setSelectedQuestionAge('all');
|
||||
setSelectedRoles([]);
|
||||
@@ -320,8 +344,8 @@ export default function QuestionsBrowsePage() {
|
||||
{...field}
|
||||
clearOnSelect={true}
|
||||
filterOption={(option) => {
|
||||
return !selectedCompanies.some((company) => {
|
||||
return company === option.value;
|
||||
return !selectedCompanySlugs.some((companySlug) => {
|
||||
return companySlug === `${option.id}_${option.label}`;
|
||||
});
|
||||
}}
|
||||
isLabelHidden={true}
|
||||
@@ -337,10 +361,15 @@ export default function QuestionsBrowsePage() {
|
||||
)}
|
||||
onOptionChange={(option) => {
|
||||
if (option.checked) {
|
||||
setSelectedCompanies([...selectedCompanies, option.label]);
|
||||
setSelectedCompanySlugs([
|
||||
...selectedCompanySlugs,
|
||||
`${option.id}_${option.label}`,
|
||||
]);
|
||||
} else {
|
||||
setSelectedCompanies(
|
||||
selectedCompanies.filter((company) => company !== option.label),
|
||||
setSelectedCompanySlugs(
|
||||
selectedCompanySlugs.filter(
|
||||
(companySlug) => companySlug !== `${option.id}_${option.label}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -348,7 +377,10 @@ export default function QuestionsBrowsePage() {
|
||||
<FilterSection
|
||||
label="Roles"
|
||||
options={selectedRoleOptions}
|
||||
renderInput={({ onOptionChange, field: { ref: _, ...field } }) => (
|
||||
renderInput={({
|
||||
onOptionChange,
|
||||
field: { ref: _, onChange: __, ...field },
|
||||
}) => (
|
||||
<RoleTypeahead
|
||||
{...field}
|
||||
clearOnSelect={true}
|
||||
@@ -406,13 +438,16 @@ export default function QuestionsBrowsePage() {
|
||||
<FilterSection
|
||||
label="Locations"
|
||||
options={selectedLocationOptions}
|
||||
renderInput={({ onOptionChange, field: { ref: _, ...field } }) => (
|
||||
renderInput={({
|
||||
onOptionChange,
|
||||
field: { ref: _, onChange: __, ...field },
|
||||
}) => (
|
||||
<LocationTypeahead
|
||||
{...field}
|
||||
clearOnSelect={true}
|
||||
filterOption={(option) => {
|
||||
return !selectedLocations.some((location) => {
|
||||
return location === option.value;
|
||||
return location.id === option.id;
|
||||
});
|
||||
}}
|
||||
isLabelHidden={true}
|
||||
@@ -428,10 +463,14 @@ export default function QuestionsBrowsePage() {
|
||||
)}
|
||||
onOptionChange={(option) => {
|
||||
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 {
|
||||
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">
|
||||
<ContributeQuestionCard
|
||||
onSubmit={(data) => {
|
||||
const { cityId, countryId, stateId } = data.location;
|
||||
createQuestion({
|
||||
cityId,
|
||||
companyId: data.company,
|
||||
content: data.questionContent,
|
||||
location: data.location,
|
||||
countryId,
|
||||
questionType: data.questionType,
|
||||
role: data.role,
|
||||
role: data.role.value,
|
||||
seenAt: data.date,
|
||||
stateId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
|
||||
<QuestionSearchBar
|
||||
sortOrderOptions={SORT_ORDERS}
|
||||
sortOrderValue={sortOrder}
|
||||
sortTypeOptions={SORT_TYPES}
|
||||
sortTypeValue={sortType}
|
||||
onFilterOptionsToggle={() => {
|
||||
setFilterDrawerOpen(!filterDrawerOpen);
|
||||
@@ -477,7 +517,7 @@ export default function QuestionsBrowsePage() {
|
||||
{(questionsQueryData?.pages ?? []).flatMap(
|
||||
({ data: questions }) =>
|
||||
questions.map((question) => {
|
||||
const { companyCounts, locationCounts, roleCounts } =
|
||||
const { companyCounts, countryCounts, roleCounts } =
|
||||
relabelQuestionAggregates(
|
||||
question.aggregatedQuestionEncounters,
|
||||
);
|
||||
@@ -488,10 +528,10 @@ export default function QuestionsBrowsePage() {
|
||||
answerCount={question.numAnswers}
|
||||
companies={companyCounts}
|
||||
content={question.content}
|
||||
countries={countryCounts}
|
||||
href={`/questions/${question.id}/${createSlug(
|
||||
question.content,
|
||||
)}`}
|
||||
locations={locationCounts}
|
||||
questionId={question.id}
|
||||
receivedCount={question.receivedCount}
|
||||
roles={roleCounts}
|
||||
@@ -508,15 +548,7 @@ export default function QuestionsBrowsePage() {
|
||||
);
|
||||
}),
|
||||
)}
|
||||
<Button
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
fetchNextPage();
|
||||
}}
|
||||
/>
|
||||
<PaginationLoadMoreButton query={questionsInfiniteQuery} />
|
||||
{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">
|
||||
<NoSymbolIcon className="h-6 w-6" />
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function ListPage() {
|
||||
<div className="flex flex-col gap-4 pb-4">
|
||||
{lists[selectedListIndex].questionEntries.map(
|
||||
({ question, id: entryId }) => {
|
||||
const { companyCounts, locationCounts, roleCounts } =
|
||||
const { companyCounts, countryCounts, roleCounts } =
|
||||
relabelQuestionAggregates(
|
||||
question.aggregatedQuestionEncounters,
|
||||
);
|
||||
@@ -184,10 +184,10 @@ export default function ListPage() {
|
||||
key={question.id}
|
||||
companies={companyCounts}
|
||||
content={question.content}
|
||||
countries={countryCounts}
|
||||
href={`/questions/${question.id}/${createSlug(
|
||||
question.content,
|
||||
)}`}
|
||||
locations={locationCounts}
|
||||
questionId={question.id}
|
||||
receivedCount={question.receivedCount}
|
||||
roles={roleCounts}
|
||||
|
||||
@@ -19,9 +19,11 @@ export const locationsRouter = createRouter()
|
||||
select: {
|
||||
country: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,16 +4,43 @@ import { Vote } from '@prisma/client';
|
||||
import { createRouter } from '../context';
|
||||
|
||||
import type { AnswerComment } from '~/types/questions';
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
|
||||
export const questionsAnswerCommentRouter = createRouter().query(
|
||||
'getAnswerComments',
|
||||
{
|
||||
input: z.object({
|
||||
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 }) {
|
||||
const { answerId, cursor } = input;
|
||||
|
||||
const sortCondition =
|
||||
input.sortType === SortType.TOP
|
||||
? [
|
||||
{
|
||||
upvotes: input.sortOrder,
|
||||
},
|
||||
{
|
||||
id: input.sortOrder,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
updatedAt: input.sortOrder,
|
||||
},
|
||||
{
|
||||
id: input.sortOrder,
|
||||
},
|
||||
];
|
||||
|
||||
const questionAnswerCommentsData =
|
||||
await ctx.prisma.questionsAnswerComment.findMany({
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
@@ -23,14 +50,13 @@ export const questionsAnswerCommentRouter = createRouter().query(
|
||||
},
|
||||
votes: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
orderBy: sortCondition,
|
||||
take: input.limit + 1,
|
||||
where: {
|
||||
answerId: input.answerId,
|
||||
answerId,
|
||||
},
|
||||
});
|
||||
return questionAnswerCommentsData.map((data) => {
|
||||
const processedQuestionAnswerCommentsData = questionAnswerCommentsData.map((data) => {
|
||||
const votes: number = data.votes.reduce(
|
||||
(previousValue: number, currentValue) => {
|
||||
let result: number = previousValue;
|
||||
@@ -59,6 +85,22 @@ export const questionsAnswerCommentRouter = createRouter().query(
|
||||
};
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
@@ -100,27 +100,237 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('createVote', {
|
||||
.mutation('setUpVote', {
|
||||
input: z.object({
|
||||
answerCommentId: z.string(),
|
||||
vote: z.nativeEnum(Vote),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
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([
|
||||
ctx.prisma.questionsAnswerCommentVote.create({
|
||||
data: {
|
||||
answerCommentId,
|
||||
userId,
|
||||
vote,
|
||||
const vote = await tx.questionsAnswerCommentVote.findUnique({
|
||||
where: {
|
||||
answerCommentId_userId: { answerCommentId, userId },
|
||||
},
|
||||
}),
|
||||
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: {
|
||||
upvotes: {
|
||||
increment: incrementValue,
|
||||
@@ -129,101 +339,9 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
|
||||
where: {
|
||||
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) {
|
||||
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;
|
||||
return voteToDelete;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,16 +5,42 @@ import { TRPCError } from '@trpc/server';
|
||||
import { createRouter } from '../context';
|
||||
|
||||
import type { Answer } from '~/types/questions';
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
|
||||
export const questionsAnswerRouter = createRouter()
|
||||
.query('getAnswers', {
|
||||
input: z.object({
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).default(50),
|
||||
questionId: z.string(),
|
||||
sortOrder: z.nativeEnum(SortOrder),
|
||||
sortType: z.nativeEnum(SortType),
|
||||
}),
|
||||
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({
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
@@ -29,14 +55,14 @@ export const questionsAnswerRouter = createRouter()
|
||||
},
|
||||
votes: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
orderBy: sortCondition,
|
||||
take: input.limit + 1,
|
||||
where: {
|
||||
questionId,
|
||||
},
|
||||
});
|
||||
return answersData.map((data) => {
|
||||
|
||||
const processedAnswersData = answersData.map((data) => {
|
||||
const votes: number = data.votes.reduce(
|
||||
(previousValue: number, currentValue) => {
|
||||
let result: number = previousValue;
|
||||
@@ -65,6 +91,22 @@ export const questionsAnswerRouter = createRouter()
|
||||
};
|
||||
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', {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
||||
},
|
||||
});
|
||||
|
||||
if (answerToUpdate?.id !== userId) {
|
||||
if (answerToUpdate?.userId !== userId) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
@@ -98,27 +98,234 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('createVote', {
|
||||
.mutation('setUpVote', {
|
||||
input: z.object({
|
||||
answerId: z.string(),
|
||||
vote: z.nativeEnum(Vote),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
const { answerId } = input;
|
||||
|
||||
const { answerId, vote } = input;
|
||||
|
||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
||||
|
||||
const [answerVote] = await ctx.prisma.$transaction([
|
||||
ctx.prisma.questionsAnswerVote.create({
|
||||
data: {
|
||||
answerId,
|
||||
userId,
|
||||
vote,
|
||||
return await ctx.prisma.$transaction(async (tx) => {
|
||||
const answerToUpdate = await tx.questionsAnswer.findUnique({
|
||||
where: {
|
||||
id: answerId,
|
||||
},
|
||||
}),
|
||||
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: {
|
||||
upvotes: {
|
||||
increment: incrementValue,
|
||||
@@ -127,98 +334,9 @@ export const questionsAnswerUserRouter = createProtectedRouter()
|
||||
where: {
|
||||
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;
|
||||
|
||||
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,
|
||||
},
|
||||
return voteToDelete;
|
||||
});
|
||||
|
||||
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: {
|
||||
select: {
|
||||
city: true,
|
||||
company: true,
|
||||
location: true,
|
||||
country: true,
|
||||
role: true,
|
||||
seenAt: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -83,10 +85,12 @@ export const questionsListRouter = createProtectedRouter()
|
||||
},
|
||||
encounters: {
|
||||
select: {
|
||||
city: true,
|
||||
company: true,
|
||||
location: true,
|
||||
country: true,
|
||||
role: true,
|
||||
seenAt: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
|
||||
@@ -4,17 +4,43 @@ import { Vote } from '@prisma/client';
|
||||
import { createRouter } from '../context';
|
||||
|
||||
import type { QuestionComment } from '~/types/questions';
|
||||
import { SortOrder, SortType } from '~/types/questions.d';
|
||||
|
||||
export const questionsQuestionCommentRouter = createRouter().query(
|
||||
'getQuestionComments',
|
||||
{
|
||||
input: z.object({
|
||||
cursor: z.string().nullish(),
|
||||
limit: z.number().min(1).default(50),
|
||||
questionId: z.string(),
|
||||
sortOrder: z.nativeEnum(SortOrder),
|
||||
sortType: z.nativeEnum(SortType),
|
||||
}),
|
||||
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 =
|
||||
await ctx.prisma.questionsQuestionComment.findMany({
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
@@ -24,14 +50,13 @@ export const questionsQuestionCommentRouter = createRouter().query(
|
||||
},
|
||||
votes: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
orderBy: sortCondition,
|
||||
take: input.limit + 1,
|
||||
where: {
|
||||
questionId,
|
||||
},
|
||||
});
|
||||
return questionCommentsData.map((data) => {
|
||||
const processedQuestionCommentsData = questionCommentsData.map((data) => {
|
||||
const votes: number = data.votes.reduce(
|
||||
(previousValue: number, currentValue) => {
|
||||
let result: number = previousValue;
|
||||
@@ -59,6 +84,22 @@ export const questionsQuestionCommentRouter = createRouter().query(
|
||||
};
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
@@ -101,26 +101,240 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('createVote', {
|
||||
.mutation('setUpVote', {
|
||||
input: z.object({
|
||||
questionCommentId: z.string(),
|
||||
vote: z.nativeEnum(Vote),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
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([
|
||||
ctx.prisma.questionsQuestionCommentVote.create({
|
||||
data: {
|
||||
questionCommentId,
|
||||
userId,
|
||||
vote,
|
||||
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 },
|
||||
},
|
||||
}),
|
||||
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: {
|
||||
upvotes: {
|
||||
increment: incrementValue,
|
||||
@@ -129,100 +343,9 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
|
||||
where: {
|
||||
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) {
|
||||
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;
|
||||
return voteToDelete;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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(
|
||||
'getAggregatedEncounters',
|
||||
@@ -14,48 +14,17 @@ export const questionsQuestionEncounterRouter = createRouter().query(
|
||||
const questionEncountersData =
|
||||
await ctx.prisma.questionsQuestionEncounter.findMany({
|
||||
include: {
|
||||
city: true,
|
||||
company: true,
|
||||
country: true,
|
||||
state: true,
|
||||
},
|
||||
where: {
|
||||
...input,
|
||||
},
|
||||
});
|
||||
|
||||
const companyCounts: Record<string, number> = {};
|
||||
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;
|
||||
return createAggregatedQuestionEncounter(questionEncountersData);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,38 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createAggregatedQuestionEncounter } from '~/utils/questions/server/aggregate-encounters';
|
||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||
|
||||
import { createProtectedRouter } from '../context';
|
||||
|
||||
import { SortOrder } from '~/types/questions.d';
|
||||
|
||||
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', {
|
||||
input: z.object({
|
||||
cityId: z.string().nullish(),
|
||||
companyId: z.string(),
|
||||
location: z.string(),
|
||||
countryId: z.string(),
|
||||
questionId: z.string(),
|
||||
role: z.string(),
|
||||
role: z.nativeEnum(JobTitleLabels),
|
||||
seenAt: z.date(),
|
||||
stateId: z.string().nullish(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
|
||||
@@ -11,22 +11,18 @@ import { SortOrder, SortType } from '~/types/questions.d';
|
||||
export const questionsQuestionRouter = createRouter()
|
||||
.query('getQuestionsByFilter', {
|
||||
input: z.object({
|
||||
companyNames: z.string().array(),
|
||||
cursor: z
|
||||
.object({
|
||||
idCursor: z.string().optional(),
|
||||
lastSeenCursor: z.date().nullish().optional(),
|
||||
upvoteCursor: z.number().optional(),
|
||||
})
|
||||
.nullish(),
|
||||
cityIds: z.string().array(),
|
||||
companyIds: z.string().array(),
|
||||
countryIds: z.string().array(),
|
||||
cursor: z.string().nullish(),
|
||||
endDate: z.date().default(new Date()),
|
||||
limit: z.number().min(1).default(50),
|
||||
locations: z.string().array(),
|
||||
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
|
||||
roles: z.string().array(),
|
||||
sortOrder: z.nativeEnum(SortOrder),
|
||||
sortType: z.nativeEnum(SortType),
|
||||
startDate: z.date().optional(),
|
||||
stateIds: z.string().array(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { cursor } = input;
|
||||
@@ -51,12 +47,7 @@ export const questionsQuestionRouter = createRouter()
|
||||
];
|
||||
|
||||
const questionsData = await ctx.prisma.questionsQuestion.findMany({
|
||||
cursor:
|
||||
cursor !== undefined
|
||||
? {
|
||||
id: cursor ? cursor!.idCursor : undefined,
|
||||
}
|
||||
: undefined,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
@@ -66,10 +57,12 @@ export const questionsQuestionRouter = createRouter()
|
||||
},
|
||||
encounters: {
|
||||
select: {
|
||||
city: true,
|
||||
company: true,
|
||||
location: true,
|
||||
country: true,
|
||||
role: true,
|
||||
seenAt: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -95,19 +88,39 @@ export const questionsQuestionRouter = createRouter()
|
||||
gte: input.startDate,
|
||||
lte: input.endDate,
|
||||
},
|
||||
...(input.companyNames.length > 0
|
||||
...(input.companyIds.length > 0
|
||||
? {
|
||||
company: {
|
||||
name: {
|
||||
in: input.companyNames,
|
||||
id: {
|
||||
in: input.companyIds,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(input.locations.length > 0
|
||||
...(input.cityIds.length > 0
|
||||
? {
|
||||
location: {
|
||||
in: input.locations,
|
||||
city: {
|
||||
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();
|
||||
|
||||
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 = {
|
||||
idCursor: nextIdCursor,
|
||||
lastSeenCursor: nextLastSeenCursor,
|
||||
upvoteCursor: nextUpvoteCursor,
|
||||
};
|
||||
nextCursor = nextIdCursor;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,10 +172,12 @@ export const questionsQuestionRouter = createRouter()
|
||||
},
|
||||
encounters: {
|
||||
select: {
|
||||
city: true,
|
||||
company: true,
|
||||
location: true,
|
||||
country: true,
|
||||
role: true,
|
||||
seenAt: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -201,21 +208,23 @@ export const questionsQuestionRouter = createRouter()
|
||||
async resolve({ ctx, input }) {
|
||||
const escapeChars = /[()|&:*!]/g;
|
||||
|
||||
const query =
|
||||
input.content
|
||||
.replace(escapeChars, " ")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.join(" | ");
|
||||
const query = input.content
|
||||
.replace(escapeChars, ' ')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.join(' | ');
|
||||
|
||||
const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw`
|
||||
const relatedQuestionsId: Array<{ id: string }> = await ctx.prisma
|
||||
.$queryRaw`
|
||||
SELECT id FROM "QuestionsQuestion"
|
||||
WHERE
|
||||
to_tsvector("content") @@ to_tsquery('english', ${query})
|
||||
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({
|
||||
include: {
|
||||
@@ -227,10 +236,12 @@ export const questionsQuestionRouter = createRouter()
|
||||
},
|
||||
encounters: {
|
||||
select: {
|
||||
city: true,
|
||||
company: true,
|
||||
location: true,
|
||||
country: true,
|
||||
role: true,
|
||||
seenAt: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -241,9 +252,9 @@ export const questionsQuestionRouter = createRouter()
|
||||
votes: true,
|
||||
},
|
||||
where: {
|
||||
id : {
|
||||
in : relatedQuestionsIdArray,
|
||||
}
|
||||
id: {
|
||||
in: relatedQuestionsIdArray,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -252,5 +263,5 @@ export const questionsQuestionRouter = createRouter()
|
||||
);
|
||||
|
||||
return processedQuestionsData;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,12 +7,14 @@ import { createProtectedRouter } from '../context';
|
||||
export const questionsQuestionUserRouter = createProtectedRouter()
|
||||
.mutation('create', {
|
||||
input: z.object({
|
||||
cityId: z.string().nullish(),
|
||||
companyId: z.string(),
|
||||
content: z.string(),
|
||||
location: z.string(),
|
||||
countryId: z.string(),
|
||||
questionType: z.nativeEnum(QuestionsQuestionType),
|
||||
role: z.string(),
|
||||
seenAt: z.date(),
|
||||
stateId: z.string().nullish(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
@@ -22,14 +24,34 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
||||
content: input.content,
|
||||
encounters: {
|
||||
create: {
|
||||
city:
|
||||
input.cityId !== null
|
||||
? {
|
||||
connect: {
|
||||
id: input.cityId,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
company: {
|
||||
connect: {
|
||||
id: input.companyId,
|
||||
},
|
||||
},
|
||||
location: input.location,
|
||||
country: {
|
||||
connect: {
|
||||
id: input.countryId,
|
||||
},
|
||||
},
|
||||
role: input.role,
|
||||
seenAt: input.seenAt,
|
||||
state:
|
||||
input.stateId !== null
|
||||
? {
|
||||
connect: {
|
||||
id: input.stateId,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
@@ -59,7 +81,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
||||
},
|
||||
});
|
||||
|
||||
if (questionToUpdate?.id !== userId) {
|
||||
if (questionToUpdate?.userId !== userId) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
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({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User have no authorization to record.',
|
||||
@@ -123,26 +145,234 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('createVote', {
|
||||
.mutation('setUpVote', {
|
||||
input: z.object({
|
||||
questionId: z.string(),
|
||||
vote: z.nativeEnum(Vote),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const userId = ctx.session?.user?.id;
|
||||
const { questionId, vote } = input;
|
||||
const { questionId } = input;
|
||||
|
||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
||||
|
||||
const [questionVote] = await ctx.prisma.$transaction([
|
||||
ctx.prisma.questionsQuestionVote.create({
|
||||
data: {
|
||||
questionId,
|
||||
userId,
|
||||
vote,
|
||||
return await ctx.prisma.$transaction(async (tx) => {
|
||||
const questionToUpdate = await tx.questionsQuestion.findUnique({
|
||||
where: {
|
||||
id: questionId,
|
||||
},
|
||||
}),
|
||||
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: {
|
||||
upvotes: {
|
||||
increment: incrementValue,
|
||||
@@ -151,98 +381,9 @@ export const questionsQuestionUserRouter = createProtectedRouter()
|
||||
where: {
|
||||
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;
|
||||
|
||||
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,
|
||||
},
|
||||
return voteToDelete;
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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 = {
|
||||
companyCounts: Record<string, number>;
|
||||
countryCounts: Record<string, CountryInfo>;
|
||||
latestSeenAt: Date;
|
||||
locationCounts: Record<string, number>;
|
||||
roleCounts: Record<string, number>;
|
||||
};
|
||||
|
||||
|
||||
@@ -63,47 +63,6 @@ export const QUESTION_AGES: FilterChoices<QuestionAge> = [
|
||||
},
|
||||
] 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 = [
|
||||
{
|
||||
label: 'Ascending',
|
||||
|
||||
@@ -3,10 +3,8 @@ import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||
import type { AggregatedQuestionEncounter } from '~/types/questions';
|
||||
|
||||
export default function relabelQuestionAggregates({
|
||||
locationCounts,
|
||||
companyCounts,
|
||||
roleCounts,
|
||||
latestSeenAt,
|
||||
...rest
|
||||
}: AggregatedQuestionEncounter) {
|
||||
const newRoleCounts = Object.fromEntries(
|
||||
Object.entries(roleCounts).map(([roleId, count]) => [
|
||||
@@ -16,10 +14,8 @@ export default function relabelQuestionAggregates({
|
||||
);
|
||||
|
||||
const relabeledAggregate: AggregatedQuestionEncounter = {
|
||||
companyCounts,
|
||||
latestSeenAt,
|
||||
locationCounts,
|
||||
roleCounts: newRoleCounts,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return relabeledAggregate;
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import type {
|
||||
City,
|
||||
Company,
|
||||
Country,
|
||||
QuestionsQuestion,
|
||||
QuestionsQuestionVote,
|
||||
State,
|
||||
} 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<{
|
||||
city: City | null;
|
||||
company: Company | null;
|
||||
location: string;
|
||||
country: Country | null;
|
||||
role: string;
|
||||
seenAt: Date;
|
||||
state: State | null;
|
||||
}>;
|
||||
|
||||
type QuestionWithAggregatableData = QuestionsQuestion & {
|
||||
@@ -67,8 +76,8 @@ export function createQuestionWithAggregateData(
|
||||
export function createAggregatedQuestionEncounter(
|
||||
encounters: AggregatableEncounters,
|
||||
): AggregatedQuestionEncounter {
|
||||
const countryCounts: Record<string, CountryInfo> = {};
|
||||
const companyCounts: Record<string, number> = {};
|
||||
const locationCounts: Record<string, number> = {};
|
||||
const roleCounts: Record<string, number> = {};
|
||||
|
||||
let latestSeenAt = encounters[0].seenAt;
|
||||
@@ -77,15 +86,47 @@ export function createAggregatedQuestionEncounter(
|
||||
latestSeenAt =
|
||||
latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
|
||||
|
||||
if (!(encounter.company!.name in companyCounts)) {
|
||||
companyCounts[encounter.company!.name] = 0;
|
||||
if (encounter.company !== null) {
|
||||
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)) {
|
||||
locationCounts[encounter.location] = 0;
|
||||
if (encounter.country !== null) {
|
||||
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)) {
|
||||
roleCounts[encounter.role] = 0;
|
||||
@@ -95,8 +136,8 @@ export function createAggregatedQuestionEncounter(
|
||||
|
||||
return {
|
||||
companyCounts,
|
||||
countryCounts,
|
||||
latestSeenAt,
|
||||
locationCounts,
|
||||
roleCounts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
import type { FilterChoice } from '~/components/questions/filter/FilterSection';
|
||||
|
||||
import { LOCATIONS } from './constants';
|
||||
import { trpc } from '../trpc';
|
||||
|
||||
export default function useDefaultLocation(): FilterChoice | undefined {
|
||||
return LOCATIONS[0];
|
||||
import type { Location } from '~/types/questions';
|
||||
|
||||
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';
|
||||
|
||||
type UseVoteOptions = {
|
||||
createVote: (opts: { vote: Vote }) => void;
|
||||
deleteVote: (opts: { id: string }) => void;
|
||||
updateVote: (opts: BackendVote) => void;
|
||||
setDownVote: () => void;
|
||||
setNoVote: () => void;
|
||||
setUpVote: () => void;
|
||||
};
|
||||
|
||||
type BackendVote = {
|
||||
@@ -19,47 +19,23 @@ const createVoteCallbacks = (
|
||||
vote: BackendVote | null,
|
||||
opts: UseVoteOptions,
|
||||
) => {
|
||||
const { createVote, updateVote, deleteVote } = opts;
|
||||
const { setDownVote, setNoVote, setUpVote } = opts;
|
||||
|
||||
const handleUpvote = () => {
|
||||
// Either upvote or remove upvote
|
||||
if (vote) {
|
||||
if (vote.vote === 'DOWNVOTE') {
|
||||
updateVote({
|
||||
id: vote.id,
|
||||
vote: 'UPVOTE',
|
||||
});
|
||||
} else {
|
||||
deleteVote({
|
||||
id: vote.id,
|
||||
});
|
||||
}
|
||||
// Update vote to an upvote
|
||||
if (vote && vote.vote === 'UPVOTE') {
|
||||
setNoVote();
|
||||
} else {
|
||||
createVote({
|
||||
vote: 'UPVOTE',
|
||||
});
|
||||
setUpVote();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownvote = () => {
|
||||
// Either downvote or remove downvote
|
||||
if (vote) {
|
||||
if (vote.vote === 'UPVOTE') {
|
||||
updateVote({
|
||||
id: vote.id,
|
||||
vote: 'DOWNVOTE',
|
||||
});
|
||||
} else {
|
||||
deleteVote({
|
||||
id: vote.id,
|
||||
});
|
||||
}
|
||||
// Update vote to an upvote
|
||||
if (vote && vote.vote === 'DOWNVOTE') {
|
||||
setNoVote();
|
||||
} else {
|
||||
createVote({
|
||||
vote: 'DOWNVOTE',
|
||||
});
|
||||
setDownVote();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,61 +47,61 @@ type QueryKey = Parameters<typeof trpc.useQuery>[0][0];
|
||||
|
||||
export const useQuestionVote = (id: string) => {
|
||||
return useVote(id, {
|
||||
create: 'questions.questions.user.createVote',
|
||||
deleteKey: 'questions.questions.user.deleteVote',
|
||||
idKey: 'questionId',
|
||||
invalidateKeys: [
|
||||
'questions.questions.getQuestionsByFilter',
|
||||
'questions.questions.getQuestionById',
|
||||
],
|
||||
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) => {
|
||||
return useVote(id, {
|
||||
create: 'questions.answers.user.createVote',
|
||||
deleteKey: 'questions.answers.user.deleteVote',
|
||||
idKey: 'answerId',
|
||||
invalidateKeys: [
|
||||
'questions.answers.getAnswers',
|
||||
'questions.answers.getAnswerById',
|
||||
],
|
||||
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) => {
|
||||
return useVote(id, {
|
||||
create: 'questions.questions.comments.user.createVote',
|
||||
deleteKey: 'questions.questions.comments.user.deleteVote',
|
||||
idKey: 'questionCommentId',
|
||||
invalidateKeys: ['questions.questions.comments.getQuestionComments'],
|
||||
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) => {
|
||||
return useVote(id, {
|
||||
create: 'questions.answers.comments.user.createVote',
|
||||
deleteKey: 'questions.answers.comments.user.deleteVote',
|
||||
idKey: 'answerCommentId',
|
||||
invalidateKeys: ['questions.answers.comments.getAnswerComments'],
|
||||
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> = {
|
||||
create: MutationKey;
|
||||
deleteKey: MutationKey;
|
||||
idKey: string;
|
||||
invalidateKeys: Array<VoteQueryKey>;
|
||||
query: VoteQueryKey;
|
||||
update: MutationKey;
|
||||
setDownVoteKey: MutationKey;
|
||||
setNoVoteKey: MutationKey;
|
||||
setUpVoteKey: MutationKey;
|
||||
};
|
||||
|
||||
type UseVoteMutationContext = {
|
||||
@@ -137,7 +113,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
id: string,
|
||||
opts: VoteProps<VoteQueryKey>,
|
||||
) => {
|
||||
const { create, deleteKey, query, update, idKey, invalidateKeys } = opts;
|
||||
const {
|
||||
idKey,
|
||||
invalidateKeys,
|
||||
query,
|
||||
setDownVoteKey,
|
||||
setNoVoteKey,
|
||||
setUpVoteKey,
|
||||
} = opts;
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const onVoteUpdate = useCallback(() => {
|
||||
@@ -157,8 +140,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
|
||||
const backendVote = data as BackendVote;
|
||||
|
||||
const { mutate: createVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
create,
|
||||
const { mutate: setUpVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
setUpVoteKey,
|
||||
{
|
||||
onError: (err, variables, context) => {
|
||||
if (context !== undefined) {
|
||||
@@ -185,8 +168,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
onSettled: onVoteUpdate,
|
||||
},
|
||||
);
|
||||
const { mutate: updateVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
update,
|
||||
const { mutate: setDownVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
setDownVoteKey,
|
||||
{
|
||||
onError: (error, variables, context) => {
|
||||
if (context !== undefined) {
|
||||
@@ -214,8 +197,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: deleteVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
deleteKey,
|
||||
const { mutate: setNoVote } = trpc.useMutation<any, UseVoteMutationContext>(
|
||||
setNoVoteKey,
|
||||
{
|
||||
onError: (err, variables, context) => {
|
||||
if (context !== undefined) {
|
||||
@@ -242,14 +225,21 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
|
||||
const { handleDownvote, handleUpvote } = createVoteCallbacks(
|
||||
backendVote ?? null,
|
||||
{
|
||||
createVote: ({ vote }) => {
|
||||
createVote({
|
||||
setDownVote: () => {
|
||||
setDownVote({
|
||||
[idKey]: id,
|
||||
vote,
|
||||
} as any);
|
||||
});
|
||||
},
|
||||
setNoVote: () => {
|
||||
setNoVote({
|
||||
[idKey]: id,
|
||||
});
|
||||
},
|
||||
setUpVote: () => {
|
||||
setUpVote({
|
||||
[idKey]: id,
|
||||
});
|
||||
},
|
||||
deleteVote,
|
||||
updateVote,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user