Skip to content

Commit adf6c77

Browse files
committed
feat: linked quizzes (#14)
1 parent 7d49f7c commit adf6c77

File tree

24 files changed

+473
-57
lines changed

24 files changed

+473
-57
lines changed

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"promise-retry": "^2.0.1",
2828
"react": "^18.2.0",
2929
"react-beautiful-dnd": "^13.1.1",
30+
"react-copy-to-clipboard": "^5.1.0",
3031
"react-dom": "^18.2.0",
3132
"react-error-boundary": "^4.0.13",
3233
"react-helmet-async": "^2.0.5",
@@ -48,6 +49,7 @@
4849
"@types/promise-retry": "^1.1.6",
4950
"@types/react": "^18.2.66",
5051
"@types/react-beautiful-dnd": "^13.1.8",
52+
"@types/react-copy-to-clipboard": "^5.0.7",
5153
"@types/react-dom": "^18.2.22",
5254
"@types/react-helmet": "^6.1.11",
5355
"@typescript-eslint/eslint-plugin": "^7.2.0",

src/api/answer/answer.api.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TAnswerId, TPublicQuestion, TUserAnswer } from "./answer.schema";
55
type StartQuizInput = {
66
questionsId: string;
77
quizId: string;
8-
userId: string;
8+
userId?: string;
99
email: string;
1010
username: string;
1111
};
@@ -35,27 +35,27 @@ export const startQuiz = async ({
3535

3636
export type GetUserAnswerInput = {
3737
answerId?: string;
38-
answersUser?: {
39-
userId: string;
40-
questionsId: string;
41-
};
38+
email: string;
39+
questionsId?: string;
4240
};
4341

4442
export const getUserAnswers = async ({
4543
answerId,
46-
answersUser,
44+
email,
45+
questionsId,
4746
}: GetUserAnswerInput) => {
4847
const query = new URLSearchParams();
4948

5049
if (answerId) {
5150
query.set("answerId", answerId);
5251
}
5352

54-
if (answersUser) {
55-
query.set("userId", answersUser.userId);
56-
query.set("questionsId", answersUser.questionsId);
53+
if (questionsId) {
54+
query.set("questionsId", questionsId);
5755
}
5856

57+
query.set("email", email);
58+
5959
return await request("/api/answer/details").get(
6060
{
6161
query,
@@ -98,6 +98,7 @@ type SaveAnswerInput = {
9898
answers: Array<string>;
9999
order: number;
100100
isLast: boolean;
101+
userEmail: string;
101102
};
102103

103104
export const saveAnswer = async ({
@@ -108,6 +109,7 @@ export const saveAnswer = async ({
108109
answers,
109110
order,
110111
isLast,
112+
userEmail,
111113
}: SaveAnswerInput) => {
112114
return await request("/api/answer/:answerId/save").post({
113115
body: {
@@ -117,6 +119,7 @@ export const saveAnswer = async ({
117119
answers,
118120
order,
119121
isLast,
122+
email: userEmail,
120123
},
121124
params: {
122125
answerId,

src/api/quiz/quiz.api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,25 @@ export const getPublicQuizzes = async ({
109109

110110
export type GetPublicQuizDetailsInput = {
111111
quizId: string;
112+
userId?: string;
112113
};
113114

114115
export const getPublicQuizDetails = async ({
115116
quizId,
117+
userId,
116118
}: GetPublicQuizDetailsInput) => {
119+
const query = new URLSearchParams();
120+
121+
if (userId) {
122+
query.set("userId", userId);
123+
}
124+
117125
return await request("/api/quizzes/:quizId").get(
118126
{
119127
params: {
120128
quizId,
121129
},
130+
query,
122131
},
123132
TPublicQuiz
124133
);

src/api/quiz/quiz.schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const TQuizStatus = z.union([z.literal("DRAFT"), z.literal("READY")]);
3939
export type QuizStatus = z.infer<typeof TQuizStatus>;
4040

4141
const TQuizUser = z.object({
42-
userId: z.string(),
42+
userId: z.string().optional(),
4343
email: z.string().email(),
4444
username: z.string(),
4545
answerId: z.string(),

src/app/assets/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { IconArrow } from "./arrow";
22
export { IconCheck } from "./check";
33
export { IconClose } from "./close";
44
export { IconDraggableDots } from "./draggable-dots";
5+
export { IconLink } from "./link";
56
export { IconPlus } from "./plus";
67
export { IconQuiz } from "./quiz";
78
export * from "./quiz-categories";

src/app/assets/icons/link.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
2+
3+
export const IconLink = ({ ...props }: SvgIconProps) => {
4+
return (
5+
<SvgIcon {...props} viewBox="0 0 24 24">
6+
<g clipPath="url(#clip0_100_8)">
7+
<path
8+
d="M17.303 9.524L20.485 12.706C21.0013 13.2155 21.4118 13.822 21.6927 14.4908C21.9737 15.1595 22.1196 15.8773 22.122 16.6026C22.1244 17.328 21.9834 18.0467 21.7069 18.7173C21.4304 19.3879 21.024 19.9972 20.5111 20.5101C19.9982 21.0231 19.3889 21.4294 18.7183 21.7059C18.0477 21.9824 17.329 22.1235 16.6036 22.121C15.8783 22.1186 15.1605 21.9727 14.4918 21.6918C13.823 21.4108 13.2165 21.0003 12.707 20.484L11.647 19.424C11.5037 19.2857 11.3893 19.1202 11.3106 18.9373C11.2319 18.7543 11.1904 18.5575 11.1886 18.3583C11.1868 18.1592 11.2247 17.9616 11.3 17.7772C11.3753 17.5929 11.4866 17.4253 11.6274 17.2844C11.7682 17.1435 11.9356 17.0321 12.1199 16.9566C12.3042 16.881 12.5017 16.843 12.7009 16.8446C12.9 16.8463 13.0969 16.8876 13.2799 16.9661C13.463 17.0446 13.6285 17.1588 13.767 17.302L14.829 18.363C15.2986 18.8284 15.9334 19.0888 16.5946 19.0873C17.2557 19.0857 17.8893 18.8223 18.3568 18.3548C18.8242 17.8872 19.0874 17.2535 19.0887 16.5924C19.0901 15.9312 18.8295 15.2965 18.364 14.827L15.182 11.645C14.8381 11.3009 14.4012 11.0647 13.925 10.9652C13.4488 10.8657 12.9539 10.9074 12.501 11.085C12.3397 11.149 12.1883 11.2143 12.047 11.281L11.583 11.498C10.963 11.778 10.486 11.898 9.87899 11.292C9.00699 10.42 9.23299 9.615 10.296 8.882C11.3549 8.15336 12.6356 7.81861 13.9156 7.93589C15.1956 8.05317 16.3941 8.61507 17.303 9.524ZM11.293 3.514L12.353 4.574C12.6264 4.85677 12.7777 5.23561 12.7745 5.62891C12.7713 6.0222 12.6137 6.3985 12.3357 6.67674C12.0577 6.95499 11.6816 7.11292 11.2883 7.11652C10.895 7.12012 10.516 6.96911 10.233 6.696L9.17199 5.636C8.94144 5.39716 8.66563 5.20663 8.36066 5.07552C8.05568 4.94441 7.72765 4.87535 7.39571 4.87238C7.06376 4.8694 6.73454 4.93256 6.42727 5.05817C6.11999 5.18379 5.84081 5.36934 5.60601 5.60401C5.37121 5.83867 5.1855 6.11775 5.05971 6.42495C4.93393 6.73216 4.87058 7.06134 4.87337 7.39329C4.87616 7.72524 4.94503 8.05331 5.07597 8.35835C5.2069 8.6634 5.39728 8.93932 5.63599 9.17L8.81799 12.352C9.16191 12.6961 9.59876 12.9323 10.075 13.0318C10.5512 13.1313 11.0461 13.0896 11.499 12.912C11.6603 12.848 11.8117 12.7827 11.953 12.716L12.417 12.499C13.037 12.219 13.515 12.099 14.121 12.705C14.993 13.577 14.767 14.382 13.704 15.115C12.6451 15.8436 11.3644 16.1784 10.0844 16.0611C8.80437 15.9438 7.60586 15.3819 6.69699 14.473L3.51499 11.291C2.99865 10.7815 2.58818 10.175 2.30723 9.50621C2.02628 8.83746 1.88039 8.11974 1.87796 7.39437C1.87553 6.669 2.01661 5.95032 2.29307 5.2797C2.56954 4.60908 2.97593 3.99977 3.48884 3.48686C4.00176 2.97394 4.61107 2.56755 5.28169 2.29109C5.95231 2.01462 6.67099 1.87354 7.39636 1.87597C8.12172 1.8784 8.83944 2.02429 9.5082 2.30525C10.1769 2.5862 10.7835 2.99766 11.293 3.514Z"
9+
fill="currentColor"
10+
/>
11+
</g>
12+
<defs>
13+
<clipPath id="clip0_100_8">
14+
<rect width="24" height="24" fill="white" />
15+
</clipPath>
16+
</defs>
17+
</SvgIcon>
18+
);
19+
};

src/app/routes/paths.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const paths = {
1111
addQuizQuestions: "/my-quizzes/:quizId/:questionsId/add-questions",
1212
profile: "/profile",
1313
createQuiz: "/create-quiz",
14+
15+
linkedQuiz: "/linked-quiz/:quizId",
1416
} as const;
1517

1618
export type PathKey = keyof typeof paths;

src/app/routes/routes.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { createRoute } from "./create-route";
22
import { paths } from "./paths";
33

4-
export const authRoutes = [
4+
const sharedRoutes = [
55
createRoute({
6-
path: paths.dashboard,
7-
factory: () => import("../../pages/dashboard"),
8-
title: "Dashboard",
9-
}),
10-
createRoute({
11-
path: paths.startQuiz,
12-
factory: () => import("../../pages/start-quiz"),
13-
title: "Start quiz",
6+
path: paths.linkedQuiz,
7+
factory: () => import("../../pages/linked-quiz"),
8+
title: "Quiz",
149
}),
1510
createRoute({
1611
path: paths.passQuiz,
@@ -22,6 +17,19 @@ export const authRoutes = [
2217
factory: () => import("../../pages/result"),
2318
title: "Quiz result",
2419
}),
20+
];
21+
22+
export const authRoutes = [
23+
createRoute({
24+
path: paths.dashboard,
25+
factory: () => import("../../pages/dashboard"),
26+
title: "Dashboard",
27+
}),
28+
createRoute({
29+
path: paths.startQuiz,
30+
factory: () => import("../../pages/start-quiz"),
31+
title: "Start quiz",
32+
}),
2533
createRoute({
2634
path: paths.myQuizzes,
2735
factory: () => import("../../pages/my-quizzes"),
@@ -52,6 +60,7 @@ export const authRoutes = [
5260
factory: () => import("../../pages/profile"),
5361
title: "Profile",
5462
}),
63+
...sharedRoutes,
5564
];
5665

5766
export const unauthRoutes = [
@@ -65,6 +74,7 @@ export const unauthRoutes = [
6574
factory: () => import("../../pages/sign-up"),
6675
title: "Sign up",
6776
}),
77+
...sharedRoutes,
6878
];
6979

7080
export const routes = [...authRoutes, ...unauthRoutes];

src/app/ui/copy-text.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { IconLink } from "@app/assets/icons";
2+
import { useBoolean } from "@lib/hooks";
3+
import { Box, SxProps, Theme, Tooltip } from "@mui/material";
4+
import { useEffect } from "react";
5+
import { CopyToClipboard } from "react-copy-to-clipboard";
6+
7+
type Props = {
8+
text: string;
9+
copyText?: string;
10+
iconSx?: SxProps<Theme>;
11+
};
12+
13+
export const CopyText = ({ text, copyText, iconSx }: Props) => {
14+
const isCopied = useBoolean();
15+
16+
useEffect(() => {
17+
if (isCopied.isTrue) {
18+
setTimeout(() => {
19+
isCopied.setFalse();
20+
}, 3000);
21+
}
22+
}, [isCopied]);
23+
24+
return (
25+
<Box
26+
onClick={(event) => {
27+
event.stopPropagation();
28+
}}
29+
>
30+
<CopyToClipboard text={text} onCopy={isCopied.setTrue}>
31+
<Tooltip
32+
title={isCopied.isTrue ? "Copied" : copyText ?? "Copy"}
33+
arrow
34+
placement="top"
35+
>
36+
<Box
37+
sx={{
38+
display: "flex",
39+
alignItems: "center",
40+
justifyContent: "center",
41+
cursor: "pointer",
42+
}}
43+
>
44+
<IconLink
45+
sx={{
46+
color: isCopied.isTrue ? "success.main" : "text.disabled",
47+
...iconSx,
48+
}}
49+
/>
50+
</Box>
51+
</Tooltip>
52+
</CopyToClipboard>
53+
</Box>
54+
);
55+
};

0 commit comments

Comments
 (0)