diff --git a/web/src/api/schema.gen.ts b/web/src/api/schema.gen.ts
index 0d2f2ff..d1fd8bb 100644
--- a/web/src/api/schema.gen.ts
+++ b/web/src/api/schema.gen.ts
@@ -818,8 +818,8 @@ export interface components {
* "name": "John D.",
* "avatar_url": "https://avatars.akamai.steamstatic.com/0000000000000000.jpg",
* "profile_url": "https://steamcommunity.com/id/john_doe",
- * "created_at": "2025-04-09T01:04:16.289611048+00:00",
- * "updated_at": "2025-04-09T01:04:16.289614158+00:00"
+ * "created_at": "2025-04-09T14:24:33.977349865+00:00",
+ * "updated_at": "2025-04-09T14:24:33.977352725+00:00"
* }
*/
User: {
diff --git a/web/src/components/auth/Avatar.tsx b/web/src/components/auth/Avatar.tsx
index a935db5..4d2b9de 100644
--- a/web/src/components/auth/Avatar.tsx
+++ b/web/src/components/auth/Avatar.tsx
@@ -2,28 +2,12 @@ import * as RadixAvatar from '@radix-ui/react-avatar';
import { FC, useMemo } from 'react';
import { FaCat } from 'react-icons/fa';
+import { backgroundColorBySeed } from '@/util/user';
+
export const Avatar: FC<{ src?: string; seed?: string }> = ({ src, seed }) => {
// Generate a deterministic color based on the seed string
const backgroundColor = useMemo(() => {
- if (!seed) return '#e2e8f0'; // Default color if no seed
-
- // Simple hash function to generate a color from the seed
- let hash = 0;
-
- for (let i = 0; i < seed.length; i++) {
- hash = seed.charCodeAt(i) + ((hash << 5) - hash);
- }
-
- // Convert to hex color
- let color = '#';
-
- for (let i = 0; i < 3; i++) {
- const value = (hash >> (i * 8)) & 0xff;
-
- color += ('00' + value.toString(16)).slice(-2);
- }
-
- return color;
+ return backgroundColorBySeed(seed);
}, [seed]);
return (
diff --git a/web/src/components/party/codes/PartyProgress.tsx b/web/src/components/party/codes/PartyProgress.tsx
index 058c598..153995c 100644
--- a/web/src/components/party/codes/PartyProgress.tsx
+++ b/web/src/components/party/codes/PartyProgress.tsx
@@ -1,11 +1,12 @@
import { useVirtualizer } from '@tanstack/react-virtual';
-import cx from 'classnames';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { PartyEvent } from '@/api/party/events';
+import { PartyEvent, usePartyEvents } from '@/api/party/events';
import { usePartyCodes, usePartyProgress } from '@/api/progress';
import { Tooltip } from '@/components/helpers/Tooltip';
+import { ProgressCell } from './ProgressCell';
+
export const PartyProgress: FC<{ party_id: string }> = ({ party_id }) => {
const { data: orderedCodes } = usePartyCodes(party_id);
const { triedCodes, percentages } = usePartyProgress(party_id);
@@ -14,19 +15,24 @@ export const PartyProgress: FC<{ party_id: string }> = ({ party_id }) => {
return (
<>
-
+
>
);
};
export const PartyProgressList: FC<{
- party_id: string,
- orderedCodes: string[],
- triedCodes: Map,
- percentages: Map,
- codes: string[]
+ party_id: string;
+ orderedCodes: string[];
+ triedCodes: Map;
+ percentages: Map;
+ codes: string[];
}> = ({ party_id, orderedCodes, triedCodes, percentages, codes }) => {
-
const parentRef = useRef(null);
// Constants for item sizing
@@ -105,6 +111,20 @@ export const PartyProgressList: FC<{
[columnCount]
);
+ const visibleCodes: Map = new Map();
+
+ usePartyEvents(party_id, (event) => {
+ return event.data.type === 'PartyCursorUpdate';
+ }).events.forEach((event) => {
+ if (event.data.type === 'PartyCursorUpdate') {
+ const cursor = parseInt(event.data.cursor, 10);
+ const { size } = event.data;
+ const codes = orderedCodes.slice(cursor, cursor + size);
+
+ visibleCodes.set(event.user_id, codes);
+ }
+ });
+
// Memoize virtual cells to prevent unnecessary re-renders
const virtualCells = useMemo(() => {
return rowVirtualizer.getVirtualItems().flatMap(
@@ -177,16 +197,11 @@ export const PartyProgressList: FC<{
height: `${ITEM_HEIGHT}px`,
}}
>
-
- {cell.code}
-
+
)
)}
diff --git a/web/src/components/party/codes/ProgressCell.tsx b/web/src/components/party/codes/ProgressCell.tsx
new file mode 100644
index 0000000..3fc4c7e
--- /dev/null
+++ b/web/src/components/party/codes/ProgressCell.tsx
@@ -0,0 +1,45 @@
+import cx from 'classnames';
+import { FC } from 'react';
+
+import { PartyEvent } from '@/api/party/events';
+import { backgroundColorBySeed } from '@/util/user';
+
+export const ProgressCell: FC<{
+ code: string;
+ triedCodes: Map;
+ visibleCodes: Map;
+}> = ({ code, triedCodes, visibleCodes }) => {
+ // Visible codes key is user_id, value is an array of codes. Find first user_id that has this code
+ const userId = Array.from(visibleCodes.entries()).find(([_, codes]) =>
+ codes.includes(code)
+ )?.[0];
+
+ return (
+
+ {code}
+
+ );
+};
diff --git a/web/src/util/user.ts b/web/src/util/user.ts
new file mode 100644
index 0000000..6d13372
--- /dev/null
+++ b/web/src/util/user.ts
@@ -0,0 +1,25 @@
+export const backgroundColorBySeed = (
+ seed?: string,
+ {
+ saturation = 75,
+ lightness = 60,
+ }: {
+ saturation?: number;
+ lightness?: number;
+ } = { saturation: 75, lightness: 60 }
+) => {
+ const hash = (str: string) => {
+ let hash = 0;
+
+ for (let i = 0; i < str.length; i++) {
+ hash = (hash << 5) - hash + str.charCodeAt(i);
+ hash |= 0; // Convert to 32bit integer
+ }
+
+ return Math.abs(hash % 360);
+ };
+
+ const hue = seed ? hash(seed) : 0;
+
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+};