Skip to content

Commit

Permalink
feat(v2): vote and estimation tracking
Browse files Browse the repository at this point in the history
The RoomState TRPC endpoints now are used to track those events.
  • Loading branch information
jkrumm committed Jan 19, 2024
1 parent 91b72b4 commit 07da824
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 260 deletions.
4 changes: 1 addition & 3 deletions src/hooks/use-room-state.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ export const useRoomState = ({
},
{
onSuccess: (roomStateJson) => {
updateRoomState(
RoomStateClient.fromJson(roomStateJson as RoomStateDto),
);
updateRoomState(RoomStateClient.fromJson(roomStateJson));
setIsConnecting(false);
},
},
Expand Down
80 changes: 0 additions & 80 deletions src/pages/api/track-estimation.ts

This file was deleted.

45 changes: 1 addition & 44 deletions src/server/api/routers/vote.router.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,11 @@
import { createTRPCRouter, publicProcedure } from "fpp/server/api/trpc";
import { z } from "zod";
import { fibonacciSequence } from "fpp/constants/fibonacci.constant";
import { DateTime } from "luxon";
import { type ICreateVote, votes } from "fpp/server/db/schema";
import { votes } from "fpp/server/db/schema";
import { sql } from "drizzle-orm";
import { round } from "fpp/utils/number.utils";
import { countTable } from "fpp/utils/db-api.util";

export const voteRouter = createTRPCRouter({
vote: publicProcedure
.input(
z.object({
roomId: z.number(),
estimations: z
.array(z.number())
.min(1)
.refine(
(values) =>
values.every((value) => fibonacciSequence.includes(value)),
{
message: "Every vote must be a valid fibonacci number",
},
),
duration: z.number().min(0),
amountOfSpectators: z.number().min(0),
}),
)
.mutation(
async ({
ctx: { db },
input: { roomId, estimations, duration, amountOfSpectators },
}) => {
const vote: ICreateVote = {
roomId,
avgEstimation: String(
estimations.reduce((a, b) => a + b, 0) / estimations.length,
),
maxEstimation: String(
estimations.reduce((a, b) => Math.max(a, b), 1),
),
minEstimation: String(
estimations.reduce((a, b) => Math.min(a, b), 21),
),
amountOfEstimations: String(estimations.length),
amountOfSpectators,
duration,
};
await db.insert(votes).values(vote);
},
),
getVotes: publicProcedure.query(async ({ ctx: { db } }) => {
const averages = (await db.execute(sql`
SELECT AVG(avg_estimation) as avg_avgEstimation,
Expand Down
15 changes: 3 additions & 12 deletions src/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,9 @@ export const votes = mysqlTable("votes", {
precision: 4,
scale: 2,
}).notNull(),
maxEstimation: decimal("max_estimation", {
precision: 4,
scale: 2,
}).notNull(),
minEstimation: decimal("min_estimation", {
precision: 4,
scale: 2,
}).notNull(),
amountOfEstimations: decimal("amount_of_estimations", {
precision: 4,
scale: 2,
}).notNull(),
maxEstimation: smallint("max_estimation").notNull(),
minEstimation: smallint("min_estimation").notNull(),
amountOfEstimations: smallint("amount_of_estimations").notNull(),
amountOfSpectators: smallint("amount_of_spectators").notNull(),
duration: smallint("duration").notNull(),
votedAt: timestamp("voted_at").defaultNow().notNull(),
Expand Down
80 changes: 59 additions & 21 deletions src/server/room-state/room-state.entity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type ICreateVote } from "fpp/server/db/schema";

export interface CreateUserDto {
id: string;
name: string;
Expand All @@ -7,10 +9,12 @@ export interface CreateUserDto {

export interface RoomStateDto {
id: number;
startedAt: number;
lastUpdated: number;
users: CreateUserDto[];
isFlipped: boolean;
isAutoFlip: boolean;
isFlippable: boolean;
}

export class User {
Expand Down Expand Up @@ -39,29 +43,20 @@ export class User {

class RoomStateBase {
readonly id: number;
startedAt: number;
lastUpdated: number;

users: User[] = [];
isFlipped = false;
isAutoFlip = false;

static fromJson<T extends RoomStateBase>(
this: new (id: number) => T,
roomStateJson: RoomStateDto,
) {
const roomState = new this(roomStateJson.id);
roomState.lastUpdated = roomStateJson.lastUpdated;
roomState.users = roomStateJson.users.map((user) => new User(user));
roomState.isFlipped = roomStateJson.isFlipped;
roomState.isAutoFlip = roomStateJson.isAutoFlip;
return roomState;
}

get isFlippable() {
return (
this.users.every(
(user) => user.estimation !== null || user.isSpectator,
) && this.users.some((user) => !user.isSpectator)
) &&
this.users.some((user) => !user.isSpectator) &&
!this.isFlipped
);
}

Expand All @@ -78,11 +73,28 @@ class RoomStateBase {

constructor(id: number) {
this.id = id;
this.startedAt = Date.now();
this.lastUpdated = Date.now();
}

toJson() {
static fromJson<T extends RoomStateBase>(
this: new (id: number) => T,
roomStateJson: RoomStateDto,
) {
const roomState = new this(roomStateJson.id);
roomState.startedAt = roomStateJson.startedAt;
roomState.lastUpdated = roomStateJson.lastUpdated;
roomState.users = roomStateJson.users.map((user) => new User(user));
roomState.isFlipped = roomStateJson.isFlipped;
roomState.isAutoFlip = roomStateJson.isAutoFlip;
return roomState;
}

toJson(): RoomStateDto {
return {
id: this.id,
startedAt: this.startedAt,
lastUpdated: this.lastUpdated,
users: this.users,
isFlipped: this.isFlipped,
isAutoFlip: this.isAutoFlip,
Expand Down Expand Up @@ -122,6 +134,7 @@ export class RoomStateClient extends RoomStateBase {

export class RoomStateServer extends RoomStateBase {
hasChanged = false;
isFlipAction = false;

/**
/* USER MANAGEMENT
Expand Down Expand Up @@ -151,6 +164,7 @@ export class RoomStateServer extends RoomStateBase {
this.users = this.users.map((user) => {
if (user.id === userId) {
user.estimation = estimation;
user.isSpectator = false;
this.hasChanged = true;
}
return user;
Expand All @@ -162,6 +176,7 @@ export class RoomStateServer extends RoomStateBase {
this.users = this.users.map((user) => {
if (user.id === userId) {
user.isSpectator = isSpectator;
user.estimation = null;
this.hasChanged = true;
}
return user;
Expand All @@ -175,10 +190,11 @@ export class RoomStateServer extends RoomStateBase {
}
this.isFlipped = true;
this.hasChanged = true;
this.isFlipAction = true;
}

private autoFlip() {
if (this.isFlippable && this.isAutoFlip) {
if (this.isAutoFlip && this.isFlippable && !this.isFlipped) {
this.flip();
}
}
Expand All @@ -190,19 +206,41 @@ export class RoomStateServer extends RoomStateBase {
}

reset() {
this.startedAt = Date.now();
this.lastUpdated = Date.now();
this.users = this.users.map((user) => {
user.estimation = null;
return user;
});
this.isFlipped = false;
this.isAutoFlip = false;
this.hasChanged = true;
}

fullReset() {
this.users = [];
this.isFlipped = false;
this.isAutoFlip = false;
this.hasChanged = true;
/**
* ANALYTICS
*/
calculateVote(): ICreateVote {
const estimations = this.users
.map((user) => user.estimation)
.filter((estimation) => estimation !== null) as number[];
if (estimations.length === 0) {
throw new Error("Cannot calculateCreateVote when no estimations");
}

return {
roomId: this.id,
avgEstimation: String(
estimations.reduce((a, b) => a + b, 0) / estimations.length,
),
maxEstimation: String(
estimations.reduce((a, b) => Math.max(a, b), 1),
) as unknown as number,
minEstimation: String(
estimations.reduce((a, b) => Math.min(a, b), 21),
) as unknown as number,
amountOfEstimations: String(estimations.length) as unknown as number,
amountOfSpectators: this.users.filter((user) => user.isSpectator).length,
duration: Math.ceil((Date.now() - this.startedAt) / 1000),
};
}
}
Loading

0 comments on commit 07da824

Please sign in to comment.