diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx index 73faba27..dc7d2836 100644 --- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -7,7 +7,7 @@ import { import { TextInput } from '@tih/ui'; import ContributeQuestionDialog from './ContributeQuestionDialog'; -import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; +import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm'; export type ContributeQuestionCardProps = Pick< ContributeQuestionFormProps, diff --git a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx index 2696bd57..dd548d37 100644 --- a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx @@ -2,9 +2,9 @@ import { Fragment, useState } from 'react'; import { Dialog, Transition } from '@headlessui/react'; import { HorizontalDivider } from '@tih/ui'; -import type { ContributeQuestionFormProps } from './ContributeQuestionForm'; -import ContributeQuestionForm from './ContributeQuestionForm'; import DiscardDraftDialog from './DiscardDraftDialog'; +import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm'; +import ContributeQuestionForm from './forms/ContributeQuestionForm'; export type ContributeQuestionDialogProps = Pick< ContributeQuestionFormProps, @@ -60,7 +60,7 @@ export default function ContributeQuestionDialog({ leave="ease-in duration-200" leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> - +
diff --git a/apps/portal/src/components/questions/card/FullQuestionCard.tsx b/apps/portal/src/components/questions/card/FullQuestionCard.tsx index 2c3c79ef..2e86d6a0 100644 --- a/apps/portal/src/components/questions/card/FullQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/FullQuestionCard.tsx @@ -4,13 +4,15 @@ import QuestionCard from './QuestionCard'; export type QuestionOverviewCardProps = Omit< QuestionCardProps & { showActionButton: false; - showUserStatistics: false; + showAnswerStatistics: false; + showReceivedStatistics: true; showVoteButtons: true; }, | 'actionButtonLabel' | 'onActionButtonClick' | 'showActionButton' - | 'showUserStatistics' + | 'showAnswerStatistics' + | 'showReceivedStatistics' | 'showVoteButtons' >; @@ -19,7 +21,8 @@ export default function FullQuestionCard(props: QuestionOverviewCardProps) { ); diff --git a/apps/portal/src/components/questions/card/QuestionCard.tsx b/apps/portal/src/components/questions/card/QuestionCard.tsx index 9677fc5f..bcec3f21 100644 --- a/apps/portal/src/components/questions/card/QuestionCard.tsx +++ b/apps/portal/src/components/questions/card/QuestionCard.tsx @@ -1,9 +1,15 @@ -import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'; +import { useState } from 'react'; +import { + ChatBubbleBottomCenterTextIcon, + EyeIcon, +} from '@heroicons/react/24/outline'; import type { QuestionsQuestionType } from '@prisma/client'; import { Badge, Button } from '@tih/ui'; import { useQuestionVote } from '~/utils/questions/useVote'; +import type { CreateQuestionEncounterData } from '../forms/CreateQuestionEncounterForm'; +import CreateQuestionEncounterForm from '../forms/CreateQuestionEncounterForm'; import QuestionTypeBadge from '../QuestionTypeBadge'; import VotingButtons from '../VotingButtons'; @@ -17,14 +23,14 @@ type UpvoteProps = upvoteCount?: never; }; -type StatisticsProps = +type AnswerStatisticsProps = | { answerCount: number; - showUserStatistics: true; + showAnswerStatistics: true; } | { answerCount?: never; - showUserStatistics?: false; + showAnswerStatistics?: false; }; type ActionButtonProps = @@ -39,14 +45,26 @@ type ActionButtonProps = showActionButton?: false; }; +type ReceivedStatisticsProps = + | { + onReceivedSubmit: (data: CreateQuestionEncounterData) => void; + receivedCount: number; + showReceivedStatistics: true; + } + | { + onReceivedSubmit?: never; + receivedCount?: never; + showReceivedStatistics?: false; + }; + export type QuestionCardProps = ActionButtonProps & - StatisticsProps & + AnswerStatisticsProps & + ReceivedStatisticsProps & UpvoteProps & { company: string; content: string; location: string; questionId: string; - receivedCount: number; role: string; showHover?: boolean; timestamp: string; @@ -58,10 +76,11 @@ export default function QuestionCard({ company, answerCount, content, - // ReceivedCount, + receivedCount, type, showVoteButtons, - showUserStatistics, + showAnswerStatistics, + showReceivedStatistics, showActionButton, actionButtonLabel, onActionButtonClick, @@ -70,12 +89,24 @@ export default function QuestionCard({ role, location, showHover, + onReceivedSubmit, }: QuestionCardProps) { + const [showReceivedForm, setShowReceivedForm] = useState(false); const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId); const hoverClass = showHover ? 'hover:bg-slate-50' : ''; - return ( -
+ + const cardContent = showReceivedForm ? ( + { + setShowReceivedForm(false); + }} + onSubmit={(data) => { + onReceivedSubmit?.(data); + setShowReceivedForm(false); + }} + /> + ) : ( + <> {showVoteButtons && (

{content}

- {showUserStatistics && ( + {(showAnswerStatistics || showReceivedStatistics) && (
-
)}
+ + ); + + return ( +
+ {cardContent}
); } diff --git a/apps/portal/src/components/questions/card/QuestionOverviewCard.tsx b/apps/portal/src/components/questions/card/QuestionOverviewCard.tsx index 9bb165ea..ff4708f9 100644 --- a/apps/portal/src/components/questions/card/QuestionOverviewCard.tsx +++ b/apps/portal/src/components/questions/card/QuestionOverviewCard.tsx @@ -6,13 +6,15 @@ import QuestionCard from './QuestionCard'; export type QuestionOverviewCardProps = Omit< QuestionCardProps & { showActionButton: false; - showUserStatistics: true; + showAnswerStatistics: true; + showReceivedStatistics: true; showVoteButtons: true; }, | 'actionButtonLabel' | 'onActionButtonClick' | 'showActionButton' - | 'showUserStatistics' + | 'showAnswerStatistics' + | 'showReceivedStatistics' | 'showVoteButtons' >; @@ -21,8 +23,9 @@ function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) { ); diff --git a/apps/portal/src/components/questions/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx similarity index 94% rename from apps/portal/src/components/questions/ContributeQuestionForm.tsx rename to apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx index 73cca0fc..fa8c4409 100644 --- a/apps/portal/src/components/questions/ContributeQuestionForm.tsx +++ b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx @@ -17,9 +17,11 @@ import { useSelectRegister, } from '~/utils/questions/useFormRegister'; -import CompaniesTypeahead from '../shared/CompaniesTypeahead'; -import type { Month } from '../shared/MonthYearPicker'; -import MonthYearPicker from '../shared/MonthYearPicker'; +import LocationTypeahead from '../typeahead/LocationTypeahead'; +import RoleTypeahead from '../typeahead/RoleTypeahead'; +import CompaniesTypeahead from '../../shared/CompaniesTypeahead'; +import type { Month } from '../../shared/MonthYearPicker'; +import MonthYearPicker from '../../shared/MonthYearPicker'; export type ContributeQuestionData = { company: string; @@ -86,9 +88,7 @@ export default function ContributeQuestionForm({ control={control} name="location" render={({ field }) => ( - {}} @@ -142,9 +142,7 @@ export default function ContributeQuestionForm({ control={control} name="role" render={({ field }) => ( - {}} diff --git a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx new file mode 100644 index 00000000..b753ccbe --- /dev/null +++ b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx @@ -0,0 +1,110 @@ +import { startOfMonth } from 'date-fns'; +import { useState } from 'react'; +import { Button } from '@tih/ui'; + +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { Month } from '~/components/shared/MonthYearPicker'; +import MonthYearPicker from '~/components/shared/MonthYearPicker'; + +import LocationTypeahead from '../typeahead/LocationTypeahead'; +import RoleTypeahead from '../typeahead/RoleTypeahead'; + +export type CreateQuestionEncounterData = { + company: string; + location: string; + role: string; + seenAt: Date; +}; + +export type CreateQuestionEncounterFormProps = { + onCancel: () => void; + onSubmit: (data: CreateQuestionEncounterData) => void; +}; + +export default function CreateQuestionEncounterForm({ + onCancel, + onSubmit, +}: CreateQuestionEncounterFormProps) { + const [selectedCompany, setSelectedCompany] = useState(null); + const [selectedLocation, setSelectedLocation] = useState(null); + const [selectedRole, setSelectedRole] = useState(null); + const [selectedDate, setSelectedDate] = useState( + startOfMonth(new Date()), + ); + + return ( +
+
+
+ ); +} diff --git a/apps/portal/src/components/questions/typeahead/LocationTypeahead.tsx b/apps/portal/src/components/questions/typeahead/LocationTypeahead.tsx new file mode 100644 index 00000000..dd2a5b2d --- /dev/null +++ b/apps/portal/src/components/questions/typeahead/LocationTypeahead.tsx @@ -0,0 +1,12 @@ +import type { ComponentProps } from 'react'; +import { Typeahead } from '@tih/ui'; + +import { LOCATIONS } from '~/utils/questions/constants'; + +type TypeaheadProps = ComponentProps; + +export type LocationTypeaheadProps = Omit; + +export default function LocationTypeahead(props: LocationTypeaheadProps) { + return ; +} diff --git a/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx b/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx new file mode 100644 index 00000000..cb260125 --- /dev/null +++ b/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx @@ -0,0 +1,12 @@ +import type { ComponentProps } from 'react'; +import { Typeahead } from '@tih/ui'; + +import { ROLES } from '~/utils/questions/constants'; + +type TypeaheadProps = ComponentProps; + +export type RoleTypeaheadProps = Omit; + +export default function RoleTypeahead(props: RoleTypeaheadProps) { + return ; +} diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index e07227ff..439f2fd9 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -45,6 +45,11 @@ export default function QuestionPage() { { id: questionId as string }, ]); + const { data: aggregatedEncounters } = trpc.useQuery([ + 'questions.questions.encounters.getAggregatedEncounters', + { questionId: questionId as string }, + ]); + const utils = trpc.useContext(); const { data: comments } = trpc.useQuery([ @@ -74,6 +79,18 @@ export default function QuestionPage() { }, }); + const { mutate: addEncounter } = trpc.useMutation( + 'questions.questions.encounters.create', + { + onSuccess: () => { + utils.invalidateQueries( + 'questions.questions.encounters.getAggregatedEncounters', + ); + utils.invalidateQueries('questions.questions.getQuestionById'); + }, + }, + ); + const handleBackNavigation = () => { router.back(); }; @@ -114,12 +131,21 @@ export default function QuestionPage() { { + addEncounter({ + companyId: data.company, + location: data.location, + questionId: questionId as string, + role: data.role, + seenAt: data.seenAt, + }); + }} />
@@ -180,7 +206,7 @@ export default function QuestionPage() { authorName={comment.user} content={comment.content} createdAt={comment.createdAt} - upvoteCount={0} + upvoteCount={comment.numVotes} /> ))} diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx index 5f74c523..f3ef3c49 100644 --- a/apps/portal/src/pages/questions/index.tsx +++ b/apps/portal/src/pages/questions/index.tsx @@ -381,14 +381,17 @@ export default function QuestionsHomePage() { )}`} location={question.location} questionId={question.id} - receivedCount={0} + receivedCount={question.receivedCount} role={question.role} timestamp={question.seenAt.toLocaleDateString(undefined, { month: 'short', year: 'numeric', - })} // TODO: Implement received count + })} type={question.type} upvoteCount={question.numVotes} + onReceivedSubmit={(data) => { + console.log(data); + }} /> ))} {questions?.length === 0 && ( diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index c3046659..99e32b83 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -10,6 +10,7 @@ import { protectedExampleRouter } from './protected-example-router'; import { questionsAnswerCommentRouter } from './questions-answer-comment-router'; import { questionsAnswerRouter } from './questions-answer-router'; import { questionsQuestionCommentRouter } from './questions-question-comment-router'; +import { questionsQuestionEncounterRouter } from './questions-question-encounter-router'; import { questionsQuestionRouter } from './questions-question-router'; import { resumeCommentsRouter } from './resumes/resumes-comments-router'; import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router'; @@ -40,6 +41,7 @@ export const appRouter = createRouter() .merge('questions.answers.comments.', questionsAnswerCommentRouter) .merge('questions.answers.', questionsAnswerRouter) .merge('questions.questions.comments.', questionsQuestionCommentRouter) + .merge('questions.questions.encounters.', questionsQuestionEncounterRouter) .merge('questions.questions.', questionsQuestionRouter) .merge('offers.', offersRouter) .merge('offers.profile.', offersProfileRouter) diff --git a/apps/portal/src/server/router/questions-question-encounter-router.ts b/apps/portal/src/server/router/questions-question-encounter-router.ts index 2894fdf4..1f328dc6 100644 --- a/apps/portal/src/server/router/questions-question-encounter-router.ts +++ b/apps/portal/src/server/router/questions-question-encounter-router.ts @@ -11,46 +11,46 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() questionId: z.string(), }), async resolve({ ctx, input }) { - const questionEncountersData = await ctx.prisma.questionsQuestionEncounter.findMany({ - include: { - company : true, - }, - where: { - ...input, - }, - }); + const questionEncountersData = + await ctx.prisma.questionsQuestionEncounter.findMany({ + include: { + company: true, + }, + where: { + ...input, + }, + }); const companyCounts: Record = {}; const locationCounts: Record = {}; - const roleCounts:Record = {}; + const roleCounts: Record = {}; for (let i = 0; i < questionEncountersData.length; i++) { const encounter = questionEncountersData[i]; if (!(encounter.company!.name in companyCounts)) { - companyCounts[encounter.company!.name] = 1; + companyCounts[encounter.company!.name] = 1; } companyCounts[encounter.company!.name] += 1; if (!(encounter.location in locationCounts)) { - locationCounts[encounter.location] = 1; + locationCounts[encounter.location] = 1; } locationCounts[encounter.location] += 1; if (!(encounter.role in roleCounts)) { - roleCounts[encounter.role] = 1; + roleCounts[encounter.role] = 1; } roleCounts[encounter.role] += 1; - } - const questionEncounter:AggregatedQuestionEncounter = { + const questionEncounter: AggregatedQuestionEncounter = { companyCounts, locationCounts, roleCounts, - } + }; return questionEncounter; - } + }, }) .mutation('create', { input: z.object({ @@ -58,7 +58,7 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() location: z.string(), questionId: z.string(), role: z.string(), - seenAt: z.date() + seenAt: z.date(), }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; @@ -83,11 +83,12 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const questionEncounterToUpdate = await ctx.prisma.questionsQuestionEncounter.findUnique({ - where: { - id: input.id, - }, - }); + const questionEncounterToUpdate = + await ctx.prisma.questionsQuestionEncounter.findUnique({ + where: { + id: input.id, + }, + }); if (questionEncounterToUpdate?.id !== userId) { throw new TRPCError({ @@ -113,11 +114,12 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const questionEncounterToDelete = await ctx.prisma.questionsQuestionEncounter.findUnique({ - where: { - id: input.id, - }, - }); + const questionEncounterToDelete = + await ctx.prisma.questionsQuestionEncounter.findUnique({ + where: { + id: input.id, + }, + }); if (questionEncounterToDelete?.id !== userId) { throw new TRPCError({ @@ -132,4 +134,4 @@ export const questionsQuestionEncounterRouter = createProtectedRouter() }, }); }, - }); \ No newline at end of file + }); diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts index fb855434..cd1c4d34 100644 --- a/apps/portal/src/server/router/questions-question-router.ts +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -107,6 +107,7 @@ export const questionsQuestionRouter = createProtectedRouter() numAnswers: data._count.answers, numComments: data._count.comments, numVotes: votes, + receivedCount: data.encounters.length, role: data.encounters[0].role ?? 'Unknown role', seenAt: data.encounters[0].seenAt, type: data.questionType, @@ -180,6 +181,7 @@ export const questionsQuestionRouter = createProtectedRouter() numAnswers: questionData._count.answers, numComments: questionData._count.comments, numVotes: votes, + receivedCount: questionData.encounters.length, role: questionData.encounters[0].role ?? 'Unknown role', seenAt: questionData.encounters[0].seenAt, type: questionData.questionType, diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 521f3b8b..45141d3c 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -9,6 +9,7 @@ export type Question = { numAnswers: number; numComments: number; numVotes: number; + receivedCount: number; role: string; seenAt: Date; type: QuestionsQuestionType; @@ -20,7 +21,7 @@ export type AggregatedQuestionEncounter = { companyCounts: Record; locationCounts: Record; roleCounts: Record; -} +}; export type AnswerComment = { content: string;