Skip to content

Commit

Permalink
feat(app): add questions answers (#466)
Browse files Browse the repository at this point in the history
* feat(app): add questions answers

* refactor(api): remove createdAt date from user

* feat(app): add answer sources

* refactor(app): change delete source icon to button

* refactor: some changes
  • Loading branch information
AdiPol1359 committed Jan 1, 2023
1 parent 3f66263 commit 6145944
Show file tree
Hide file tree
Showing 35 changed files with 524 additions and 111 deletions.
4 changes: 4 additions & 0 deletions apps/api/modules/answers/answers.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import { answerSelect } from "./answers.routes";
export const dbAnswerToDto = ({
id,
content,
sources,
createdAt,
CreatedBy: { socialLogin, ...createdBy },
}: Prisma.QuestionAnswerGetPayload<{ select: typeof answerSelect }>) => {
return {
id,
content,
sources,
createdAt: createdAt.toISOString(),
createdBy: {
socialLogin: socialLogin as Record<string, string | number>,
...createdBy,
Expand Down
10 changes: 6 additions & 4 deletions apps/api/modules/answers/answers.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
export const answerSelect = {
id: true,
content: true,
sources: true,
createdAt: true,
CreatedBy: {
select: { id: true, firstName: true, lastName: true, socialLogin: true },
},
Expand Down Expand Up @@ -72,7 +74,7 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
const {
session: { data: sessionData },
params: { id },
body: { content },
body: { content, sources },
} = request;

if (!sessionData) {
Expand All @@ -81,7 +83,7 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {

try {
const answer = await fastify.db.questionAnswer.create({
data: { content, questionId: id, createdById: sessionData._user.id },
data: { questionId: id, createdById: sessionData._user.id, content, sources },
select: answerSelect,
});

Expand All @@ -106,12 +108,12 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
async handler(request) {
const {
params: { id },
body: { content },
body: { content, sources },
} = request;

const answer = await fastify.db.questionAnswer.update({
where: { id },
data: { content },
data: { content, sources },
select: answerSelect,
});

Expand Down
12 changes: 9 additions & 3 deletions apps/api/modules/answers/answers.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Type } from "@sinclair/typebox";
const answerSchema = Type.Object({
id: Type.Number(),
content: Type.String(),
sources: Type.Array(Type.String()),
createdAt: Type.String({ format: "date-time" }),
createdBy: Type.Object({
id: Type.Integer(),
firstName: Type.Union([Type.String(), Type.Null()]),
Expand All @@ -28,6 +30,7 @@ export const createAnswerSchema = {
}),
body: Type.Object({
content: Type.String(),
sources: Type.Array(Type.String()),
}),
response: {
200: Type.Object({
Expand All @@ -40,9 +43,12 @@ export const updateAnswerSchema = {
params: Type.Object({
id: Type.Integer(),
}),
body: Type.Object({
content: Type.Optional(Type.String()),
}),
body: Type.Partial(
Type.Object({
content: Type.String(),
sources: Type.Array(Type.String()),
}),
),
response: {
200: Type.Object({
data: answerSchema,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "QuestionAnswer" ADD COLUMN "sources" TEXT[] DEFAULT ARRAY[]::TEXT[];
1 change: 1 addition & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ model QuestionAnswer {
createdById Int @map("_createdById")
questionId Int @map("_questionId")
content String
sources String[] @default([])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt() @db.Timestamptz(6)
CreatedBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
Expand Down
7 changes: 7 additions & 0 deletions apps/app/public/icons/bin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 22 additions & 8 deletions apps/app/src/app/(main-layout)/questions/p/[questionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { redirect } from "next/navigation";
import { serializeSource } from "../../../../../lib/markdown";
import { QuestionAnswers } from "../../../../../components/QuestionAnswers/QuestionAnswers";
import { SingleQuestion } from "../../../../../components/SingleQuestion";
import { serializeQuestionToMarkdown } from "../../../../../lib/question";
import { getQuestionById } from "../../../../../services/questions.service";
import { getQuestionAnswers, getQuestionById } from "../../../../../services/questions.service";
import { Params } from "../../../../../types";

export default async function SingleQuestionPage({ params }: { params: Params<"questionId"> }) {
Expand All @@ -11,13 +13,25 @@ export default async function SingleQuestionPage({ params }: { params: Params<"q
return redirect("/");
}

const {
data: { data },
} = await getQuestionById({
id: questionId,
});
const [questionData, answersData] = await Promise.all([
getQuestionById({
id: questionId,
}),
getQuestionAnswers({ id: questionId }),
]);

const question = await serializeQuestionToMarkdown(data);
const question = await serializeQuestionToMarkdown(questionData.data.data);
const answers = await Promise.all(
answersData.data.data.map(async ({ content, ...rest }) => {
const mdxContent = await serializeSource(content);
return { mdxContent, ...rest };
}),
);

return <SingleQuestion question={question} />;
return (
<>
<SingleQuestion question={question} />
<QuestionAnswers questionId={questionId} answers={answers} />
</>
);
}
2 changes: 1 addition & 1 deletion apps/app/src/components/AddQuestionConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { ComponentProps } from "react";
import Logo from "../../public/typeofweb-logo.svg";
import { useUIContext } from "../providers/UIProvider";
import { BaseModal } from "./BaseModal";
import { BaseModal } from "./BaseModal/BaseModal";
import { Button } from "./Button/Button";

export const AddQuestionConfirmationModal = (props: ComponentProps<typeof BaseModal>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

import { ChangeEvent, ComponentProps, useEffect, useState } from "react";
import type { FormEvent } from "react";
import { twMerge } from "tailwind-merge";
import { useUIContext } from "../../providers/UIProvider";
import { technologiesLabel, Technology, validateTechnology } from "../../lib/technologies";
import { Level, levels, validateLevel } from "../../lib/level";
import { BaseModal } from "../BaseModal";
import { Button } from "../Button/Button";
import { Select } from "../Select/Select";
import { useQuestionMutation } from "../../hooks/useQuestionMutation";
import { QuestionEditor } from "./QuestionEditor/QuestionEditor";
import { useUIContext } from "../providers/UIProvider";
import { technologiesLabel, Technology, validateTechnology } from "../lib/technologies";
import { Level, levels, validateLevel } from "../lib/level";
import { useQuestionMutation } from "../hooks/useQuestionMutation";
import { BaseModal } from "./BaseModal/BaseModal";
import { Button } from "./Button/Button";
import { Select } from "./Select/Select";
import { WysiwygEditor } from "./WysiwygEditor/WysiwygEditor";

type SelectDataState = Readonly<{
technology?: Technology;
Expand All @@ -28,7 +27,7 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
const disabled =
!selectData.technology ||
!selectData.level ||
content.length === 0 ||
content.trim().length === 0 ||
createQuestionMutation.isLoading;

useEffect(() => {
Expand Down Expand Up @@ -81,9 +80,7 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {

return (
<BaseModal {...props}>
<h2 className="text-center text-xl font-bold uppercase text-primary dark:text-neutral-200">
{modalData ? "Edytuj" : "Nowe"} pytanie
</h2>
<BaseModal.Title>{modalData ? "Edytuj" : "Nowe"} pytanie</BaseModal.Title>
<form onSubmit={handleFormSubmit}>
<div className="mt-10 flex flex-col gap-y-3 px-5 sm:flex-row sm:justify-evenly sm:gap-x-5">
<label className="flex w-full flex-col gap-2">
Expand Down Expand Up @@ -123,30 +120,16 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
</Select>
</label>
</div>
<QuestionEditor value={content} onChange={setContent} />
<div className="mt-3 flex flex-col gap-2 sm:flex-row-reverse">
<WysiwygEditor value={content} onChange={setContent} />
<BaseModal.Footer>
<Button type="submit" variant="brandingInverse" disabled={disabled}>
{modalData ? "Edytuj" : "Dodaj"} pytanie
</Button>
<Button type="button" variant="branding" onClick={closeModal}>
Anuluj
</Button>
</div>
<p
className={twMerge("-mb-10 mt-3 text-red-600", isError ? "visible" : "invisible")}
role="alert"
>
⚠️ Wystąpił nieoczekiwany błąd przy dodawaniu pytania. Spróbuj ponownie, a jeśli problem
będzie się powtarzał,{" "}
<a
href="https://discord.com/invite/va2NhBv"
className="underline"
target="_blank"
rel="noreferrer"
>
skontaktuj się z administracją.
</a>
</p>
</BaseModal.Footer>
<BaseModal.Error visible={isError} />
</form>
</BaseModal>
);
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions apps/app/src/components/AppModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import type { ComponentProps, ComponentType } from "react";
import { useUIContext } from "../providers/UIProvider";
import type { Modal } from "../providers/UIProvider";
import { AddQuestionModal } from "./AddQuestionModal/AddQuestionModal";
import { AddQuestionModal } from "./AddQuestionModal";
import { AddQuestionConfirmationModal } from "./AddQuestionConfirmationModal";
import { BaseModal } from "./BaseModal";
import { BaseModal } from "./BaseModal/BaseModal";

const modals: Record<Modal, ComponentType<ComponentProps<typeof BaseModal>>> = {
AddQuestionModal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import { ReactNode, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { lockScroll, unlockScroll } from "../utils/pageScroll";
import { useUIContext } from "../providers/UIProvider";
import { CloseButton } from "./CloseButton/CloseButton";
import { lockScroll, unlockScroll } from "../../utils/pageScroll";
import { useUIContext } from "../../providers/UIProvider";
import { CloseButton } from "../CloseButton/CloseButton";
import { ModalTitle } from "./ModalTitle";
import { ModalFooter } from "./ModalFooter";
import { ModalError } from "./ModalError";

type BaseModalProps = Readonly<{
isOpen: boolean;
Expand Down Expand Up @@ -58,3 +61,7 @@ export const BaseModal = ({ isOpen, onClose, children }: BaseModalProps) => {
</Transition>
);
};

BaseModal.Title = ModalTitle;
BaseModal.Footer = ModalFooter;
BaseModal.Error = ModalError;
7 changes: 7 additions & 0 deletions apps/app/src/components/BaseModal/ModalError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ComponentProps, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { Error } from "../Error";

export const ModalError = ({ className, ...props }: ComponentProps<typeof Error>) => (
<Error className={twMerge("-mb-10 mt-3", className)} {...props} />
);
9 changes: 9 additions & 0 deletions apps/app/src/components/BaseModal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactNode } from "react";

type ModalFooterProps = Readonly<{
children: ReactNode;
}>;

export const ModalFooter = ({ children }: ModalFooterProps) => (
<div className="mt-3 flex flex-col gap-2 sm:flex-row-reverse">{children}</div>
);
11 changes: 11 additions & 0 deletions apps/app/src/components/BaseModal/ModalTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ReactNode } from "react";

type ModalTitleProps = Readonly<{
children: ReactNode;
}>;

export const ModalTitle = ({ children }: ModalTitleProps) => (
<h2 className="text-center text-xl font-bold uppercase text-primary dark:text-neutral-200">
{children}
</h2>
);
27 changes: 27 additions & 0 deletions apps/app/src/components/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { twMerge } from "tailwind-merge";

type ErrorProps = Readonly<{
visible: boolean;
className?: string;
}>;

export const Error = ({ visible, className }: ErrorProps) => (
<p
role="alert"
className={twMerge(
"text-right text-sm text-red-600",
visible ? "visible" : "invisible",
className,
)}
>
⚠️ Wystąpił nieoczekiwany błąd. Spróbuj ponownie, a jeśli problem będzie się powtarzał,{" "}
<a
href="https://discord.com/invite/va2NhBv"
className="underline"
target="_blank"
rel="noreferrer"
>
skontaktuj się z administracją.
</a>
</p>
);
24 changes: 24 additions & 0 deletions apps/app/src/components/GitHubAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Image from "next/image";
import { ComponentProps } from "react";
import { User } from "../types";

type GitHubAvatarProps = Readonly<{
user: Pick<User, "id" | "firstName" | "lastName" | "socialLogin">;
}> &
Omit<ComponentProps<typeof Image>, "src" | "alt">;

export const GitHubAvatar = ({
user: { id, firstName, lastName, socialLogin },
...props
}: GitHubAvatarProps) => {
if (!socialLogin.github) {
return null;
}

const avatarUrl = `https://avatars.githubusercontent.com/u/${socialLogin.github}`;
const alt = firstName
? `Avatar of ${firstName} ${lastName || ""}`.trim()
: `Avatar of user ${id}`;

return <Image src={avatarUrl} alt={alt} {...props} />;
};
Loading

0 comments on commit 6145944

Please sign in to comment.