Skip to content

Commit

Permalink
feat: implemented new design after answering, and results page
Browse files Browse the repository at this point in the history
  • Loading branch information
maxwiseman committed Jan 23, 2025
1 parent d89c74f commit a5ec0aa
Show file tree
Hide file tree
Showing 18 changed files with 1,124 additions and 328 deletions.
53 changes: 45 additions & 8 deletions apps/multiplayer-server/src/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import { protocolSchema } from "@scibo/multiplayer-server/from-client";

import type { urlParams } from ".";
import {
serverAnswerSchema,
protocolSchema as serverProtocolSchema,
serverUpdateGameStateSchema,
} from "./schema/from-server";
import { clientQuestionSchema, serverQuestionSchema } from "./schema/shared";
import { channelData } from "./state";
import {
clientQuestionSchema,
serverAnswerSchema,
serverQuestionSchema,
} from "./schema/shared";
import { channelData, storedData } from "./state";
import { publish } from "./utils";

export async function handleIncomingMessage(
Expand All @@ -35,6 +38,20 @@ export async function handleIncomingMessage(

case "startGame": {
if (currentChannelData.users[ws.data.userId]?.role !== "host") return;
if (currentChannelData.gameSettings.end.type === "time")
setTimeout(
() => {
const currentChannelData = storedData[ws.data.room];
publish(ws.data.room, {
type: "updateGameState",
state: {
stage: "results",
history: currentChannelData?.history ?? [],
},
});
},
currentChannelData.gameSettings.end.maxTime * 60 * 1000,
);

publish(ws.data.room, await nextQuestion(currentChannelData));
break;
Expand Down Expand Up @@ -212,6 +229,23 @@ export async function handleIncomingMessage(
async function nextQuestion(
currentChannelData: channelData,
): Promise<z.infer<typeof serverUpdateGameStateSchema>> {
const lastQuestionData =
currentChannelData.gameState.stage === "question"
? currentChannelData.gameState.question
: undefined;

if (
currentChannelData.gameSettings.end.type === "questions" &&
(lastQuestionData?.qNumber ?? 0) >=
currentChannelData.gameSettings.end.maxQuestions
) {
console.log("Game finished!", currentChannelData.history);
return {
type: "updateGameState",
state: { stage: "results", history: currentChannelData.history },
};
}

const nextQuestionData = (
await db
.select()
Expand All @@ -221,11 +255,6 @@ async function nextQuestion(
.limit(1)
)[0]! as typeof Question.$inferSelect;

const lastQuestionData =
currentChannelData.gameState.stage === "question"
? currentChannelData.gameState.question
: undefined;

console.log(nextQuestionData);

currentChannelData.gameState = {
Expand All @@ -241,6 +270,14 @@ async function nextQuestion(
answers: {},
};

currentChannelData.history = [
...currentChannelData.history,
{
question: currentChannelData.gameState.question,
answers: currentChannelData.gameState.answers,
},
];

const outgoingMsg: z.infer<typeof serverUpdateGameStateSchema> = {
type: "updateGameState",
state: {
Expand Down
24 changes: 14 additions & 10 deletions apps/multiplayer-server/src/schema/from-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ import {
clientQuestionSchema,
gameSettingsSchema,
lobbyStateSchema,
resultsStateSchema,
} from "./shared";

export const clientAnswerSchema = z
.record(
z.string(),
z.object({
time: z.coerce.date(),
answer: z.string(),
correct: z.enum(["correct", "incorrect", "skipped", "grading"]),
}),
)
.optional();

export const clientQuestionStateSchema = z.object({
stage: z.literal("question"),
question: z.intersection(
Expand All @@ -18,19 +30,11 @@ export const clientQuestionStateSchema = z.object({
),
correctAnswer: z.string().optional(),
explanation: z.string().optional(),
answers: z
.record(
z.string(),
z.object({
time: z.coerce.date(),
answer: z.string(),
correct: z.enum(["correct", "incorrect", "skipped", "grading"]),
}),
)
.optional(),
answers: clientAnswerSchema,
});
export const clientGameStateSchema = z.discriminatedUnion("stage", [
lobbyStateSchema,
resultsStateSchema,
clientQuestionStateSchema,
]);

Expand Down
8 changes: 3 additions & 5 deletions apps/multiplayer-server/src/schema/from-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ import { clientGameStateSchema } from "./from-client";
import {
gameSettingsSchema,
lobbyStateSchema,
resultsStateSchema,
serverAnswerSchema,
serverQuestionSchema,
userSchema,
} from "./shared";

export const serverAnswerSchema = z.object({
time: z.date(),
answer: z.string(),
correct: z.enum(["incorrect", "correct", "skipped", "grading"]),
});
export const serverQuestionStateSchema = z.object({
stage: z.literal("question"),
question: z.intersection(
Expand All @@ -27,6 +24,7 @@ export const serverQuestionStateSchema = z.object({
});
export const serverGameStateSchema = z.discriminatedUnion("stage", [
lobbyStateSchema,
resultsStateSchema,
serverQuestionStateSchema,
]);

Expand Down
22 changes: 21 additions & 1 deletion apps/multiplayer-server/src/schema/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { z } from "zod";

import { topicEnum } from "@scibo/db/types";

// import { serverAnswerSchema } from "./from-server";

// import { historySchema } from "../state";

export const userPlayerSchema = z.object({
id: z.string(),
username: z.string(),
Expand All @@ -19,6 +23,11 @@ export const userSchema = z.discriminatedUnion("role", [
userPlayerSchema,
userSpectatorSchema,
]);
export const serverAnswerSchema = z.object({
time: z.coerce.date(),
answer: z.string(),
correct: z.enum(["incorrect", "correct", "skipped", "grading"]),
});

export const serverMcqAnswerSchema = z.object({
answer: z.coerce.string(),
Expand Down Expand Up @@ -90,8 +99,19 @@ export const clientQuestionSchema = z.discriminatedUnion("type", [
clientMcqQuestionSchema,
]);

export const gameStages = z.enum(["lobby", "question"]);
export const historySchema = z.array(
z.object({
question: serverQuestionSchema,
answers: z.record(z.string(), serverAnswerSchema),
}),
);

export const gameStages = z.enum(["lobby", "question", "results"]);
export const lobbyStateSchema = z.object({ stage: z.literal("lobby") });
export const resultsStateSchema = z.object({
stage: z.literal("results"),
history: historySchema,
});

export const gameSettingsSchema = z.object({
timing: z
Expand Down
15 changes: 5 additions & 10 deletions apps/multiplayer-server/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { ServerWebSocket } from "bun";
import { z } from "zod";

import type { gameSettingsSchema, userSchema } from "./schema/shared";
import type {
gameSettingsSchema,
historySchema,
userSchema,
} from "./schema/shared";
import { urlParams } from ".";
import { clientGameStateSchema } from "./schema/from-client";
import {
serverAnswerSchema,
serverCatchupSchema,
serverGameStateSchema,
} from "./schema/from-server";
import { serverQuestionSchema } from "./schema/shared";

export const historySchema = z.array(
z.object({
question: serverQuestionSchema,
answers: serverAnswerSchema,
}),
);

export type channelData = {
users: Record<
Expand Down
1 change: 1 addition & 0 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^1.0.8",
"@number-flow/react": "^0.5.5",
"@scibo/api": "workspace:*",
"@scibo/auth": "workspace:*",
"@scibo/db": "workspace:*",
Expand Down
7 changes: 6 additions & 1 deletion apps/nextjs/src/app/_components/blur-transition.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { MotionProps } from "motion/react";

export const blurTransition: MotionProps = {
type ObjectOnly<T> = T extends object ? T : never;
export const blurTransition: {
animate: ObjectOnly<MotionProps["animate"]>;
exit: ObjectOnly<MotionProps["exit"]>;
initial: ObjectOnly<MotionProps["initial"]>;
} = {
exit: {
// y: -25,
opacity: 0,
Expand Down
95 changes: 60 additions & 35 deletions apps/nextjs/src/app/_components/quiz/answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,40 @@

import { useEffect, useState } from "react";
import { IconArrowForward } from "@tabler/icons-react";
import { useSelector } from "@xstate/store/react";
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import { isMobile } from "react-device-detect";
import { z } from "zod";

import { clientAnswerSchema } from "@scibo/multiplayer-server/from-client";
import { serverAnswerSchema } from "@scibo/multiplayer-server/from-server";
import { clientQuestionSchema } from "@scibo/multiplayer-server/shared";
import { Button } from "@scibo/ui/button";

import { blurTransition } from "../blur-transition";
import websocketStore from "../websocket/xstate";
import { QuizMcqAnswers } from "./mcq-answers";
import { QuizShortAnswer } from "./short-answer";

export function QuizAnswers({
question,
responses,
locked = false,
hideIncorrect,
correct,
onSubmit,
}: {
// question: sciboQuestion & { qNumber: number };
question: z.infer<typeof clientQuestionSchema> & { qNumber: number };
responses?: z.infer<typeof clientAnswerSchema>;
hideIncorrect?: boolean;
correct?: string;
locked?: boolean;
debug?: boolean;
onSubmit: (val: string) => void;
}) {
const [answer, setAnswer] = useState("");
const users = useSelector(websocketStore, (i) => i.context.users);

useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Enter" || event.keyCode === 13) {
Expand All @@ -34,45 +49,55 @@ export function QuizAnswers({
}, [answer, onSubmit]);
useEffect(() => {
setAnswer("");
}, [question]);
}, [question.qNumber]);

return (
<div className="flex w-full flex-col items-center gap-12">
{question.type === "shortAnswer" ? (
<QuizShortAnswer answer={answer} onAnswerChange={setAnswer} />
) : (
<QuizMcqAnswers
selection={answer}
onSelectChange={setAnswer}
answers={question.answer}
/>
)}
{isMobile === true ? (
<Button
suppressHydrationWarning
className="w-full"
onClick={() => onSubmit(answer)}
>
Submit
</Button>
) : (
<>
{question.qNumber === 1 && (
<div
onClick={() => {
// setQuestionNumber(question.qNumber + 1);
}}
className="flex cursor-default items-center gap-1 text-sm text-muted-foreground opacity-75"
<LayoutGroup>
{question.type === "shortAnswer" ? (
<QuizShortAnswer answer={answer} onAnswerChange={setAnswer} />
) : (
<QuizMcqAnswers
selection={answer}
onSelectChange={setAnswer}
answers={question.answer}
responses={responses}
hideIncorrect={hideIncorrect}
correct={correct}
locked={locked}
/>
)}
<AnimatePresence mode="popLayout">
{isMobile === true ? (
<Button
suppressHydrationWarning
className="w-full"
onClick={() => onSubmit(answer)}
>
<span className="inline-flex items-center gap-1 rounded-sm bg-muted p-1 px-2 text-xs">
<IconArrowForward className="h-3 w-3" />
Enter
</span>{" "}
to submit
</div>
Submit
</Button>
) : (
question.qNumber === 1 &&
!locked && (
<motion.div
layout
{...blurTransition}
initial={false}
onClick={() => {
// setQuestionNumber(question.qNumber + 1);
}}
className="flex cursor-default items-center gap-1 text-sm text-muted-foreground opacity-75"
>
<span className="inline-flex items-center gap-1 rounded-sm bg-muted p-1 px-2 text-xs">
<IconArrowForward className="h-3 w-3" />
Enter
</span>{" "}
to submit
</motion.div>
)
)}
</>
)}
</AnimatePresence>
</LayoutGroup>
</div>
);
}
Loading

0 comments on commit a5ec0aa

Please sign in to comment.