Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d876523
Merge remote-tracking branch 'origin/main' into develop
typeofweb Jan 5, 2023
13bfdbf
fix(app): change logout trigger (#486)
grzegorzpokorski Jan 5, 2023
08ca431
feat(api): add question answers likes and dislikes (#468)
xStrixU Jan 6, 2023
7778dea
fix: voting on single questions
typeofweb Jan 6, 2023
2a09b97
fix: position right section
typeofweb Jan 6, 2023
ceb0fab
fix(app): fix mobile actions z-index (#490)
AdiPol1359 Jan 6, 2023
f04d7b3
fix: make voting button more visible
typeofweb Jan 6, 2023
4bc8dd6
fix(app): fix colors (#493)
grzegorzpokorski Jan 8, 2023
a0ec16d
chore: update contributors (#492)
AdiPol1359 Jan 8, 2023
6b90322
feat(app): add static rendering to single question
AdiPol1359 Jan 8, 2023
0547573
refactor: move revalidation to the server
AdiPol1359 Jan 8, 2023
f8683c6
refactor(api): remove test code
AdiPol1359 Jan 8, 2023
f1d3587
feat(api): add fetch types
AdiPol1359 Jan 8, 2023
3660d0c
feat(app): add answer editor to question answer (#491)
AdiPol1359 Jan 11, 2023
db5824a
Merge branch 'develop' of https://github.com/typeofweb/devfaq into an…
AdiPol1359 Jan 11, 2023
c17d1da
fix(app): fix refetch after delete answer
AdiPol1359 Jan 11, 2023
081bc8f
feat(app): add focus trap to main nav and QuestionsSidebar; add small…
grzegorzpokorski Jan 11, 2023
33a907f
Merge branch 'develop' of https://github.com/typeofweb/devfaq into an…
AdiPol1359 Jan 12, 2023
97a5e87
feat(app): close modal on press Escape key (#497)
grzegorzpokorski Jan 12, 2023
23c55d4
Merge branch 'develop' of https://github.com/typeofweb/devfaq into an…
AdiPol1359 Jan 12, 2023
a0db81e
feat: add editing own questions (#496)
xStrixU Jan 12, 2023
9e453b6
feat(app): add user context menu (#495)
grzegorzpokorski Jan 12, 2023
c171ead
Merge branch 'develop' of https://github.com/typeofweb/devfaq into an…
AdiPol1359 Jan 12, 2023
48deec8
fix(app): fix styles in admin and user panel (#501)
grzegorzpokorski Jan 13, 2023
2c8b610
feat(app): new hook 'useOnKeydown'; implement new hook in 'BaseModal'…
grzegorzpokorski Jan 13, 2023
8f45ba8
Merge branch 'develop' of https://github.com/typeofweb/devfaq into an…
AdiPol1359 Jan 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@
"login": "AdiPol1359",
"name": "Adrian Polak",
"avatar_url": "https://avatars.githubusercontent.com/u/27779154?v=4",
"profile": "https://projectcode.pl/",
"profile": "https://github.com/AdiPol1359",
"contributions": [
"code"
]
},
{
"login": "xStrixU",
"name": "xStrixU",
"name": "Kacper Polak",
"avatar_url": "https://avatars.githubusercontent.com/u/41890821?v=4",
"profile": "https://github.com/xStrixU",
"contributions": [
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"titleBar.inactiveBackground": "#401886",
"titleBar.activeForeground": "#ffffff",
"titleBar.inactiveForeground": "#ffffff"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
}
3 changes: 3 additions & 0 deletions apps/api/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ COOKIE_PASSWORD=blablablblablablblablablblablabl
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

APP_URL=http://app.devfaq.localhost:3000
REVALIDATION_TOKEN=

DATABASE_URL=postgres://postgres:api2022@localhost:54421/database_development
6 changes: 6 additions & 0 deletions apps/api/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export function getConfig(name: "NODE_ENV"): "production" | "development";
export function getConfig(name: "ENV"): "production" | "staging" | "development" | "test";
export function getConfig(name: "GITHUB_CLIENT_ID"): string;
export function getConfig(name: "GITHUB_CLIENT_SECRET"): string;
export function getConfig(name: "APP_URL"): string;
export function getConfig(name: "REVALIDATION_TOKEN"): string;
export function getConfig(name: "GIT_BRANCH"): string;
export function getConfig(name: "GIT_COMMIT_HASH"): string;
export function getConfig(name: "VERSION"): string;
Expand All @@ -21,6 +23,10 @@ export function getConfig(name: string): string | number {
case "GITHUB_CLIENT_ID":
case "GITHUB_CLIENT_SECRET":
return val || "";
case "APP_URL":
return val || "";
case "REVALIDATION_TOKEN":
return val || "";
case "GIT_BRANCH":
return val || "(unknown_branch)";
case "GIT_COMMIT_HASH":
Expand Down
6 changes: 5 additions & 1 deletion apps/api/modules/answers/answers.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ export const dbAnswerToDto = ({
content,
sources,
createdAt,
QuestionAnswerVote,
CreatedBy: { socialLogin, ...createdBy },
}: Prisma.QuestionAnswerGetPayload<{ select: typeof answerSelect }>) => {
_count,
}: Prisma.QuestionAnswerGetPayload<{ select: ReturnType<typeof answerSelect> }>) => {
return {
id,
content,
sources,
createdAt: createdAt.toISOString(),
votesCount: _count.QuestionAnswerVote,
currentUserVotedOn: QuestionAnswerVote.length > 0,
createdBy: {
socialLogin: socialLogin as Record<string, string | number>,
...createdBy,
Expand Down
136 changes: 122 additions & 14 deletions apps/api/modules/answers/answers.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { Prisma } from "@prisma/client";
import { FastifyPluginAsync, preHandlerAsyncHookHandler, preHandlerHookHandler } from "fastify";
import { revalidate } from "../../services/revalidation.service.js";
import { PrismaErrorCode } from "../db/prismaErrors.js";
import { isPrismaError } from "../db/prismaErrors.util.js";
import { dbAnswerToDto } from "./answers.mapper.js";
Expand All @@ -9,17 +10,33 @@ import {
createAnswerSchema,
deleteAnswerSchema,
updateAnswerSchema,
upvoteAnswerSchema,
} from "./answers.schemas.js";

export const answerSelect = {
id: true,
content: true,
sources: true,
createdAt: true,
CreatedBy: {
select: { id: true, firstName: true, lastName: true, socialLogin: true },
},
} satisfies Prisma.QuestionAnswerSelect;
export const answerSelect = (userId: number) => {
return {
id: true,
questionId: true,
content: true,
sources: true,
createdAt: true,
CreatedBy: {
select: { id: true, firstName: true, lastName: true, socialLogin: true },
},
_count: {
select: {
QuestionAnswerVote: true,
},
},
QuestionAnswerVote: {
where: {
userId: userId,
},
},
} satisfies Prisma.QuestionAnswerSelect;
};

const revalidateQuestion = (id: number) => revalidate(`/questions/p/${id}`);

const answersPlugin: FastifyPluginAsync = async (fastify) => {
const checkAnswerUserHook: preHandlerAsyncHookHandler = async (request) => {
Expand Down Expand Up @@ -53,15 +70,16 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
async handler(request) {
const {
params: { id },
session: { data: sessionData },
} = request;

const answers = await fastify.db.questionAnswer.findMany({
where: { questionId: id },
select: answerSelect,
select: answerSelect(request.session.data?._user.id || 0),
});

return {
data: answers.map(dbAnswerToDto),
data: answers.map((answer) => dbAnswerToDto(answer)),
};
},
});
Expand All @@ -84,9 +102,11 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
try {
const answer = await fastify.db.questionAnswer.create({
data: { questionId: id, createdById: sessionData._user.id, content, sources },
select: answerSelect,
select: answerSelect(request.session.data?._user.id || 0),
});

await revalidateQuestion(id);

return { data: dbAnswerToDto(answer) };
} catch (err) {
if (isPrismaError(err) && err.code === PrismaErrorCode.UniqueKeyViolation) {
Expand All @@ -109,14 +129,17 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
const {
params: { id },
body: { content, sources },
session: { data: sessionData },
} = request;

const answer = await fastify.db.questionAnswer.update({
where: { id },
data: { content, sources },
select: answerSelect,
select: answerSelect(request.session.data?._user.id || 0),
});

await revalidateQuestion(answer.questionId);

return { data: dbAnswerToDto(answer) };
},
});
Expand All @@ -131,10 +154,95 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
params: { id },
} = request;

await fastify.db.questionAnswer.delete({
const { questionId } = await fastify.db.questionAnswer.delete({
where: { id },
});

await revalidateQuestion(questionId);

return reply.status(204).send();
},
});

fastify.withTypeProvider<TypeBoxTypeProvider>().route({
url: "/answers/:id/votes",
method: "POST",
schema: upvoteAnswerSchema,
async handler(request, reply) {
const {
params: { id },
session: { data: sessionData },
} = request;

if (!sessionData) {
throw fastify.httpErrors.unauthorized();
}

try {
const questionAnswerVote = await fastify.db.questionAnswerVote.upsert({
where: {
userId_questionAnswerId: {
userId: sessionData._user.id,
questionAnswerId: id,
},
},
create: {
userId: sessionData._user.id,
questionAnswerId: id,
},
update: {
userId: sessionData._user.id,
questionAnswerId: id,
},
});

return {
data: {
userId: sessionData._user.id,
answerId: id,
},
};
} catch (err) {
if (isPrismaError(err) && PrismaErrorCode.ForeignKeyViolation) {
throw fastify.httpErrors.notFound(`Answer vote with id: ${id} not found!`);
}

throw err;
}
},
});

fastify.withTypeProvider<TypeBoxTypeProvider>().route({
url: "/answers/:id/votes",
method: "DELETE",
schema: deleteAnswerSchema,
async handler(request, reply) {
const {
params: { id },
session: { data: sessionData },
} = request;

if (!sessionData) {
throw fastify.httpErrors.unauthorized();
}

const questionAnswer = await fastify.db.questionAnswer.findFirst({
where: {
id,
},
});

if (!questionAnswer) {
throw fastify.httpErrors.notFound(`Answer vote with id: ${id} not found!`);
}

await fastify.db.questionAnswerVote.deleteMany({
where: {
userId: sessionData._user.id,
questionAnswerId: id,
},
});

return reply.status(204).send();
},
});
Expand Down
25 changes: 25 additions & 0 deletions apps/api/modules/answers/answers.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const answerSchema = Type.Object({
content: Type.String(),
sources: Type.Array(Type.String()),
createdAt: Type.String({ format: "date-time" }),
votesCount: Type.Integer(),
currentUserVotedOn: Type.Boolean(),
createdBy: Type.Object({
id: Type.Integer(),
firstName: Type.Union([Type.String(), Type.Null()]),
Expand Down Expand Up @@ -64,3 +66,26 @@ export const deleteAnswerSchema = {
204: Type.Never(),
},
};

export const upvoteAnswerSchema = {
params: Type.Object({
id: Type.Integer(),
}),
response: {
200: Type.Object({
data: Type.Object({
userId: Type.Integer(),
answerId: Type.Integer(),
}),
}),
},
};

export const downvoteAnswerSchema = {
params: Type.Object({
id: Type.Integer(),
}),
response: {
204: Type.Never(),
},
};
12 changes: 11 additions & 1 deletion apps/api/modules/questions/questions.params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { kv } from "../../utils.js";
import { GetQuestionsQuery } from "./questions.schemas.js";

export const getQuestionsPrismaParams = (
{ category, level, status = "accepted", limit, offset, order, orderBy }: GetQuestionsQuery,
{
category,
level,
status = "accepted",
limit,
offset,
order,
orderBy,
userId,
}: GetQuestionsQuery,
userRole: string | undefined,
) => {
const levels = level?.split(",");
Expand All @@ -13,6 +22,7 @@ export const getQuestionsPrismaParams = (
...(category && { categoryId: category }),
...(levels && { levelId: { in: levels } }),
...(status && userRole === "admin" ? { statusId: status } : { statusId: "accepted" }),
...(userId && { createdById: userId }),
},
take: limit,
skip: offset,
Expand Down
1 change: 1 addition & 0 deletions apps/api/modules/questions/questions.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const generateGetQuestionsQuerySchema = <
Type.Literal("votesCount"),
]),
order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
userId: Type.Integer(),
}),
);
export type GetQuestionsQuery = Static<ReturnType<typeof generateGetQuestionsQuerySchema>>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "QuestionAnswerVote" (
"_userId" INTEGER NOT NULL,
"_questionAnswerId" INTEGER NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "QuestionAnswerVote_pkey" PRIMARY KEY ("_userId","_questionAnswerId")
);

-- AddForeignKey
ALTER TABLE "QuestionAnswerVote" ADD CONSTRAINT "QuestionAnswerVote__questionAnswerId_fkey" FOREIGN KEY ("_questionAnswerId") REFERENCES "QuestionAnswer"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "QuestionAnswerVote" ADD CONSTRAINT "QuestionAnswerVote__userId_fkey" FOREIGN KEY ("_userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Loading