Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 13 additions & 2 deletions openapi/playerz-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -24044,7 +24044,7 @@
},
"venue": {
"type": "object",
"required": ["id", "slug", "name", "city", "logoUrl"],
"required": ["id", "slug", "name", "city", "cityKey", "cityNames", "logoUrl"],
"properties": {
"id": {
"type": "string"
Expand All @@ -24058,6 +24058,13 @@
"city": {
"type": "string"
},
"cityKey": {
"type": ["string", "null"],
"description": "The canonical city (#667)."
},
"cityNames": {
"$ref": "#/components/schemas/CityNames"
},
"logoUrl": {
"type": ["string", "null"],
"description": "The club's logo (#668)."
Expand Down Expand Up @@ -28294,7 +28301,7 @@
"OpenGamePlayer": {
"type": "object",
"description": "One person on a game (#668). Never carries a user id.",
"required": ["firstName", "avatarUrl", "level", "isBooker"],
"required": ["firstName", "avatarUrl", "level", "isBooker", "isYou"],
"properties": {
"firstName": {
"type": ["string", "null"]
Expand All @@ -28308,6 +28315,10 @@
},
"isBooker": {
"type": "boolean"
},
"isYou": {
"type": "boolean",
"description": "The viewer themselves, so their seat can say «Вие»."
}
}
},
Expand Down
15 changes: 14 additions & 1 deletion src/app-layer/usecases/open-play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export interface OpenGamePlayer {
/** Their level in the game's sport, as they let others see it; null when hidden or none. */
level: number | null;
isBooker: boolean;
/** The viewer themselves, so their seat can say «Вие». */
isYou: boolean;
}

export interface OpenGameSummary {
Expand All @@ -176,6 +178,8 @@ export interface OpenGameSummary {
slug: string;
name: string;
city: string;
/** The canonical city (#667), or null for one outside the list. */
cityKey: string | null;
/** The club's logo (#668), or null. */
logoUrl: string | null;
};
Expand Down Expand Up @@ -254,6 +258,7 @@ const GAME_SELECT = {
select: {
id: true,
tenantId: true,
cityKey: true,
slug: true,
name: true,
city: true,
Expand Down Expand Up @@ -387,6 +392,7 @@ async function summarise(
avatarUrl: live ? avatarUrlOf(live.avatarUrl) : null,
level: rating ? rating.level : null,
isBooker,
isYou: userId === viewerId,
};
};
const playersOf = (g: GameRow): OpenGamePlayer[] | null => {
Expand All @@ -398,7 +404,13 @@ async function summarise(
...b.participants.map((p) =>
p.userId
? person(p.userId, sport, false)
: { firstName: firstName(p.guestName), avatarUrl: null, level: null, isBooker: false },
: {
firstName: firstName(p.guestName),
avatarUrl: null,
level: null,
isBooker: false,
isYou: false,
},
),
];
};
Expand All @@ -423,6 +435,7 @@ async function summarise(
slug: v.slug,
name: v.name,
city: v.city,
cityKey: v.cityKey,
logoUrl: logos.get(v.tenantId) ?? null,
},
courtPrice: { totalCents: b.totalCents, currency: b.currency },
Expand Down
16 changes: 14 additions & 2 deletions src/app/api/v1/_lib/open-play-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type {
OpenGameSummary,
RequestCard,
} from '@/app-layer/usecases/open-play';
import type { CityKey } from '@/lib/geo/cities';

import { cityNames, type CityNames } from './city-names';

/**
* Wire shapes for open play (#376). Dates are ISO strings. What anyone sees
Expand All @@ -23,7 +26,16 @@ export interface OpenGameDto {
resourceType: string;
courtName: string;
isIndoor: boolean;
venue: { id: string; slug: string; name: string; city: string; logoUrl: string | null };
venue: {
id: string;
slug: string;
name: string;
city: string;
/** The canonical city and its names (#667), as on a venue card. */
cityKey: string | null;
cityNames: CityNames;
logoUrl: string | null;
};
courtPrice: { totalCents: number; currency: string };
/** Booker first; null unless the viewer books, plays or has asked (#668). */
players: OpenGamePlayer[] | null;
Expand Down Expand Up @@ -56,7 +68,7 @@ export function toOpenGameDto(g: OpenGameSummary): OpenGameDto {
resourceType: g.resourceType,
courtName: g.courtName,
isIndoor: g.isIndoor,
venue: g.venue,
venue: { ...g.venue, cityNames: cityNames(g.venue.city, g.venue.cityKey as CityKey | null) },
courtPrice: g.courtPrice,
players: g.players,
startTs: g.startTs.toISOString(),
Expand Down
10 changes: 8 additions & 2 deletions tests/integration/open-play.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe('open play (#376)', () => {
expect((await card(null)).players).toBeNull();
expect((await card(other)).players).toBeNull();
expect((await card(booker)).players).toEqual([
{ firstName: 'Иван', avatarUrl: null, level: null, isBooker: true },
{ firstName: 'Иван', avatarUrl: null, level: null, isBooker: true, isYou: true },
]);

// Asking shows who is on it; still only the booker.
Expand All @@ -239,6 +239,9 @@ describe('open play (#376)', () => {
await answer(booker, id, requests[0]!.id, 'accept');
const seen = (await card(booker)).players!;
expect(seen.map((p) => p.firstName)).toEqual(['Иван', 'Мария']);
// Each sees their own seat as theirs.
const mine = (await card(asker)).players as unknown as Array<{ isYou: boolean }>;
expect(mine.map((p) => p.isYou)).toEqual([false, true]);
expect((await card(other)).players).toBeNull();

// Declined: back to counts only.
Expand All @@ -261,7 +264,10 @@ describe('open play (#376)', () => {
expect(await card(null)).toMatchObject({
courtPrice: { totalCents: 2400, currency: 'EUR' },
isIndoor: false,
venue: { logoUrl: 'https://cdn.test/l.webp' },
venue: {
logoUrl: 'https://cdn.test/l.webp',
cityNames: { bg: expect.any(String), en: expect.any(String) },
},
});
});

Expand Down
10 changes: 9 additions & 1 deletion tests/rendered/open-play.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ const GAME: OpenGameDto = {
resourceType: 'COURT',
courtName: 'Корт 1',
isIndoor: false,
venue: { id: 'v1', slug: 'alfa', name: 'Алфа Кортове', city: 'Sofia', logoUrl: null },
venue: {
id: 'v1',
slug: 'alfa',
name: 'Алфа Кортове',
city: 'Sofia',
cityKey: 'sofia',
cityNames: { bg: 'София', en: 'Sofia' },
logoUrl: null,
},
courtPrice: { totalCents: 4000, currency: 'EUR' },
players: null,
startTs: '2026-10-20T16:00:00.000Z',
Expand Down
Loading