Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
3660d0c
feat(app): add answer editor to question answer (#491)
AdiPol1359 Jan 11, 2023
081bc8f
feat(app): add focus trap to main nav and QuestionsSidebar; add small…
grzegorzpokorski Jan 11, 2023
97a5e87
feat(app): close modal on press Escape key (#497)
grzegorzpokorski 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
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
9d7ae9e
feat: add ability to sort questions by update / edit date
grzegorzpokorski Jan 22, 2023
51ee25c
feat: add ability to sort questions in user and admin dashboard
grzegorzpokorski Jan 23, 2023
41778f1
Fix: 'CloseButton' a11y improvements (#505)
grzegorzpokorski Jan 23, 2023
57c9b9f
Update apps/app/src/hooks/useGetAllQuestions.ts
grzegorzpokorski Jan 23, 2023
e708de7
Merge branch 'develop' into 507-issue
grzegorzpokorski Jan 23, 2023
b3b68ca
add "SelectLabel" component
grzegorzpokorski Jan 23, 2023
764b5c6
add "SortBySelect" component
grzegorzpokorski Jan 23, 2023
89c1b67
implement newly created components
grzegorzpokorski Jan 23, 2023
a18b490
rename sortByLabels
grzegorzpokorski Jan 23, 2023
3ef42b0
Fix 'sortByLabels' labels
grzegorzpokorski Jan 23, 2023
ab8e183
feat(api): add simple "/answers" endpoint to fetch all answers
grzegorzpokorski Jan 24, 2023
9b4bd00
style(api): rename "CreateBy" => "createdBy in "/answers" endpoint"
grzegorzpokorski Jan 24, 2023
87a9408
feat(api): add "votesCount" field to "/answers" response
grzegorzpokorski Jan 24, 2023
320979c
fix(api): move "/answers" schema to separate file
grzegorzpokorski Jan 24, 2023
0de234a
feat(api): add "socialLogin" field to "/amswers" response
grzegorzpokorski Jan 24, 2023
7c98b84
feat(app): add ability to edit question on questions page and single …
grzegorzpokorski Jan 24, 2023
f153bff
remove console.log
grzegorzpokorski Jan 29, 2023
a1ce3bb
feat(api): handle 'sortBy', 'sort', 'limit', 'offset' query params in…
grzegorzpokorski Jan 29, 2023
d6e9b15
Merge branch 'add-answers-endpoint' into 478-admin-deshboard-for-mana…
grzegorzpokorski Jan 29, 2023
2c1116f
feat(app): add '/answers' page skeleton
grzegorzpokorski Jan 29, 2023
dc52c62
Merge branch 'develop' into 507-issue
grzegorzpokorski Jan 29, 2023
8b60de2
feat(api): add count of all answers to each "/answers" response
grzegorzpokorski Jan 29, 2023
7d0e804
regenerate open-api types
grzegorzpokorski Jan 29, 2023
6c1e67f
feat(app): add 'useGetAllAnswers' hook and answers.service
grzegorzpokorski Jan 29, 2023
d5f46f0
feat(app): add "AnswersDashboars" and modify some of the reusable com…
grzegorzpokorski Jan 29, 2023
ea65d38
add AnswersDashboadHedaer skeleton
grzegorzpokorski Jan 29, 2023
4ee0973
Revert "add AnswersDashboadHedaer skeleton"
grzegorzpokorski Jan 29, 2023
0d7721e
Merge branch '507-issue' into 478-admin-dashboard-for-managing-answer…
grzegorzpokorski Jan 30, 2023
4f00711
move 'sortByLabels' out of the 'SortBySelect' component
grzegorzpokorski Jan 30, 2023
9373b6a
feat(app): add hedaer with filter to AnswersDashboard
grzegorzpokorski Jan 30, 2023
49bb821
add loader version for answers in AnswersDashboard
grzegorzpokorski Jan 30, 2023
a2541ae
fix(app): switch edit mode to false after deleting answer
grzegorzpokorski Jan 30, 2023
c88de9c
change when answers list are refetching onSubmit in "AnswersForm"
grzegorzpokorski Jan 30, 2023
ff66152
make unable to save empty answer in edit mode
grzegorzpokorski Jan 30, 2023
9a7f190
remove comments
grzegorzpokorski Jan 30, 2023
420ffe3
fix searchParams in /answers
grzegorzpokorski Jan 30, 2023
92049bc
make unable to add empty answer
grzegorzpokorski Jan 30, 2023
156bc81
remove unused imports
grzegorzpokorski Jan 30, 2023
8d21ffb
set whitespace to nowrap for CtaHeader links
grzegorzpokorski Jan 30, 2023
3c5eb70
adjust position of info text in dashboards
grzegorzpokorski Jan 30, 2023
37d3c08
remove unused import
grzegorzpokorski Jan 30, 2023
dc4844b
remove duplicated styles
grzegorzpokorski Jan 31, 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
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
"titleBar.inactiveBackground": "#401886",
"titleBar.activeForeground": "#ffffff",
"titleBar.inactiveForeground": "#ffffff"
}
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"prettier.configPath": "prettier.config.js"
}
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
22 changes: 22 additions & 0 deletions apps/api/modules/answers/answers.params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Prisma } from "@prisma/client";
import { kv } from "../../utils.js";
import { GetAnswersQuery } from "./answers.schemas";

export const getAnswersPrismaParams = ({ limit, offset, order, orderBy }: GetAnswersQuery) => {
return {
take: limit,
skip: offset,
...(order &&
orderBy && {
orderBy: {
...(orderBy === "votesCount"
? {
QuestionAnswerVote: {
_count: order,
},
}
: kv(orderBy, order)),
},
}),
} satisfies Prisma.QuestionAnswerFindManyArgs;
};
180 changes: 165 additions & 15 deletions apps/api/modules/answers/answers.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,37 @@ import { FastifyPluginAsync, preHandlerAsyncHookHandler, preHandlerHookHandler }
import { PrismaErrorCode } from "../db/prismaErrors.js";
import { isPrismaError } from "../db/prismaErrors.util.js";
import { dbAnswerToDto } from "./answers.mapper.js";
import { getAnswersPrismaParams } from "./answers.params.js";
import {
getAnswersSchema,
getAnswersRelatedToPostSchema,
createAnswerSchema,
deleteAnswerSchema,
updateAnswerSchema,
upvoteAnswerSchema,
getAnswersSchema,
} 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,
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 answersPlugin: FastifyPluginAsync = async (fastify) => {
const checkAnswerUserHook: preHandlerAsyncHookHandler = async (request) => {
Expand Down Expand Up @@ -47,21 +62,72 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
};

fastify.withTypeProvider<TypeBoxTypeProvider>().route({
url: "/questions/:id/answers",
url: "/answers",
method: "GET",
schema: getAnswersSchema,
async handler(request) {
const params = getAnswersPrismaParams(request.query);
const [total, answers] = await Promise.all([
fastify.db.questionAnswer.count(),
fastify.db.questionAnswer.findMany({
...params,
select: {
id: true,
content: true,
sources: true,
createdAt: true,
updatedAt: true,
CreatedBy: {
select: { id: true, firstName: true, lastName: true, socialLogin: true },
},
_count: {
select: {
QuestionAnswerVote: true,
},
},
},
}),
]);

return {
data: answers.map((a) => {
return {
id: a.id,
content: a.content,
sources: a.sources,
createdAt: a.createdAt.toISOString(),
updatedAt: a.createdAt.toISOString(),
createdBy: {
id: a.CreatedBy.id,
firstName: a.CreatedBy.firstName,
lastName: a.CreatedBy.lastName,
socialLogin: a.CreatedBy.socialLogin as Record<string, string | number>,
},
votesCount: a._count.QuestionAnswerVote,
};
}),
meta: { total },
};
},
});

fastify.withTypeProvider<TypeBoxTypeProvider>().route({
url: "/questions/:id/answers",
method: "GET",
schema: getAnswersRelatedToPostSchema,
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,7 +150,7 @@ 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),
});

return { data: dbAnswerToDto(answer) };
Expand All @@ -109,12 +175,13 @@ 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),
});

return { data: dbAnswerToDto(answer) };
Expand All @@ -138,6 +205,89 @@ const answersPlugin: FastifyPluginAsync = async (fastify) => {
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();
},
});
};

export default answersPlugin;
71 changes: 69 additions & 2 deletions apps/api/modules/answers/answers.schemas.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Type } from "@sinclair/typebox";
import { Static, 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" }),
votesCount: Type.Integer(),
currentUserVotedOn: Type.Boolean(),
createdBy: Type.Object({
id: Type.Integer(),
firstName: Type.Union([Type.String(), Type.Null()]),
Expand All @@ -13,7 +15,7 @@ const answerSchema = Type.Object({
}),
});

export const getAnswersSchema = {
export const getAnswersRelatedToPostSchema = {
params: Type.Object({
id: Type.Integer(),
}),
Expand Down Expand Up @@ -64,3 +66,68 @@ 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(),
},
};

const generateGetAnswersQuerySchema = Type.Partial(
Type.Object({
limit: Type.Integer(),
offset: Type.Integer(),
orderBy: Type.Union([
Type.Literal("createdAt"),
Type.Literal("updatedAt"),
Type.Literal("votesCount"),
]),
order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
}),
);

export const getAnswersSchema = {
querystring: generateGetAnswersQuerySchema,
response: {
200: Type.Object({
data: Type.Array(
Type.Object({
id: Type.Number(),
content: Type.String(),
sources: Type.Array(Type.String()),
createdAt: Type.String({ format: "date-time" }),
updatedAt: Type.String({ format: "date-time" }),
createdBy: Type.Object({
id: Type.Integer(),
firstName: Type.Union([Type.String(), Type.Null()]),
lastName: Type.Union([Type.String(), Type.Null()]),
socialLogin: Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()])),
}),
votesCount: Type.Integer(),
}),
),
meta: Type.Object({
total: Type.Integer(),
}),
}),
},
};

export type GetAnswersQuery = Static<typeof generateGetAnswersQuerySchema>;
Loading