Skip to content

Commit d36d68d

Browse files
authored
Add Solo Mode, icon system polish, and leaderboard fixes (#18)
* polishing: removing all emojis, patching some leaderboard bugs * adding solo player mode * Prevent duplicate solo submissions on timer expiry * Hide timed-out solo distance in summary
1 parent 3694925 commit d36d68d

File tree

32 files changed

+1332
-122
lines changed

32 files changed

+1332
-122
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ picturedata.txt
88
node_modules/
99
dist/
1010
.codex/
11+
*package-lock.json

apps/frontend/src/lib/api/solo.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { api } from './client.js';
2+
3+
export interface SoloRound {
4+
id: number;
5+
imageUrl: string;
6+
}
7+
8+
export interface SoloGuessResult {
9+
distance: number;
10+
points: number;
11+
actualLat: number;
12+
actualLng: number;
13+
}
14+
15+
export async function getSoloRounds(count: number = 5): Promise<SoloRound[]> {
16+
const { data, error } = await api.get<SoloRound[]>(`/api/solo/rounds?count=${count}`);
17+
if (error) {
18+
console.error('Failed to get solo rounds:', error);
19+
return [];
20+
}
21+
return data ?? [];
22+
}
23+
24+
export async function submitSoloGuess(
25+
pictureId: number,
26+
guessLat: number,
27+
guessLng: number
28+
): Promise<SoloGuessResult | null> {
29+
const { data, error } = await api.post<SoloGuessResult>('/api/solo/submit', {
30+
pictureId,
31+
guessLat,
32+
guessLng
33+
});
34+
if (error) {
35+
console.error('Failed to submit solo guess:', error);
36+
return null;
37+
}
38+
return data ?? null;
39+
}

apps/frontend/src/lib/components/ActiveMatchBanner.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import Card from './Card.svelte';
33
import Button from './Button.svelte';
4+
import Icon from './Icon.svelte';
45
56
let {
67
opponent,
@@ -18,7 +19,7 @@
1819
<Card variant="orange" class="py-6 {className}">
1920
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
2021
<div class="flex items-center gap-4">
21-
<span class="text-4xl">⚔️</span>
22+
<Icon name="swords" class="text-4xl" />
2223
<div>
2324
<div class="font-black text-lg">Your Match is Ready!</div>
2425
<div class="text-sm opacity-80">

apps/frontend/src/lib/components/ChallengeCard.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import Button from './Button.svelte';
33
import StatusBadge from './StatusBadge.svelte';
44
import { formatTimeAgo } from '$lib/utils/time';
5+
import Icon from './Icon.svelte';
56
67
type ChallengeStatus = 'pending' | 'sent' | 'active';
78
@@ -59,7 +60,7 @@
5960
{:else if status === 'active' && onStart}
6061
<div class="flex items-center justify-between gap-4 mt-0 flex-wrap">
6162
<div class="flex items-center gap-3">
62-
<span class="text-2xl">⚔️</span>
63+
<Icon name="swords" class="text-2xl" />
6364
<div class="text-xs opacity-60">5 rounds • 30s each</div>
6465
</div>
6566
<div class="flex items-center gap-2">

apps/frontend/src/lib/components/GameModeCard.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script lang="ts">
22
import Card from './Card.svelte';
3+
import Icon from './Icon.svelte';
4+
import type { IconName } from './icons';
35
46
type CardVariant = 'cyan' | 'magenta' | 'lime' | 'orange';
57
68
let {
79
href,
810
variant,
11+
icon,
912
emoji,
1013
title,
1114
description,
@@ -16,7 +19,8 @@
1619
}: {
1720
href: string;
1821
variant: CardVariant;
19-
emoji: string;
22+
icon?: IconName;
23+
emoji?: string;
2024
title: string;
2125
description: string;
2226
stat: string;
@@ -36,7 +40,11 @@
3640
<span>Done</span>
3741
</div>
3842
{/if}
39-
<div class="text-5xl mb-6">{emoji}</div>
43+
{#if icon}
44+
<Icon name={icon} class="text-5xl mb-6" />
45+
{:else if emoji}
46+
<div class="text-5xl mb-6">{emoji}</div>
47+
{/if}
4048
<h3 class="text-2xl font-black mb-4">{title}</h3>
4149
<p class="opacity-80 mb-6 grow leading-relaxed">
4250
{description}
Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
1+
<script lang="ts">
2+
import Icon from '$lib/components/Icon.svelte';
3+
</script>
4+
15
<header class="header-fixed bg-white brutal-border">
26
<div class="w-full h-full px-6 flex items-center justify-between">
37
<a href="/menu">
48
<img src="/logo.png" alt="TigerSpot Logo" class="inline-block w-40" />
59
</a>
610
<nav class="flex items-center gap-6">
7-
<a href="/menu" class="font-bold text-sm uppercase hover:text-orange transition-colors"
8-
>🏠 Menu</a
11+
<a
12+
href="/menu"
13+
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
914
>
10-
<a href="/leaderboard" class="font-bold text-sm uppercase hover:text-orange transition-colors"
11-
>🏆 Leaderboard</a
15+
<Icon name="home" class="text-base" />
16+
Menu
17+
</a>
18+
<a
19+
href="/leaderboard"
20+
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
1221
>
13-
<a href="/settings" class="font-bold text-sm uppercase hover:text-orange transition-colors"
14-
>⚙️ Settings</a
22+
<Icon name="trophy" class="text-base" />
23+
Leaderboard
24+
</a>
25+
<a
26+
href="/settings"
27+
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
1528
>
16-
<a href="/about" class="font-bold text-sm uppercase hover:text-orange transition-colors"
17-
>👥 About</a
29+
<Icon name="settings" class="text-base" />
30+
Settings
31+
</a>
32+
<a
33+
href="/about"
34+
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
1835
>
36+
<Icon name="users" class="text-base" />
37+
About
38+
</a>
1939
</nav>
2040
</div>
2141
</header>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script lang="ts">
2+
import { icons, type IconName } from './icons';
3+
4+
let {
5+
name,
6+
title,
7+
strokeWidth = 2.5,
8+
class: className = '',
9+
style: styleProp = ''
10+
}: {
11+
name: IconName;
12+
title?: string;
13+
strokeWidth?: number;
14+
class?: string;
15+
style?: string;
16+
} = $props();
17+
18+
const icon = $derived(icons[name]);
19+
const svgClass = $derived(`inline-block align-middle ${className}`);
20+
const resolvedStrokeWidth = $derived(icon?.strokeWidth ?? strokeWidth);
21+
</script>
22+
23+
{#if icon}
24+
<svg
25+
xmlns="http://www.w3.org/2000/svg"
26+
class={svgClass}
27+
viewBox={icon.viewBox ?? '0 0 24 24'}
28+
fill={icon.fill ?? 'none'}
29+
stroke={icon.stroke ?? 'currentColor'}
30+
stroke-width={resolvedStrokeWidth}
31+
stroke-linecap="round"
32+
stroke-linejoin="round"
33+
width="1em"
34+
height="1em"
35+
style={styleProp}
36+
role="img"
37+
aria-hidden={!title}
38+
focusable="false"
39+
>
40+
{#if title}
41+
<title>{title}</title>
42+
{/if}
43+
{#each icon.paths ?? [] as path}
44+
<path d={path} />
45+
{/each}
46+
{#each icon.circles ?? [] as circle}
47+
<circle cx={circle.cx} cy={circle.cy} r={circle.r} />
48+
{/each}
49+
{#each icon.rects ?? [] as rect}
50+
<rect
51+
x={rect.x}
52+
y={rect.y}
53+
width={rect.width}
54+
height={rect.height}
55+
rx={rect.rx}
56+
ry={rect.ry}
57+
/>
58+
{/each}
59+
{#each icon.lines ?? [] as line}
60+
<line x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2} />
61+
{/each}
62+
{#each icon.polylines ?? [] as polyline}
63+
<polyline points={polyline} />
64+
{/each}
65+
{#each icon.polygons ?? [] as polygon}
66+
<polygon points={polygon} />
67+
{/each}
68+
</svg>
69+
{/if}

apps/frontend/src/lib/components/ImageUpload.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { processImagePreview } from '$lib/api/admin';
3+
import Icon from '$lib/components/Icon.svelte';
34
45
interface Props {
56
onSelect?: (
@@ -247,7 +248,7 @@
247248
{:else if processing}
248249
<!-- Processing image -->
249250
<div class="brutal-border border-dashed border-4 p-8 text-center bg-cyan/10 border-cyan">
250-
<div class="text-5xl mb-4 animate-pulse">🔄</div>
251+
<Icon name="refresh" class="text-5xl mb-4 animate-pulse" />
251252
<p class="font-bold text-lg mb-2">Processing image...</p>
252253
<p class="text-black/60 text-sm">Extracting GPS data and generating preview</p>
253254
</div>
@@ -265,7 +266,7 @@
265266
onclick={openFilePicker}
266267
onkeydown={(e) => e.key === 'Enter' && openFilePicker()}
267268
>
268-
<div class="text-5xl mb-4">📸</div>
269+
<Icon name="camera" class="text-5xl mb-4" />
269270
<p class="font-bold text-lg mb-2">Drop an image here</p>
270271
<p class="text-black/60 text-sm mb-4">or click to browse (HEIC supported)</p>
271272
<span class="brutal-border brutal-shadow-sm bg-cyan text-white px-4 py-2 font-bold text-sm">

0 commit comments

Comments
 (0)