Skip to content

Commit e2921f9

Browse files
authored
Adding Trivia (#2)
* Working on Trivia app * Reworking Trivia * Working on Trivia app, hooking up to external server * Handling vote stuff with push * Remove .env * Hook up to env * Working on category hookup * Hooking things up * Improving bar, more space, etc * Shuffle the answers * Fixing bug with onClose on Trivia * Some cleanup * Update dev deps * Upgrade deps
1 parent a850ddc commit e2921f9

File tree

11 files changed

+557
-757
lines changed

11 files changed

+557
-757
lines changed

.env

Lines changed: 0 additions & 1 deletion
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ yarn-debug.log*
2525
yarn-error.log*
2626

2727
# local env files
28+
.env
2829
.env.local
2930
.env.development.local
3031
.env.test.local

components/inputs/select.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { forwardRef, SelectHTMLAttributes } from "react";
22
import { ChevronDown } from "react-feather";
33

44
interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
5-
options: string[];
5+
options: {
6+
value: any;
7+
label: string;
8+
}[];
69
}
710

811
const Select = forwardRef<HTMLSelectElement, Props>(function SelectElement(
@@ -17,8 +20,8 @@ const Select = forwardRef<HTMLSelectElement, Props>(function SelectElement(
1720
{...rest}
1821
>
1922
{options.map((option) => (
20-
<option key={option} value={option}>
21-
{option}
23+
<option key={option.value} value={option.value}>
24+
{option.label}
2225
</option>
2326
))}
2427
</select>

components/roomService.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { RoomServiceProvider } from "@roomservice/react";
2+
import { useSession, useSoapboxRoomId } from "../hooks";
3+
import { AuthFunction } from "../lib/roomservice";
4+
import LoadingView from "../views/loading";
5+
6+
export default function RoomService({ children }) {
7+
const soapboxRoomId = useSoapboxRoomId();
8+
9+
const user = useSession();
10+
11+
const isOnline = user !== null && soapboxRoomId !== null;
12+
13+
return (
14+
<RoomServiceProvider
15+
online={isOnline}
16+
clientParameters={{
17+
auth: AuthFunction,
18+
ctx: {
19+
userID: String(user?.id),
20+
},
21+
}}
22+
>
23+
{isOnline ? children : <LoadingView />}
24+
</RoomServiceProvider>
25+
);
26+
}

hooks.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getUser, User } from "@soapboxsocial/minis.js";
22
import { useRouter } from "next/router";
33
import { useEffect, useRef, useState } from "react";
4+
import useSWR from "swr";
45

56
export function useSession() {
67
const [user, userSet] = useState<User>(null);
@@ -60,3 +61,29 @@ export function useInterval(callback: Function, delay: number) {
6061
}
6162
}, [delay]);
6263
}
64+
65+
const getTriviaCategories = async () => {
66+
type Data = {
67+
trivia_categories: {
68+
id: number;
69+
name: string;
70+
}[];
71+
};
72+
73+
const r = await fetch(`https://opentdb.com/api_category.php`);
74+
75+
const { trivia_categories }: Data = await r.json();
76+
77+
const cleaned = trivia_categories.map((val) => ({
78+
label: val.name.replace("Entertainment: ", "").replace("Science: ", ""),
79+
value: val.id.toString(),
80+
}));
81+
82+
return cleaned;
83+
};
84+
85+
export function useTriviaCategories() {
86+
const { data } = useSWR("TriviaCategories", getTriviaCategories);
87+
88+
return data;
89+
}

package.json

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,30 @@
88
"start": "next start"
99
},
1010
"dependencies": {
11+
"@harelpls/use-pusher": "^7.2.1",
1112
"@roomservice/react": "^1.0.6",
1213
"@soapboxsocial/minis.js": "^2.2.1",
1314
"classnames": "^2.2.6",
14-
"framer-motion": "^3.8.1",
15+
"dompurify": "^2.2.7",
16+
"framer-motion": "^4.0.3",
17+
"lodash.shuffle": "^4.2.0",
1518
"mitt": "^2.1.0",
16-
"next": "10.0.6",
17-
"react": "17.0.1",
18-
"react-dom": "17.0.1",
19+
"next": "^10.0.9",
20+
"react": "^17.0.2",
21+
"react-dom": "^17.0.2",
1922
"react-feather": "^2.0.9",
20-
"react-hook-form": "^6.15.1",
21-
"react-use": "^17.1.1"
23+
"react-hook-form": "^6.15.5",
24+
"react-use": "^17.2.1",
25+
"swr": "^0.5.4"
2226
},
2327
"devDependencies": {
2428
"@types/classnames": "^2.2.11",
25-
"@types/node": "^14.14.27",
26-
"@types/react": "^17.0.1",
27-
"autoprefixer": "^10.2.4",
28-
"postcss": "^8.2.6",
29-
"tailwindcss": "^2.0.3",
30-
"typescript": "^4.1.5"
29+
"@types/lodash.shuffle": "^4.2.6",
30+
"@types/node": "^14.14.35",
31+
"@types/react": "^17.0.3",
32+
"autoprefixer": "^10.2.5",
33+
"postcss": "^8.2.8",
34+
"tailwindcss": "^2.0.4",
35+
"typescript": "^4.2.3"
3136
}
3237
}

pages/trivia/.gitkeep

Whitespace-only changes.

pages/trivia/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { PusherProvider, PusherProviderProps } from "@harelpls/use-pusher";
2+
import TriviaView from "../../views/trivia";
3+
4+
const config: PusherProviderProps = {
5+
clientKey: process.env.NEXT_PUBLIC_PUSHER_CLIENT_KEY,
6+
cluster: "eu",
7+
};
8+
9+
export default function Trivia() {
10+
return (
11+
<PusherProvider {...config}>
12+
<TriviaView />
13+
</PusherProvider>
14+
);
15+
}

views/trivia/index.tsx

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { useChannel, useEvent } from "@harelpls/use-pusher";
2+
import { onClose } from "@soapboxsocial/minis.js";
3+
import cn from "classnames";
4+
import DOMPurify from "dompurify";
5+
import shuffle from "lodash.shuffle";
6+
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
7+
import Button from "../../components/inputs/button";
8+
import Select from "../../components/inputs/select";
9+
import { useParams, useSoapboxRoomId, useTriviaCategories } from "../../hooks";
10+
import LoadingView from "../loading";
11+
12+
export type Question = {
13+
category: string;
14+
correct_answer: string;
15+
difficulty: "easy" | "medium" | "hard";
16+
incorrect_answers: string[];
17+
question: string;
18+
type: "boolean" | "multiple";
19+
};
20+
21+
const SERVER_BASE = process.env.NEXT_PUBLIC_APPS_SERVER_BASE_URL as string;
22+
23+
export default function TriviaView() {
24+
const { isAppOpener } = useParams();
25+
26+
const soapboxRoomId = useSoapboxRoomId();
27+
28+
const channelName = `mini-trivia-${soapboxRoomId}`;
29+
const channel = useChannel(channelName);
30+
31+
const categories = useTriviaCategories();
32+
33+
const [category, categorySet] = useState<string>("all");
34+
35+
const handleSelect = (event: ChangeEvent<HTMLSelectElement>) =>
36+
categorySet(event.target.value);
37+
38+
const [activeQuestion, activeQuestionSet] = useState<Question>();
39+
40+
useEvent(channel, "question", (data: { question: Question }) => {
41+
console.log("Received 'question' event with payload", data);
42+
43+
activeQuestionSet(data.question);
44+
});
45+
46+
const questions = useMemo(() => {
47+
if (activeQuestion)
48+
return shuffle([
49+
activeQuestion.correct_answer,
50+
...activeQuestion.incorrect_answers,
51+
]);
52+
53+
return null;
54+
}, [activeQuestion]);
55+
56+
const init = useCallback(async () => {
57+
console.log("[init]");
58+
59+
try {
60+
await fetch(
61+
`${SERVER_BASE}/trivia/${soapboxRoomId}/setup?category=${category}`
62+
);
63+
} catch (error) {
64+
console.error(error);
65+
}
66+
}, [category, soapboxRoomId]);
67+
68+
const [votedAnswer, votedAnswerSet] = useState<string>(null);
69+
70+
useEffect(() => {
71+
if (typeof votedAnswer === "string") votedAnswerSet(null);
72+
}, [activeQuestion]);
73+
74+
const [votes, votesSet] = useState<string[]>([]);
75+
76+
useEvent(channel, "vote", (data: { votes: string[] }) => {
77+
console.log("Received 'vote' event with payload", data);
78+
79+
votesSet(data.votes);
80+
});
81+
82+
const voteOnQuestion = (answer: string) => async () => {
83+
votedAnswerSet(answer);
84+
85+
await fetch(`${SERVER_BASE}/trivia/${soapboxRoomId}/vote`, {
86+
method: "POST",
87+
body: JSON.stringify({ vote: answer }),
88+
headers: { "Content-Type": "application/json" },
89+
});
90+
};
91+
92+
const calcVoteCount = (answer: string) =>
93+
votes.filter((vote) => vote === answer).length;
94+
95+
/**
96+
* Mini Cleanup
97+
*/
98+
99+
const [isMiniClosed, isMiniClosedSet] = useState(false);
100+
101+
useEffect(() => {
102+
onClose(async () => {
103+
isMiniClosedSet(true);
104+
105+
activeQuestionSet(null);
106+
votedAnswerSet(null);
107+
categorySet("all");
108+
votesSet([]);
109+
110+
await fetch(`${SERVER_BASE}/trivia/${soapboxRoomId}/reset`);
111+
});
112+
}, []);
113+
114+
if (!activeQuestion && isAppOpener && categories) {
115+
return (
116+
<main className="flex flex-col min-h-screen select-none">
117+
<div className="pt-4 px-4">
118+
<div className="relative">
119+
<h1 className="text-title2 font-bold text-center">Trivia</h1>
120+
</div>
121+
</div>
122+
123+
<div className="flex-1 p-4 flex flex-col">
124+
<div className="flex-1">
125+
<label className="flex mb-2" htmlFor="category">
126+
<span className="text-body">Choose a category</span>
127+
</label>
128+
129+
<Select
130+
id="category"
131+
onChange={handleSelect}
132+
value={category}
133+
options={[{ label: "All", value: "all" }, ...categories]}
134+
/>
135+
</div>
136+
137+
<div className="pt-4">
138+
<Button onClick={init}>Start a round</Button>
139+
</div>
140+
</div>
141+
</main>
142+
);
143+
}
144+
145+
if (activeQuestion)
146+
return (
147+
<main className="flex flex-col min-h-screen select-none relative">
148+
<Timer />
149+
150+
<div className="flex-1 px-4 flex items-center justify-center">
151+
<p
152+
className="text-body font-bold text-center break-words"
153+
dangerouslySetInnerHTML={{
154+
__html: DOMPurify.sanitize(activeQuestion.question),
155+
}}
156+
/>
157+
</div>
158+
159+
<div className="px-4 pb-4 space-y-2">
160+
{questions.map((question) => (
161+
<TriviaButton
162+
active={votedAnswer === question}
163+
correct={
164+
votedAnswer === question &&
165+
votedAnswer === activeQuestion.correct_answer
166+
}
167+
disabled={votedAnswer}
168+
onClick={voteOnQuestion(question)}
169+
key={question}
170+
text={question}
171+
voteCount={calcVoteCount(question)}
172+
/>
173+
))}
174+
</div>
175+
</main>
176+
);
177+
178+
return <LoadingView restartCallback={isMiniClosed ? init : null} />;
179+
}
180+
181+
function Timer() {
182+
const soapboxRoomId = useSoapboxRoomId();
183+
184+
const channelName = `mini-trivia-${soapboxRoomId}`;
185+
186+
const channel = useChannel(channelName);
187+
188+
const [timer, timerSet] = useState(0);
189+
190+
useEvent(channel, "timer", (data: { timer: number }) => {
191+
console.log("Received 'timer' event with payload", data);
192+
193+
timerSet(data.timer);
194+
});
195+
196+
const DURATION = 15;
197+
198+
return (
199+
<div className="absolute top-0 right-0 left-0">
200+
<div
201+
className="h-1 bg-soapbox"
202+
style={{ width: `${(timer / DURATION) * 100}%` }}
203+
/>
204+
</div>
205+
);
206+
}
207+
208+
function TriviaButton({ active, correct, disabled, onClick, text, voteCount }) {
209+
const cachedClassNames = cn(
210+
"w-full rounded py-3 px-4 text-body font-semibold focus:outline-none focus:ring-4 border-2",
211+
disabled ? "flex justify-between" : "",
212+
active
213+
? correct
214+
? "bg-accent-green border-accent-green text-black"
215+
: "bg-soapbox border-soapbox text-white"
216+
: "border-systemGrey4-light dark:border-systemGrey4-dark"
217+
);
218+
219+
return (
220+
<button onClick={onClick} className={cachedClassNames} disabled={disabled}>
221+
<span>{text}</span>
222+
223+
{disabled && <span className="text-sm">{voteCount}</span>}
224+
</button>
225+
);
226+
}

views/would-you-rather/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { useList, useMap } from "@roomservice/react";
1+
import { useMap } from "@roomservice/react";
22
import { onClose } from "@soapboxsocial/minis.js";
3-
import { Fragment, useCallback, useEffect, useState } from "react";
3+
import { useCallback, useEffect, useState } from "react";
44
import { ArrowRight } from "react-feather";
5-
import { useInterval, useStateList } from "react-use";
65
import { CircleIconButton } from "../../components/inputs/button";
76
import { useParams, useSoapboxRoomId } from "../../hooks";
87
import getRandom from "../../lib/getRandom";

0 commit comments

Comments
 (0)