@oddsmith/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eleventy.cjs +14 -0
- package/LICENSE +28 -0
- package/README.md +118 -0
- package/custom-elements.json +1539 -0
- package/docs/_README/index.html +4 -0
- package/docs/api/index.html +2100 -0
- package/docs/components.bundle.js +1669 -0
- package/docs/components.bundle.js.map +1 -0
- package/docs/docs.css +162 -0
- package/docs/examples/index.html +56 -0
- package/docs/index.html +53 -0
- package/docs/install/index.html +45 -0
- package/docs/prism-okaidia.css +123 -0
- package/docs-src/.nojekyll +0 -0
- package/docs-src/_README.md +7 -0
- package/docs-src/_data/api.11tydata.js +8 -0
- package/docs-src/_includes/example.11ty.js +35 -0
- package/docs-src/_includes/footer.11ty.js +6 -0
- package/docs-src/_includes/header.11ty.js +7 -0
- package/docs-src/_includes/nav.11ty.js +11 -0
- package/docs-src/_includes/page.11ty.js +32 -0
- package/docs-src/_includes/relative-path.cjs +9 -0
- package/docs-src/api.11ty.js +85 -0
- package/docs-src/bundle.ts +9 -0
- package/docs-src/docs.css +162 -0
- package/docs-src/examples/index.md +15 -0
- package/docs-src/index.md +39 -0
- package/docs-src/install.md +28 -0
- package/docs-src/package.json +3 -0
- package/index.html +19 -0
- package/karma.conf.cjs +24 -0
- package/main.css +210 -0
- package/main.ts +124 -0
- package/package.json +86 -0
- package/previews/casino.ts +12 -0
- package/previews/catalog.ts +94 -0
- package/previews/leaderboard-v1.ts +12 -0
- package/previews/leaderboard-v2.ts +17 -0
- package/previews/sample-data.ts +101 -0
- package/previews/sf-leaderboard.ts +100 -0
- package/previews/sf-live-feed.ts +15 -0
- package/previews/streaks.ts +40 -0
- package/previews/types.ts +18 -0
- package/src/components/README.md +16 -0
- package/src/components/casino-leaderboard/casino-leaderboard.html +80 -0
- package/src/components/casino-leaderboard/casino-leaderboard.scss +585 -0
- package/src/components/casino-leaderboard/casino-leaderboard.ts +136 -0
- package/src/components/casino-leaderboard/data.ts +111 -0
- package/src/components/casino-leaderboard/index.ts +5 -0
- package/src/components/casino-leaderboard/todo.txt +2 -0
- package/src/components/casino-leaderboard/types.ts +19 -0
- package/src/components/leaderboard/components/leaderboard.ts +373 -0
- package/src/components/leaderboard/components/player-card.ts +342 -0
- package/src/components/leaderboard/components/ui.ts +452 -0
- package/src/components/leaderboard/data.ts +152 -0
- package/src/components/leaderboard/index.ts +2 -0
- package/src/components/leaderboard/main.ts +42 -0
- package/src/components/leaderboard/styles.ts +67 -0
- package/src/components/leaderboard/types.ts +28 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard-player.ts +451 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard-ui.ts +512 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard.ts +205 -0
- package/src/components/leaderboard-v2/constants.ts +16 -0
- package/src/components/leaderboard-v2/demo/sample-data.ts +152 -0
- package/src/components/leaderboard-v2/events.ts +13 -0
- package/src/components/leaderboard-v2/icons.ts +22 -0
- package/src/components/leaderboard-v2/index.ts +23 -0
- package/src/components/leaderboard-v2/sf-leaderboard.html +1 -0
- package/src/components/leaderboard-v2/sf-leaderboard.scss +382 -0
- package/src/components/leaderboard-v2/tokens.ts +35 -0
- package/src/components/leaderboard-v2/types.ts +30 -0
- package/src/components/sf-leaderboard/index.ts +77 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.host.ts +3 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.html +3 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.scss +18 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.ts +22 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.host.ts +14 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.html +27 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.scss +189 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.ts +70 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.host.ts +22 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.html +38 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.scss +99 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.ts +121 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.host.ts +8 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.html +6 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.scss +44 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.ts +41 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.host.ts +17 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.html +19 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.scss +37 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.ts +108 -0
- package/src/components/sf-leaderboard/services/index.ts +22 -0
- package/src/components/sf-leaderboard/services/sf-leaderboard-data.service.ts +54 -0
- package/src/components/sf-leaderboard/services/sf-leaderboard.state.ts +160 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.host.ts +7 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.html +10 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.scss +180 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.ts +88 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.host.ts +12 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.html +22 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.scss +122 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.ts +75 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.host.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.html +5 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.scss +81 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.ts +34 -0
- package/src/components/sf-leaderboard/shared/components/podium/map-players.ts +24 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.host.ts +10 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.html +53 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.scss +580 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.ts +49 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.types.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.host.ts +11 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.html +9 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.scss +98 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.ts +63 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.host.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.html +15 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.scss +210 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.ts +36 -0
- package/src/components/sf-leaderboard/shared/components/table/table.host.ts +5 -0
- package/src/components/sf-leaderboard/shared/components/table/table.html +11 -0
- package/src/components/sf-leaderboard/shared/components/table/table.scss +212 -0
- package/src/components/sf-leaderboard/shared/components/table/table.ts +111 -0
- package/src/components/sf-leaderboard/shared/constants/defaults.ts +7 -0
- package/src/components/sf-leaderboard/shared/constants/filters.ts +16 -0
- package/src/components/sf-leaderboard/shared/constants/index.ts +5 -0
- package/src/components/sf-leaderboard/shared/constants/player-stats.ts +3 -0
- package/src/components/sf-leaderboard/shared/constants/stats-overview.ts +38 -0
- package/src/components/sf-leaderboard/shared/constants/tags.ts +16 -0
- package/src/components/sf-leaderboard/shared/styles/_section.scss +35 -0
- package/src/components/sf-leaderboard/shared/types/data.ts +29 -0
- package/src/components/sf-leaderboard/shared/types/events.ts +30 -0
- package/src/components/sf-leaderboard/shared/types/player-stats.ts +3 -0
- package/src/components/sf-leaderboard/shared/types/sections.ts +100 -0
- package/src/components/sf-leaderboard/shared/utils/utils.ts +17 -0
- package/src/components/sf-leaderboard/theme/THEMING.md +54 -0
- package/src/components/sf-leaderboard/theme/context.ts +16 -0
- package/src/components/sf-leaderboard/theme/default-theme.ts +4 -0
- package/src/components/sf-leaderboard/theme/hex-to-rgb.ts +25 -0
- package/src/components/sf-leaderboard/theme/index.ts +18 -0
- package/src/components/sf-leaderboard/theme/inject-theme.ts +39 -0
- package/src/components/sf-leaderboard/theme/load-theme.ts +26 -0
- package/src/components/sf-leaderboard/theme/merge-theme.ts +59 -0
- package/src/components/sf-leaderboard/theme/scss/_colors.scss +101 -0
- package/src/components/sf-leaderboard/theme/scss/shared.scss +123 -0
- package/src/components/sf-leaderboard/theme/styles.ts +6 -0
- package/src/components/sf-leaderboard/theme/theme-to-css-vars.ts +99 -0
- package/src/components/sf-leaderboard/theme/themes/fallback.json +62 -0
- package/src/components/sf-leaderboard/theme/themes/red.json +62 -0
- package/src/components/sf-leaderboard/theme/types.ts +71 -0
- package/src/components/sf-live-feed/components/avatar/avatar.host.ts +5 -0
- package/src/components/sf-live-feed/components/avatar/avatar.html +3 -0
- package/src/components/sf-live-feed/components/avatar/avatar.scss +24 -0
- package/src/components/sf-live-feed/components/avatar/avatar.ts +27 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.host.ts +8 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.html +10 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.scss +177 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.ts +65 -0
- package/src/components/sf-live-feed/constants.ts +4 -0
- package/src/components/sf-live-feed/demo/sample-data.ts +34 -0
- package/src/components/sf-live-feed/index.ts +19 -0
- package/src/components/sf-live-feed/styles/theme.scss +19 -0
- package/src/components/sf-live-feed/styles/theme.ts +5 -0
- package/src/components/sf-live-feed/types.ts +19 -0
- package/src/components/sf-live-feed/utils.ts +17 -0
- package/src/components/streaks/constants.ts +17 -0
- package/src/components/streaks/demo/sample-steps.ts +10 -0
- package/src/components/streaks/events.ts +8 -0
- package/src/components/streaks/index.ts +16 -0
- package/src/components/streaks/sf-streaks.html +26 -0
- package/src/components/streaks/sf-streaks.scss +351 -0
- package/src/components/streaks/sf-streaks.ts +235 -0
- package/src/components/streaks/types.ts +7 -0
- package/src/lib/lit/component.ts +10 -0
- package/src/lib/lit/safe-custom-element.ts +12 -0
- package/src/lib/lit/scss.ts +6 -0
- package/src/vite-env.d.ts +18 -0
- package/styles/global.css +125 -0
- package/todo.txt +54 -0
- package/tsconfig.json +31 -0
- package/vite.config.ts +56 -0
- package/vite.docs.config.ts +33 -0
- package/vite.lit-html-plugin.ts +43 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LitElement, nothing } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { Component } from '../../../../lib/lit/component.js';
|
|
4
|
+
import { scss } from '../../../../lib/lit/scss.js';
|
|
5
|
+
import { SF_LEADERBOARD_RANKING_SECTION } from '../../shared/constants/tags.js';
|
|
6
|
+
import { sfLeaderboardTheme } from '../../theme/styles.js';
|
|
7
|
+
import type { Player } from '../../shared/types/data.js';
|
|
8
|
+
import type { SfLeaderboardRankingSection as RankingSectionData } from '../../shared/types/sections.js';
|
|
9
|
+
import { formatCurrency } from '../../shared/utils/utils.js';
|
|
10
|
+
import '../../shared/components/podium/podium.js';
|
|
11
|
+
import '../../shared/components/player-stats-card/player-stats-card.js';
|
|
12
|
+
import type { RankingSectionHost } from './ranking-section.host.js';
|
|
13
|
+
import renderTemplate from './ranking-section.html?lit-html';
|
|
14
|
+
import styles from './ranking-section.scss?inline';
|
|
15
|
+
|
|
16
|
+
@Component({ selector: SF_LEADERBOARD_RANKING_SECTION })
|
|
17
|
+
export class SfLeaderboardRankingBlock
|
|
18
|
+
extends LitElement
|
|
19
|
+
implements RankingSectionHost
|
|
20
|
+
{
|
|
21
|
+
static styles = [sfLeaderboardTheme, scss(styles)];
|
|
22
|
+
|
|
23
|
+
@property({ attribute: false })
|
|
24
|
+
ranking!: RankingSectionData;
|
|
25
|
+
|
|
26
|
+
@property({ attribute: false })
|
|
27
|
+
lookupPlayers: Player[] = [];
|
|
28
|
+
|
|
29
|
+
get hasPair(): boolean {
|
|
30
|
+
return this.ranking?.podium != null && this.ranking?.playerStats != null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get hasPodium(): boolean {
|
|
34
|
+
return this.ranking?.podium != null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get hasPlayerStats(): boolean {
|
|
38
|
+
return this.ranking?.playerStats != null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get podiumTitle(): string {
|
|
42
|
+
return this.ranking?.podium?.title ?? 'Top Champions';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get podiumBadge(): string {
|
|
46
|
+
return this.ranking?.podium?.badge ?? '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get showPodiumBadge(): boolean {
|
|
50
|
+
return Boolean(this.ranking?.podium?.badge);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get podiumPlayers(): Player[] {
|
|
54
|
+
return this.ranking?.podium?.players ?? [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get playerStatsTitle(): string {
|
|
58
|
+
return this.ranking?.playerStats?.title ?? 'Your Position';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private get resolvedPlayerStats() {
|
|
62
|
+
const block = this.ranking?.playerStats;
|
|
63
|
+
const players = this.lookupPlayers ?? [];
|
|
64
|
+
const player = block
|
|
65
|
+
? players.find((p) => p.rank === block.rank)
|
|
66
|
+
: undefined;
|
|
67
|
+
const percentileMatch = block?.message?.match(/(\d+)\s*%/);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
block,
|
|
71
|
+
player,
|
|
72
|
+
percentile:
|
|
73
|
+
block?.percentile ?? (percentileMatch ? Number(percentileMatch[1]) : 5),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get playerStatsRank(): number {
|
|
78
|
+
return this.resolvedPlayerStats.block?.rank ?? 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get playerStatsPercentile(): number {
|
|
82
|
+
return this.resolvedPlayerStats.percentile;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get playerStatsUsername(): string {
|
|
86
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
87
|
+
return block?.username ?? player?.username ?? 'You';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get playerStatsEarnings(): string {
|
|
91
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
92
|
+
return (
|
|
93
|
+
block?.earnings ?? (player ? formatCurrency(player.totalWinnings) : '$0')
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get playerStatsWinRate(): number {
|
|
98
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
99
|
+
return block?.winRate ?? player?.winRate ?? 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get playerStatsGamesPlayed(): number {
|
|
103
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
104
|
+
return block?.gamesPlayed ?? player?.gamesPlayed ?? 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get playerStatsWinStreak(): number {
|
|
108
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
109
|
+
return block?.winStreak ?? player?.streak ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get playerStatsLevel(): number {
|
|
113
|
+
const { block, player } = this.resolvedPlayerStats;
|
|
114
|
+
return block?.level ?? player?.level ?? 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render() {
|
|
118
|
+
if (!this.ranking?.podium && !this.ranking?.playerStats) return nothing;
|
|
119
|
+
return renderTemplate(this);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
@use '../../shared/styles/section';
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.stats-section .stats-grid {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
11
|
+
gap: clamp(0.625rem, 2vw, 1rem);
|
|
12
|
+
width: 100%;
|
|
13
|
+
min-width: 0;
|
|
14
|
+
align-items: stretch;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (min-width: 640px) and (max-width: 1079px) {
|
|
18
|
+
.stats-section .stats-grid {
|
|
19
|
+
gap: 0.875rem;
|
|
20
|
+
grid-auto-rows: 1fr;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (min-width: 1080px) {
|
|
25
|
+
.stats-section .stats-grid {
|
|
26
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
27
|
+
grid-auto-rows: auto;
|
|
28
|
+
gap: 1rem;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 639px) {
|
|
33
|
+
.stats-section .stats-grid {
|
|
34
|
+
gap: clamp(0.35rem, 2.5vw, 0.5rem);
|
|
35
|
+
grid-auto-rows: 1fr;
|
|
36
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (max-width: 576px) {
|
|
41
|
+
.stats-section .stats-grid {
|
|
42
|
+
gap: clamp(0.5rem, 2.8vw, 0.75rem);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { LitElement, nothing } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { Component } from '../../../../../lib/lit/component.js';
|
|
4
|
+
import { scss } from '../../../../../lib/lit/scss.js';
|
|
5
|
+
import { SF_LEADERBOARD_STATS_SECTION } from '../../shared/constants/tags.js';
|
|
6
|
+
import { sfLeaderboardTheme } from '../../theme/styles.js';
|
|
7
|
+
import type { SfLeaderboardStatsSection as StatsSectionData } from '../../shared/types/sections.js';
|
|
8
|
+
import { SF_LEADERBOARD_STAT_CARD } from '../../shared/constants/tags.js';
|
|
9
|
+
import '../../shared/components/stat-card/stat-card.js';
|
|
10
|
+
import type { StatsSectionHost } from './stats-section.host.js';
|
|
11
|
+
import renderTemplate from './stats-section.html?lit-html';
|
|
12
|
+
import styles from './stats-section.scss?inline';
|
|
13
|
+
|
|
14
|
+
@Component({ selector: SF_LEADERBOARD_STATS_SECTION })
|
|
15
|
+
export class SfLeaderboardStatsBlock extends LitElement implements StatsSectionHost {
|
|
16
|
+
static styles = [sfLeaderboardTheme, scss(styles)];
|
|
17
|
+
|
|
18
|
+
@property({ attribute: false })
|
|
19
|
+
section!: StatsSectionData;
|
|
20
|
+
|
|
21
|
+
get hasTitle(): boolean {
|
|
22
|
+
return Boolean(this.section?.title);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
renderStatCards() {
|
|
26
|
+
return this.section.items.map((stat) => {
|
|
27
|
+
const card = document.createElement(SF_LEADERBOARD_STAT_CARD);
|
|
28
|
+
card.label = stat.label;
|
|
29
|
+
card.value = stat.value;
|
|
30
|
+
card.icon = stat.icon;
|
|
31
|
+
if (stat.trendValue !== undefined) card.trendValue = stat.trendValue;
|
|
32
|
+
card.trendPositive = stat.trendPositive ?? true;
|
|
33
|
+
return card;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
if (!this.section?.items?.length) return nothing;
|
|
39
|
+
return renderTemplate(this);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Category, TimeFrame } from '../../shared/types/data.js';
|
|
2
|
+
import type { SfLeaderboardTableSection } from '../../shared/types/sections.js';
|
|
3
|
+
|
|
4
|
+
export interface TableSectionHost {
|
|
5
|
+
section: SfLeaderboardTableSection;
|
|
6
|
+
hasHeader: boolean;
|
|
7
|
+
hasTitle: boolean;
|
|
8
|
+
hasSubtitle: boolean;
|
|
9
|
+
hasFilters: boolean;
|
|
10
|
+
timeframe: TimeFrame;
|
|
11
|
+
category: Category;
|
|
12
|
+
searchQuery: string;
|
|
13
|
+
onTimeframeChange(e: CustomEvent<TimeFrame>): void;
|
|
14
|
+
onCategoryChange(e: CustomEvent<Category>): void;
|
|
15
|
+
onSearchChange(e: CustomEvent<string>): void;
|
|
16
|
+
onPlayerSelect(e: CustomEvent<import('../../shared/types/data.js').Player>): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<section class="section table-section">
|
|
2
|
+
<div class="table-section-header" ?hidden="${!host.hasHeader}">
|
|
3
|
+
<h2 class="section-title" ?hidden="${!host.hasTitle}">${host.section.title}</h2>
|
|
4
|
+
<span class="table-subtitle" ?hidden="${!host.hasSubtitle}">${host.section.subtitle}</span>
|
|
5
|
+
</div>
|
|
6
|
+
<sf-leaderboard-filters
|
|
7
|
+
?hidden="${!host.hasFilters}"
|
|
8
|
+
activeTimeframe="${host.timeframe}"
|
|
9
|
+
activeCategory="${host.category}"
|
|
10
|
+
searchQuery="${host.searchQuery}"
|
|
11
|
+
@timeframe-change="${host.onTimeframeChange}"
|
|
12
|
+
@category-change="${host.onCategoryChange}"
|
|
13
|
+
@search-change="${host.onSearchChange}"
|
|
14
|
+
></sf-leaderboard-filters>
|
|
15
|
+
<sf-leaderboard-table
|
|
16
|
+
.players="${host.section.players}"
|
|
17
|
+
@player-select="${host.onPlayerSelect}"
|
|
18
|
+
></sf-leaderboard-table>
|
|
19
|
+
</section>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@use '../../shared/styles/section';
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.table-section {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 1rem;
|
|
12
|
+
min-width: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.table-section-header {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-wrap: wrap;
|
|
18
|
+
align-items: baseline;
|
|
19
|
+
justify-content: space-between;
|
|
20
|
+
gap: 0.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.table-subtitle {
|
|
24
|
+
font-size: 0.875rem;
|
|
25
|
+
color: var(--muted-foreground);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.table-section sf-leaderboard-table {
|
|
29
|
+
width: 100%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 639px) {
|
|
33
|
+
.table-section-header {
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: flex-start;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { LitElement, nothing } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { Component } from '../../../../../lib/lit/component.js';
|
|
4
|
+
import { scss } from '../../../../../lib/lit/scss.js';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_CATEGORY,
|
|
7
|
+
DEFAULT_TIMEFRAME,
|
|
8
|
+
SF_LEADERBOARD_TABLE_SECTION,
|
|
9
|
+
} from '../../shared/constants/index.js';
|
|
10
|
+
import { sfLeaderboardTheme } from '../../theme/styles.js';
|
|
11
|
+
import type { Category, Player, TimeFrame } from '../../shared/types/data.js';
|
|
12
|
+
import {
|
|
13
|
+
SF_LEADERBOARD_CATEGORY_CHANGE,
|
|
14
|
+
SF_LEADERBOARD_PLAYER_SELECT,
|
|
15
|
+
SF_LEADERBOARD_SEARCH_CHANGE,
|
|
16
|
+
SF_LEADERBOARD_TIMEFRAME_CHANGE,
|
|
17
|
+
} from '../../shared/types/events.js';
|
|
18
|
+
import type { SfLeaderboardTableSection as TableSectionData } from '../../shared/types/sections.js';
|
|
19
|
+
import '../../shared/components/filters/filters.js';
|
|
20
|
+
import '../../shared/components/table/table.js';
|
|
21
|
+
import type { TableSectionHost } from './table-section.host.js';
|
|
22
|
+
import renderTemplate from './table-section.html?lit-html';
|
|
23
|
+
import styles from './table-section.scss?inline';
|
|
24
|
+
|
|
25
|
+
@Component({ selector: SF_LEADERBOARD_TABLE_SECTION })
|
|
26
|
+
export class SfLeaderboardTableBlock extends LitElement implements TableSectionHost {
|
|
27
|
+
static styles = [sfLeaderboardTheme, scss(styles)];
|
|
28
|
+
|
|
29
|
+
@property({ attribute: false })
|
|
30
|
+
section!: TableSectionData;
|
|
31
|
+
|
|
32
|
+
get hasHeader(): boolean {
|
|
33
|
+
return Boolean(this.section?.title || this.section?.subtitle);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get hasTitle(): boolean {
|
|
37
|
+
return Boolean(this.section?.title);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get hasSubtitle(): boolean {
|
|
41
|
+
return Boolean(this.section?.subtitle);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get hasFilters(): boolean {
|
|
45
|
+
return Boolean(this.section?.filters);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get timeframe(): TimeFrame {
|
|
49
|
+
return this.section.filters?.timeframe ?? DEFAULT_TIMEFRAME;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get category(): Category {
|
|
53
|
+
return this.section.filters?.category ?? DEFAULT_CATEGORY;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get searchQuery(): string {
|
|
57
|
+
return this.section.filters?.searchQuery ?? '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onTimeframeChange(e: CustomEvent<TimeFrame>) {
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
this.dispatchEvent(
|
|
63
|
+
new CustomEvent(SF_LEADERBOARD_TIMEFRAME_CHANGE, {
|
|
64
|
+
detail: e.detail,
|
|
65
|
+
bubbles: true,
|
|
66
|
+
composed: true,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onCategoryChange(e: CustomEvent<Category>) {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
this.dispatchEvent(
|
|
74
|
+
new CustomEvent(SF_LEADERBOARD_CATEGORY_CHANGE, {
|
|
75
|
+
detail: e.detail,
|
|
76
|
+
bubbles: true,
|
|
77
|
+
composed: true,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onSearchChange(e: CustomEvent<string>) {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
this.dispatchEvent(
|
|
85
|
+
new CustomEvent(SF_LEADERBOARD_SEARCH_CHANGE, {
|
|
86
|
+
detail: e.detail,
|
|
87
|
+
bubbles: true,
|
|
88
|
+
composed: true,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onPlayerSelect(e: CustomEvent<Player>) {
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
this.dispatchEvent(
|
|
96
|
+
new CustomEvent(SF_LEADERBOARD_PLAYER_SELECT, {
|
|
97
|
+
detail: e.detail,
|
|
98
|
+
bubbles: true,
|
|
99
|
+
composed: true,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render() {
|
|
105
|
+
if (!this.section) return nothing;
|
|
106
|
+
return renderTemplate(this);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { sfLeaderboardDataService } from './sf-leaderboard-data.service.js';
|
|
2
|
+
export type {
|
|
3
|
+
SfLeaderboardState,
|
|
4
|
+
SfLeaderboardConfig,
|
|
5
|
+
SfLeaderboardLabels,
|
|
6
|
+
SfLeaderboardSectionData,
|
|
7
|
+
SfLeaderboardSectionId,
|
|
8
|
+
SfLeaderboardSectionEntry,
|
|
9
|
+
SfLeaderboardSectionPayloadMap,
|
|
10
|
+
SfLeaderboardSectionPatch,
|
|
11
|
+
SfLeaderboardData,
|
|
12
|
+
} from './sf-leaderboard.state.js';
|
|
13
|
+
export {
|
|
14
|
+
EMPTY_SF_LEADERBOARD_STATE,
|
|
15
|
+
applySectionPatch,
|
|
16
|
+
getSectionData,
|
|
17
|
+
getSectionId,
|
|
18
|
+
hasSection,
|
|
19
|
+
patchSectionData,
|
|
20
|
+
sectionEntry,
|
|
21
|
+
updateSections,
|
|
22
|
+
} from './sf-leaderboard.state.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
|
+
import {
|
|
3
|
+
applySectionPatch,
|
|
4
|
+
EMPTY_SF_LEADERBOARD_STATE,
|
|
5
|
+
type SfLeaderboardSectionPatch,
|
|
6
|
+
type SfLeaderboardState,
|
|
7
|
+
updateSections,
|
|
8
|
+
} from './sf-leaderboard.state.js';
|
|
9
|
+
|
|
10
|
+
/** Holds the leaderboard model as one observable object. */
|
|
11
|
+
class SfLeaderboardDataService {
|
|
12
|
+
private readonly stateSubject = new BehaviorSubject<SfLeaderboardState>(
|
|
13
|
+
EMPTY_SF_LEADERBOARD_STATE,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
readonly state$: Observable<SfLeaderboardState> =
|
|
17
|
+
this.stateSubject.asObservable();
|
|
18
|
+
|
|
19
|
+
/** @deprecated Prefer `state$` */
|
|
20
|
+
readonly data$ = this.state$;
|
|
21
|
+
|
|
22
|
+
get snapshot(): SfLeaderboardState {
|
|
23
|
+
return this.stateSubject.getValue();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setState(state: SfLeaderboardState): void {
|
|
27
|
+
this.stateSubject.next(state);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Flat partial patch — `{ table: { filters: { searchQuery: 'x' } } }` */
|
|
31
|
+
patchState(patch: SfLeaderboardSectionPatch): void {
|
|
32
|
+
this.setState(applySectionPatch(this.snapshot, patch));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
updateState(
|
|
36
|
+
updater: (current: SfLeaderboardState) => SfLeaderboardState,
|
|
37
|
+
): void {
|
|
38
|
+
this.setState(updater(this.snapshot));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Immer recipe for config/sections when patch objects are not enough. */
|
|
42
|
+
updateSections(
|
|
43
|
+
recipe: Parameters<typeof updateSections>[1],
|
|
44
|
+
): void {
|
|
45
|
+
this.setState(updateSections(this.snapshot, recipe));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @deprecated Use `setState` */
|
|
49
|
+
setData(state: SfLeaderboardState): void {
|
|
50
|
+
this.setState(state);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const sfLeaderboardDataService = new SfLeaderboardDataService();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { produce, type Draft } from 'immer';
|
|
2
|
+
import type { SfLeaderboardTheme } from '../theme/index.js';
|
|
3
|
+
import type {
|
|
4
|
+
SfLeaderboardFooterSection,
|
|
5
|
+
SfLeaderboardHeaderSection,
|
|
6
|
+
SfLeaderboardRankingSection,
|
|
7
|
+
SfLeaderboardStatsSection,
|
|
8
|
+
SfLeaderboardTableSection,
|
|
9
|
+
} from '../shared/types/sections.js';
|
|
10
|
+
|
|
11
|
+
export type SfLeaderboardSectionId =
|
|
12
|
+
| 'header'
|
|
13
|
+
| 'stats'
|
|
14
|
+
| 'ranking'
|
|
15
|
+
| 'table'
|
|
16
|
+
| 'footer';
|
|
17
|
+
|
|
18
|
+
export type SfLeaderboardSectionPayloadMap = {
|
|
19
|
+
header: SfLeaderboardHeaderSection;
|
|
20
|
+
stats: SfLeaderboardStatsSection;
|
|
21
|
+
ranking: SfLeaderboardRankingSection;
|
|
22
|
+
table: SfLeaderboardTableSection;
|
|
23
|
+
footer: SfLeaderboardFooterSection;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** One layout slot: section id + its render payload. Order in `config.sections` = render order. */
|
|
27
|
+
export type SfLeaderboardSectionEntry = {
|
|
28
|
+
[K in SfLeaderboardSectionId]: {
|
|
29
|
+
[P in K]: { data: SfLeaderboardSectionPayloadMap[K] };
|
|
30
|
+
};
|
|
31
|
+
}[SfLeaderboardSectionId];
|
|
32
|
+
|
|
33
|
+
/** Shared copy not owned by a single section payload. */
|
|
34
|
+
export interface SfLeaderboardLabels {
|
|
35
|
+
back?: string;
|
|
36
|
+
live?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Layout, theme, labels, and all section payloads in one place. */
|
|
40
|
+
export interface SfLeaderboardConfig {
|
|
41
|
+
sections: readonly SfLeaderboardSectionEntry[];
|
|
42
|
+
labels?: SfLeaderboardLabels;
|
|
43
|
+
theme?: SfLeaderboardTheme;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SfLeaderboardState {
|
|
47
|
+
config: SfLeaderboardConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const EMPTY_SF_LEADERBOARD_STATE: SfLeaderboardState = {
|
|
51
|
+
config: { sections: [] },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** @deprecated Use `SfLeaderboardState` */
|
|
55
|
+
export type SfLeaderboardData = SfLeaderboardState;
|
|
56
|
+
|
|
57
|
+
/** Flat partial map — patch by section id, same ergonomics as the old `data` object. */
|
|
58
|
+
export type SfLeaderboardSectionPatch = {
|
|
59
|
+
[K in SfLeaderboardSectionId]?: Partial<SfLeaderboardSectionPayloadMap[K]>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @deprecated Use `SfLeaderboardSectionPatch` */
|
|
63
|
+
export type SfLeaderboardSectionData = {
|
|
64
|
+
[K in SfLeaderboardSectionId]?: SfLeaderboardSectionPayloadMap[K];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function getSectionId(
|
|
68
|
+
entry: SfLeaderboardSectionEntry,
|
|
69
|
+
): SfLeaderboardSectionId {
|
|
70
|
+
if ('header' in entry) return 'header';
|
|
71
|
+
if ('stats' in entry) return 'stats';
|
|
72
|
+
if ('ranking' in entry) return 'ranking';
|
|
73
|
+
if ('table' in entry) return 'table';
|
|
74
|
+
return 'footer';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hasSection(
|
|
78
|
+
state: SfLeaderboardState,
|
|
79
|
+
id: SfLeaderboardSectionId,
|
|
80
|
+
): boolean {
|
|
81
|
+
return state.config.sections.some((entry) => id in entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getSectionData<T extends SfLeaderboardSectionId>(
|
|
85
|
+
state: SfLeaderboardState,
|
|
86
|
+
id: T,
|
|
87
|
+
): SfLeaderboardSectionPayloadMap[T] | undefined {
|
|
88
|
+
const entry = state.config.sections.find((section) => id in section);
|
|
89
|
+
if (!entry) return undefined;
|
|
90
|
+
return (entry as Record<T, { data: SfLeaderboardSectionPayloadMap[T] }>)[id]
|
|
91
|
+
.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function sectionEntry<T extends SfLeaderboardSectionId>(
|
|
95
|
+
id: T,
|
|
96
|
+
data: SfLeaderboardSectionPayloadMap[T],
|
|
97
|
+
): SfLeaderboardSectionEntry {
|
|
98
|
+
return { [id]: { data } } as unknown as SfLeaderboardSectionEntry;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
102
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Deep-merge a partial patch into a draft target (immer-safe — mutates draft). */
|
|
106
|
+
function mergePartial(target: Record<string, unknown>, patch: object): void {
|
|
107
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
108
|
+
if (value === undefined) continue;
|
|
109
|
+
|
|
110
|
+
const current = target[key];
|
|
111
|
+
if (isPlainObject(value) && isPlainObject(current)) {
|
|
112
|
+
mergePartial(current, value);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
target[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Apply a flat section patch onto live state (immer).
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* applySectionPatch(state, { table: { filters: { searchQuery: 'neo' } } })
|
|
125
|
+
* applySectionPatch(state, { header: { isRefreshing: true } })
|
|
126
|
+
*/
|
|
127
|
+
export function applySectionPatch(
|
|
128
|
+
state: SfLeaderboardState,
|
|
129
|
+
patch: SfLeaderboardSectionPatch,
|
|
130
|
+
): SfLeaderboardState {
|
|
131
|
+
return produce(state, (draft) => {
|
|
132
|
+
for (const entry of draft.config.sections) {
|
|
133
|
+
const id = getSectionId(entry as SfLeaderboardSectionEntry);
|
|
134
|
+
const sectionPatch = patch[id];
|
|
135
|
+
if (!sectionPatch) continue;
|
|
136
|
+
|
|
137
|
+
const slot = entry as Record<
|
|
138
|
+
SfLeaderboardSectionId,
|
|
139
|
+
{ data: SfLeaderboardSectionPayloadMap[SfLeaderboardSectionId] }
|
|
140
|
+
>;
|
|
141
|
+
mergePartial(
|
|
142
|
+
slot[id].data as unknown as Record<string, unknown>,
|
|
143
|
+
sectionPatch as object,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Immer recipe when a patch object is not enough. */
|
|
150
|
+
export function updateSections(
|
|
151
|
+
state: SfLeaderboardState,
|
|
152
|
+
recipe: (draft: Draft<SfLeaderboardConfig>) => void,
|
|
153
|
+
): SfLeaderboardState {
|
|
154
|
+
return produce(state, (draft) => {
|
|
155
|
+
recipe(draft.config);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @deprecated Use `applySectionPatch` */
|
|
160
|
+
export const patchSectionData = applySectionPatch;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="feed-container">
|
|
2
|
+
<div class="feed-header">
|
|
3
|
+
<div class="header-left">
|
|
4
|
+
<div class="live-dot"></div>
|
|
5
|
+
<span class="header-title">Live Activity</span>
|
|
6
|
+
</div>
|
|
7
|
+
<span class="event-count">${host.activities.length} events</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="feed-body">${host.renderActivities()}</div>
|
|
10
|
+
</div>
|