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}%)`; +};