Skip to content

Raid progress #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/src/api/schema.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
22 changes: 3 additions & 19 deletions web/src/components/auth/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
53 changes: 34 additions & 19 deletions web/src/components/party/codes/PartyProgress.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -14,19 +15,24 @@ export const PartyProgress: FC<{ party_id: string }> = ({ party_id }) => {

return (
<>
<PartyProgressList party_id={party_id} orderedCodes={orderedCodes} triedCodes={triedCodes} percentages={percentages} codes={codes} />
<PartyProgressList
party_id={party_id}
orderedCodes={orderedCodes}
triedCodes={triedCodes}
percentages={percentages}
codes={codes}
/>
</>
);
};

export const PartyProgressList: FC<{
party_id: string,
orderedCodes: string[],
triedCodes: Map<string, PartyEvent[]>,
percentages: Map<string, number>,
codes: string[]
party_id: string;
orderedCodes: string[];
triedCodes: Map<string, PartyEvent[]>;
percentages: Map<string, number>;
codes: string[];
}> = ({ party_id, orderedCodes, triedCodes, percentages, codes }) => {

const parentRef = useRef<HTMLDivElement>(null);

// Constants for item sizing
Expand Down Expand Up @@ -105,6 +111,20 @@ export const PartyProgressList: FC<{
[columnCount]
);

const visibleCodes: Map<string, string[]> = 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(
Expand Down Expand Up @@ -177,16 +197,11 @@ export const PartyProgressList: FC<{
height: `${ITEM_HEIGHT}px`,
}}
>
<div
className={cx(
'flex justify-center items-center w-full h-full rounded-sm text-[0.8rem]',
triedCodes.has(cell.code)
? 'bg-accent text-primary'
: 'bg-tertiary text-secondary'
)}
>
{cell.code}
</div>
<ProgressCell
visibleCodes={visibleCodes}
code={cell.code}
triedCodes={triedCodes}
/>
</div>
)
)}
Expand Down
62 changes: 62 additions & 0 deletions web/src/components/party/codes/ProgressCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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<string, PartyEvent[]>;
visibleCodes: Map<string, string[]>;
}> = ({ 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 (
<div
className={cx(
'flex justify-center items-center w-full h-full rounded-sm text-[0.8rem]',
triedCodes.has(code) ? 'bg-tertiary text-white' : 'bg-tertiary text-secondary'
)}
style={{
...(triedCodes.has(code)
? {
backgroundColor: backgroundColorBySeed(
(triedCodes.get(code) || [])[0]?.user_id,
{
saturation: 25,
lightness: 40,
}
),
}
: {}),
...(userId
? {
borderBottom: `2px solid ${backgroundColorBySeed(userId)}`,
}
: {}),
}}
>
<div
style={
triedCodes.has(code)
? {
backgroundColor: backgroundColorBySeed(
(triedCodes.get(code) || [])[0]?.user_id,
{
saturation: 25,
lightness: 40,
}
),
borderRadius: '2px',
}
: {}
}
>
{code}
</div>
</div>
);
};
25 changes: 25 additions & 0 deletions web/src/util/user.ts
Original file line number Diff line number Diff line change
@@ -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}%)`;
};