format, prepare party view
This commit is contained in:
parent
6c965b9065
commit
d945949fed
25 changed files with 583 additions and 528 deletions
|
|
@ -8,6 +8,7 @@ import "./workflows/party-analysis";
|
||||||
import "./dbos.ts";
|
import "./dbos.ts";
|
||||||
import { partyApp } from "./routes/party";
|
import { partyApp } from "./routes/party";
|
||||||
import { partySocketApp, pubsub } from "./routes/party-socket";
|
import { partySocketApp, pubsub } from "./routes/party-socket";
|
||||||
|
import { quizRoutes } from "./routes/quiz.ts";
|
||||||
import { statsApp } from "./routes/stats.ts";
|
import { statsApp } from "./routes/stats.ts";
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
@ -19,6 +20,7 @@ const app = new Elysia()
|
||||||
.use(partyApp)
|
.use(partyApp)
|
||||||
.use(partyAnalysisApp)
|
.use(partyAnalysisApp)
|
||||||
.use(partySocketApp)
|
.use(partySocketApp)
|
||||||
|
.use(quizRoutes)
|
||||||
.get("/", () => ({ ok: true })),
|
.get("/", () => ({ ok: true })),
|
||||||
)
|
)
|
||||||
.listen(4000);
|
.listen(4000);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { party, partyMember } from "./db/schema";
|
import { party, partyMember } from "./db/schema";
|
||||||
import type { PartySnapshot } from "./party-types";
|
import type { PartySnapshot, QuizState } from "./party-types";
|
||||||
|
|
||||||
type DbClient = typeof db;
|
type DbClient = typeof db;
|
||||||
type DbTransaction = Parameters<typeof db.transaction>[0] extends (
|
type DbTransaction = Parameters<typeof db.transaction>[0] extends (
|
||||||
|
|
@ -58,7 +58,10 @@ export async function getPartyStatus(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
party: partyRecord,
|
party: {
|
||||||
|
...partyRecord,
|
||||||
|
data: partyRecord.data as QuizState,
|
||||||
|
},
|
||||||
members,
|
members,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
import type { party, partyMember, user } from "./db/schema";
|
import type { party, partyMember, user } from "./db/schema";
|
||||||
|
|
||||||
export type Party = InferSelectModel<typeof party>;
|
export type Party = Omit<InferSelectModel<typeof party>, "data"> & {
|
||||||
|
data: QuizState;
|
||||||
|
};
|
||||||
export type PartyMember = InferSelectModel<typeof partyMember>;
|
export type PartyMember = InferSelectModel<typeof partyMember>;
|
||||||
export type User = InferSelectModel<typeof user>;
|
export type User = InferSelectModel<typeof user>;
|
||||||
|
|
||||||
|
|
@ -24,15 +26,20 @@ export type PartySocketOutgoing =
|
||||||
| { type: "ping" }
|
| { type: "ping" }
|
||||||
| { type: "member_payload"; payload: unknown };
|
| { type: "member_payload"; payload: unknown };
|
||||||
|
|
||||||
export type QuizState = {
|
export type Question = {
|
||||||
status: "idle" | "running" | "results";
|
|
||||||
workflowId: string | null;
|
|
||||||
questionIndex: number;
|
|
||||||
currentQuestion: {
|
|
||||||
text: string;
|
text: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
correct: number;
|
correct: number;
|
||||||
} | null;
|
startTimestamp: number;
|
||||||
|
endTimestamp: number;
|
||||||
|
points: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuizState = {
|
||||||
|
status: "running" | "results";
|
||||||
|
workflowId: string | null;
|
||||||
|
questionIndex: number;
|
||||||
|
currentQuestion: Question | null;
|
||||||
answers: Record<
|
answers: Record<
|
||||||
string,
|
string,
|
||||||
{ playerId: string; selected: number; correct: boolean }
|
{ playerId: string; selected: number; correct: boolean }
|
||||||
|
|
|
||||||
42
api/src/party/state.ts
Normal file
42
api/src/party/state.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { db as Db } from "../db";
|
||||||
|
import { party } from "../db/schema";
|
||||||
|
import type { QuizState } from "../party-types";
|
||||||
|
import { pubsub } from "../routes/party-socket";
|
||||||
|
|
||||||
|
export async function updatePartyData(
|
||||||
|
db: typeof Db,
|
||||||
|
id: string,
|
||||||
|
data: QuizState,
|
||||||
|
) {
|
||||||
|
const members = await db.query.partyMember.findMany({
|
||||||
|
where: {
|
||||||
|
partyId: id,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const partyObject = await db.query.party.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!partyObject) throw new Error("Missing party");
|
||||||
|
|
||||||
|
pubsub.publishPartyData(id, {
|
||||||
|
type: "party_status",
|
||||||
|
party: {
|
||||||
|
...partyObject,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
members,
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.update(party)
|
||||||
|
.set({
|
||||||
|
data: data,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(party.id, id));
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { betterAuthElysia } from "../auth";
|
||||||
|
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { getMemberRecord, getPartyStatus } from "../party-data";
|
import { getMemberRecord, getPartyStatus } from "../party-data";
|
||||||
import type { QuizState } from "../party-types";
|
import type { PartySocketEvent, QuizState } from "../party-types";
|
||||||
|
|
||||||
function userTopic(userId: string) {
|
function userTopic(userId: string) {
|
||||||
return `user:${userId}`;
|
return `user:${userId}`;
|
||||||
|
|
@ -24,6 +24,9 @@ export const pubsub = {
|
||||||
publish(topic: string, data: string) {
|
publish(topic: string, data: string) {
|
||||||
this._server?.publish(topic, data);
|
this._server?.publish(topic, data);
|
||||||
},
|
},
|
||||||
|
publishPartyData(partyId: string, data: PartySocketEvent) {
|
||||||
|
pubsub.publish(`party:${partyId}`, JSON.stringify(data));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function broadcastQuizState(ws: any, partyId: string) {
|
async function broadcastQuizState(ws: any, partyId: string) {
|
||||||
|
|
|
||||||
|
|
@ -158,16 +158,7 @@ export const quizRoutes = new Elysia()
|
||||||
).quiz as QuizState | undefined;
|
).quiz as QuizState | undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quiz:
|
quiz: quizData,
|
||||||
quizData ??
|
|
||||||
({
|
|
||||||
status: "idle",
|
|
||||||
workflowId: null,
|
|
||||||
questionIndex: 0,
|
|
||||||
currentQuestion: null,
|
|
||||||
answers: {},
|
|
||||||
scores: {},
|
|
||||||
} satisfies QuizState),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { ConfiguredInstance, DBOS, WorkflowQueue } from "@dbos-inc/dbos-sdk";
|
import { ConfiguredInstance, DBOS, WorkflowQueue } from "@dbos-inc/dbos-sdk";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { party, partyMember } from "../db/schema";
|
import { partyMember } from "../db/schema";
|
||||||
|
import { updatePartyData } from "../party/state";
|
||||||
import type { QuizState } from "../party-types";
|
import type { QuizState } from "../party-types";
|
||||||
import { pubsub } from "../routes/party-socket";
|
|
||||||
|
|
||||||
const TOTAL_QUESTIONS = 5;
|
const TOTAL_QUESTIONS = 5;
|
||||||
const QUESTION_TIMEOUT_SECONDS = 30;
|
|
||||||
|
|
||||||
export const quizQueue = new WorkflowQueue("quiz_queue", {
|
export const quizQueue = new WorkflowQueue("quiz_queue", {
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
|
|
@ -36,7 +35,6 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
|
|
||||||
// Initialize quiz state
|
// Initialize quiz state
|
||||||
await this.updatePartyData(partyId, quizState);
|
await this.updatePartyData(partyId, quizState);
|
||||||
await this.broadcastState(partyId, quizState);
|
|
||||||
|
|
||||||
// Get party members to initialize scores
|
// Get party members to initialize scores
|
||||||
const members = await this.getPartyMembers(partyId);
|
const members = await this.getPartyMembers(partyId);
|
||||||
|
|
@ -52,20 +50,14 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
quizState.answers = {};
|
quizState.answers = {};
|
||||||
|
|
||||||
await this.updatePartyData(partyId, quizState);
|
await this.updatePartyData(partyId, quizState);
|
||||||
await this.broadcastState(partyId, quizState);
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_question`, question);
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_status`, "question");
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_index`, i);
|
|
||||||
|
|
||||||
// Wait for all responses with timeout
|
// Wait for all responses with timeout
|
||||||
const memberIds = new Set(members.map((m) => m.userId));
|
const memberIds = new Set(members.map((m) => m.userId));
|
||||||
const receivedPlayers = new Set<string>();
|
const receivedPlayers = new Set<string>();
|
||||||
|
|
||||||
while (receivedPlayers.size < memberIds.size) {
|
while (receivedPlayers.size < memberIds.size) {
|
||||||
const response = await DBOS.recv<Response>(
|
const response = await DBOS.recv<Response>("quiz_responses", {
|
||||||
"quiz_responses",
|
deadlineEpochMS: question.endTimestamp,
|
||||||
QUESTION_TIMEOUT_SECONDS,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
// Timeout - fill in missing players with no answer
|
// Timeout - fill in missing players with no answer
|
||||||
|
|
@ -77,9 +69,9 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
selected: -1,
|
selected: -1,
|
||||||
correct: false,
|
correct: false,
|
||||||
};
|
};
|
||||||
|
await this.updatePartyData(partyId, quizState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.broadcastState(partyId, quizState);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,23 +84,16 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
quizState.scores[response.playerId] =
|
quizState.scores[response.playerId] =
|
||||||
(quizState.scores[response.playerId] ?? 0) + 10;
|
(quizState.scores[response.playerId] ?? 0) + question.points;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updatePartyData(partyId, quizState);
|
await this.updatePartyData(partyId, quizState);
|
||||||
await this.broadcastState(partyId, quizState);
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_answers`, quizState.answers);
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_scores`, quizState.scores);
|
|
||||||
await DBOS.setEvent(`quiz_q${i}_status`, "results");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quiz complete
|
// Quiz complete
|
||||||
quizState.status = "results";
|
quizState.status = "results";
|
||||||
await this.updatePartyData(partyId, quizState);
|
await this.updatePartyData(partyId, quizState);
|
||||||
await this.broadcastState(partyId, quizState);
|
|
||||||
await DBOS.setEvent("quiz_final_status", "results");
|
|
||||||
await DBOS.setEvent("quiz_final_scores", quizState.scores);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
|
|
@ -116,25 +101,8 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
partyId: string,
|
partyId: string,
|
||||||
quizState: QuizState,
|
quizState: QuizState,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db.transaction(async (tx) => {
|
console.log(partyId, quizState);
|
||||||
const currentParty = await tx
|
await updatePartyData(db, partyId, quizState);
|
||||||
.select({ data: party.data })
|
|
||||||
.from(party)
|
|
||||||
.where(eq(party.id, partyId))
|
|
||||||
.limit(1)
|
|
||||||
.then((rows) => rows[0]);
|
|
||||||
|
|
||||||
if (!currentParty) return;
|
|
||||||
|
|
||||||
const currentData = (currentParty.data ?? {}) as Record<string, unknown>;
|
|
||||||
await tx
|
|
||||||
.update(party)
|
|
||||||
.set({
|
|
||||||
data: { ...currentData, quiz: quizState },
|
|
||||||
lastUpdated: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(party.id, partyId));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
|
|
@ -142,37 +110,46 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
text: string;
|
text: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
correct: number;
|
correct: number;
|
||||||
|
startTimestamp: number;
|
||||||
|
endTimestamp: number;
|
||||||
|
points: number;
|
||||||
}> {
|
}> {
|
||||||
// Placeholder - returns same question for now, question generation comes later
|
// Placeholder - returns same question for now, question generation comes later
|
||||||
const questions: {
|
const questions: {
|
||||||
text: string;
|
text: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
correct: number;
|
correct: number;
|
||||||
|
points: number;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
text: "What is the most common genre in your party's shared taste?",
|
text: "What is the most common genre in your party's shared taste?",
|
||||||
options: ["Hip-Hop", "Rock", "Electronic", "Jazz"],
|
options: ["Hip-Hop", "Rock", "Electronic", "Jazz"],
|
||||||
correct: 0,
|
correct: 0,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Which artist do most party members follow?",
|
text: "Which artist do most party members follow?",
|
||||||
options: ["Artist A", "Artist B", "Artist C", "Artist D"],
|
options: ["Artist A", "Artist B", "Artist C", "Artist D"],
|
||||||
correct: 1,
|
correct: 1,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "What percentage of the party shares at least 1 album?",
|
text: "What percentage of the party shares at least 1 album?",
|
||||||
options: ["0-25%", "25-50%", "50-75%", "75-100%"],
|
options: ["0-25%", "25-50%", "50-75%", "75-100%"],
|
||||||
correct: 2,
|
correct: 2,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Who has the most diverse taste in the party?",
|
text: "Who has the most diverse taste in the party?",
|
||||||
options: ["Player A", "Player B", "Player C", "Player D"],
|
options: ["Player A", "Player B", "Player C", "Player D"],
|
||||||
correct: 0,
|
correct: 0,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Which track appears most in everyone's top 50?",
|
text: "Which track appears most in everyone's top 50?",
|
||||||
options: ["Track A", "Track B", "Track C", "Track D"],
|
options: ["Track A", "Track B", "Track C", "Track D"],
|
||||||
correct: 3,
|
correct: 3,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -180,18 +157,11 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
if (!question) {
|
if (!question) {
|
||||||
throw new Error("Question not found");
|
throw new Error("Question not found");
|
||||||
}
|
}
|
||||||
return question;
|
return {
|
||||||
}
|
...question,
|
||||||
|
startTimestamp: Date.now(),
|
||||||
@DBOS.step()
|
endTimestamp: Date.now() + 60_000,
|
||||||
private async broadcastState(
|
};
|
||||||
partyId: string,
|
|
||||||
quizState: QuizState,
|
|
||||||
): Promise<void> {
|
|
||||||
pubsub.publish(
|
|
||||||
`party:${partyId}`,
|
|
||||||
JSON.stringify({ type: "quiz_state", quiz: quizState }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const SpotifyIconIcon = ({
|
const SpotifyIconIcon = ({
|
||||||
size = undefined,
|
size = undefined,
|
||||||
color = "#000000",
|
color = "#000000",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Button } from "#/components/ui/button";
|
import { Button } from "#/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -59,7 +59,9 @@ export function PartyQr() {
|
||||||
<ItemDescription>Invite someone to your party</ItemDescription>
|
<ItemDescription>Invite someone to your party</ItemDescription>
|
||||||
<ItemActions>
|
<ItemActions>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button size="sm" variant="outline" />}>Show QR</DialogTrigger>
|
<DialogTrigger render={<Button size="sm" variant="outline" />}>
|
||||||
|
Show QR
|
||||||
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Party invite</DialogTitle>
|
<DialogTitle>Party invite</DialogTitle>
|
||||||
|
|
@ -88,7 +90,9 @@ export function PartyQr() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>Close</DialogClose>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
|
Close
|
||||||
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
6
web/src/components/party/party-view.tsx
Normal file
6
web/src/components/party/party-view.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { useParty } from "#/hooks/use-party";
|
||||||
|
|
||||||
|
export function PartyView() {
|
||||||
|
const { party } = useParty();
|
||||||
|
if (!party) return null;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { client } from "#/lib/eden";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Avatar, AvatarGroup, AvatarImage } from "#/components/ui/avatar";
|
||||||
|
import { Badge } from "#/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
|
@ -7,7 +8,6 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "#/components/ui/card";
|
} from "#/components/ui/card";
|
||||||
import { Avatar, AvatarGroup, AvatarImage } from "#/components/ui/avatar";
|
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
|
|
@ -16,8 +16,8 @@ import {
|
||||||
ItemMedia,
|
ItemMedia,
|
||||||
ItemTitle,
|
ItemTitle,
|
||||||
} from "#/components/ui/item";
|
} from "#/components/ui/item";
|
||||||
import { Badge } from "#/components/ui/badge";
|
|
||||||
import { Spinner } from "#/components/ui/spinner";
|
import { Spinner } from "#/components/ui/spinner";
|
||||||
|
import { client } from "#/lib/eden";
|
||||||
import { Section, SectionTitle } from "./ui/section";
|
import { Section, SectionTitle } from "./ui/section";
|
||||||
|
|
||||||
const MAX_AVATARS = 6;
|
const MAX_AVATARS = 6;
|
||||||
|
|
|
||||||
25
web/src/components/start-party.tsx
Normal file
25
web/src/components/start-party.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useParty } from "#/hooks/use-party";
|
||||||
|
import { client } from "#/lib/eden";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Empty, EmptyContent, EmptyHeader, EmptyTitle } from "./ui/empty";
|
||||||
|
|
||||||
|
export function StartParty() {
|
||||||
|
const { party } = useParty();
|
||||||
|
if (!party) return null;
|
||||||
|
return (
|
||||||
|
<Empty>
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyTitle>Start party</EmptyTitle>
|
||||||
|
</EmptyHeader>
|
||||||
|
<EmptyContent>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
client.api.party({ partyId: party.id }).quiz.start.post()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { client } from "#/lib/eden";
|
import { client } from "#/lib/eden";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Item, ItemActions, ItemDescription, ItemTitle } from "./ui/item";
|
import { Item, ItemActions, ItemDescription } from "./ui/item";
|
||||||
|
|
||||||
export function SyncButton() {
|
export function SyncButton() {
|
||||||
const query = useQueryClient();
|
const query = useQueryClient();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useParty } from "#/hooks/use-party";
|
import { useParty } from "#/hooks/use-party";
|
||||||
import { useUser } from "#/hooks/user";
|
import { useUser } from "#/hooks/user";
|
||||||
import { initials } from "#/lib/utils";
|
import { initials } from "#/lib/utils";
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export function usePartySocket({
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const parsed = JSON.parse(event.data) as PartySocketEvent;
|
const parsed = JSON.parse(event.data) as PartySocketEvent;
|
||||||
|
console.log(parsed);
|
||||||
handlerRef.current?.(parsed);
|
handlerRef.current?.(parsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import type {
|
import type {
|
||||||
PartyMember,
|
|
||||||
PartySocketEvent,
|
PartySocketEvent,
|
||||||
PartyState,
|
PartyState,
|
||||||
} from "../../../api/src/party-types";
|
} from "../../../api/src/party-types";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
import type { AuthSession } from "./auth.serverfn";
|
import type { AuthSession } from "./auth.serverfn";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export const authClient = createAuthClient();
|
export const authClient = createAuthClient();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,22 @@ import {
|
||||||
Scripts,
|
Scripts,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
|
||||||
import TanStackQueryDevtools from "../integrations/tanstack-query/devtools";
|
|
||||||
import appCss from "../styles.css?url";
|
|
||||||
import type { AuthSession } from "#/lib/auth.serverfn";
|
|
||||||
import { fetchSession, sessionQueryKey } from "#/lib/auth-client";
|
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { AuthSession } from "#/lib/auth.serverfn";
|
||||||
|
import { fetchSession, sessionQueryKey } from "#/lib/auth-client";
|
||||||
import { client } from "#/lib/eden";
|
import { client } from "#/lib/eden";
|
||||||
import {
|
import {
|
||||||
clearPendingPartyJoin,
|
|
||||||
clearJoinIdFromLocation,
|
clearJoinIdFromLocation,
|
||||||
|
clearPendingPartyJoin,
|
||||||
getJoinIdFromLocation,
|
getJoinIdFromLocation,
|
||||||
readPendingPartyJoin,
|
readPendingPartyJoin,
|
||||||
writePendingPartyJoin,
|
writePendingPartyJoin,
|
||||||
} from "#/lib/party-join";
|
} from "#/lib/party-join";
|
||||||
import { toast } from "sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import TanStackQueryDevtools from "../integrations/tanstack-query/devtools";
|
||||||
|
import appCss from "../styles.css?url";
|
||||||
|
|
||||||
interface MyRouterContext {
|
interface MyRouterContext {
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
|
|
@ -39,7 +39,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
content: "width=device-width, initial-scale=1",
|
content: "width=device-width, initial-scale=1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "TanStack Start Starter",
|
title: "Music Quiz",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
|
|
@ -111,7 +111,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
toast.success("Joined party.");
|
toast.success("Joined party.");
|
||||||
clearPendingPartyJoin();
|
clearPendingPartyJoin();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
toast.error("Failed to join party.");
|
toast.error("Failed to join party.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { PartyView } from "#/components/party/party-view";
|
||||||
import { PartyQr } from "#/components/party-qr";
|
import { PartyQr } from "#/components/party-qr";
|
||||||
|
import { StartParty } from "#/components/start-party";
|
||||||
import { SyncButton } from "#/components/sync-button";
|
import { SyncButton } from "#/components/sync-button";
|
||||||
import { MainContent } from "#/components/ui/main-content";
|
import { MainContent } from "#/components/ui/main-content";
|
||||||
import { UserInfo } from "#/components/user-info";
|
import { UserInfo } from "#/components/user-info";
|
||||||
|
import { useParty } from "#/hooks/use-party";
|
||||||
import { useUser } from "#/hooks/user";
|
import { useUser } from "#/hooks/user";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({ component: App });
|
export const Route = createFileRoute("/")({ component: App });
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const { party, members } = useParty();
|
||||||
return (
|
return (
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
{!user?.lastSyncAt && <SyncButton />}
|
{!user?.lastSyncAt && <SyncButton />}
|
||||||
{user && <PartyQr />}
|
{user && <PartyQr />}
|
||||||
|
{party && !party.data && members.length > 1 && <StartParty />}
|
||||||
|
{party?.data && <PartyView />}
|
||||||
</MainContent>
|
</MainContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ const config = defineConfig({
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:4000",
|
target: "http://localhost:4000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) =>
|
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||||
path.replace(/^\/api/, "/api"),
|
|
||||||
},
|
},
|
||||||
"/api/party-socket/ws": {
|
"/api/party-socket/ws": {
|
||||||
target: "ws://localhost:4000",
|
target: "ws://localhost:4000",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue