Skip to content

Commit 915b960

Browse files
committed
Add compact Freebuff live map
1 parent 1b0b1fd commit 915b960

5 files changed

Lines changed: 111 additions & 20 deletions

File tree

freebuff/web/src/app/api/live/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export async function GET() {
99
const stats = await getFreebuffLiveStats()
1010
return NextResponse.json(stats, {
1111
headers: {
12-
'Cache-Control': 'no-store, max-age=0',
12+
'Cache-Control':
13+
'public, max-age=0, s-maxage=60, stale-while-revalidate=30',
1314
},
1415
})
1516
}

freebuff/web/src/app/home-client.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CopyButton } from '@/components/copy-button'
1313
import { HeroGrid } from '@/components/hero-grid'
1414
import { Icons } from '@/components/icons'
1515
import { cn } from '@/lib/utils'
16+
import { CompactLiveStats } from './live/live-client'
1617

1718
const INSTALL_COMMAND = 'npm install -g freebuff'
1819

@@ -567,6 +568,8 @@ export default function HomeClient() {
567568
</div>
568569
</div>
569570
</div>
571+
572+
<CompactLiveStats />
570573
</div>
571574
)
572575
}

freebuff/web/src/app/live/live-client.tsx

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ import Link from 'next/link'
77
import { useEffect, useState } from 'react'
88

99
import { CopyButton } from '@/components/copy-button'
10+
import { cn } from '@/lib/utils'
1011

1112
import { COUNTRY_POINTS, WORLD_LAND_PATHS } from './world-map-data'
1213

1314
import type { FreebuffLiveStats } from '@/server/live-stats'
1415
import type { LucideIcon } from 'lucide-react'
1516

1617
const INSTALL_COMMAND = 'npm install -g freebuff'
17-
const POLL_MS = 15_000
18+
const POLL_MS = 60_000
1819
const MAP_SIZE = { width: 1000, height: 520 }
1920
const REGION_NAMES = new Intl.DisplayNames(['en'], { type: 'region' })
21+
const EMPTY_LIVE_STATS: FreebuffLiveStats = {
22+
totalLiveUsers: 0,
23+
countries: [],
24+
models: [],
25+
generatedAt: '1970-01-01T00:00:00.000Z',
26+
}
2027
type CountryPoint = readonly [lat: number, lon: number]
2128
type PlottedCountry = FreebuffLiveStats['countries'][number] & {
2229
point: CountryPoint
@@ -106,7 +113,10 @@ function isPlottedCountry(
106113
return country !== null
107114
}
108115

109-
function useLiveStats(initialStats: FreebuffLiveStats) {
116+
function useLiveStats(
117+
initialStats: FreebuffLiveStats,
118+
options: { refreshOnMount?: boolean } = {},
119+
) {
110120
const [stats, setStats] = useState(initialStats)
111121

112122
useEffect(() => {
@@ -123,12 +133,16 @@ function useLiveStats(initialStats: FreebuffLiveStats) {
123133
}
124134
}
125135

136+
if (options.refreshOnMount) {
137+
void refresh()
138+
}
139+
126140
const interval = window.setInterval(refresh, POLL_MS)
127141
return () => {
128142
isMounted = false
129143
window.clearInterval(interval)
130144
}
131-
}, [])
145+
}, [options.refreshOnMount])
132146

133147
return stats
134148
}
@@ -186,7 +200,13 @@ function EmptyState({ children }: { children: React.ReactNode }) {
186200
)
187201
}
188202

189-
function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
203+
function WorldMap({
204+
stats,
205+
compact = false,
206+
}: {
207+
stats: FreebuffLiveStats
208+
compact?: boolean
209+
}) {
190210
const maxCount = Math.max(1, ...stats.countries.map((row) => row.count))
191211
const plottedCountries = stats.countries
192212
.map((country) => {
@@ -199,20 +219,25 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
199219
return (
200220
<section className="relative self-start overflow-hidden rounded-lg border border-white/10 bg-[#020807] shadow-[0_24px_90px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]">
201221
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_28%,rgba(34,211,238,0.14),transparent_38%),linear-gradient(180deg,rgba(124,255,63,0.04),rgba(0,0,0,0.2))]" />
202-
<div className="pointer-events-none absolute left-4 top-4 z-10 rounded-md border border-white/10 bg-black/45 px-3 py-2 backdrop-blur md:left-5 md:top-5">
203-
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/45">
204-
Active countries
205-
</div>
206-
<div className="mt-1 text-2xl font-serif leading-none text-white">
207-
{stats.countries.length.toLocaleString()}
222+
{!compact && (
223+
<div className="pointer-events-none absolute left-4 top-4 z-10 rounded-md border border-white/10 bg-black/45 px-3 py-2 backdrop-blur md:left-5 md:top-5">
224+
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/45">
225+
Active countries
226+
</div>
227+
<div className="mt-1 text-2xl font-serif leading-none text-white">
228+
{stats.countries.length.toLocaleString()}
229+
</div>
208230
</div>
209-
</div>
231+
)}
210232

211233
<svg
212234
viewBox={`0 0 ${MAP_SIZE.width} ${MAP_SIZE.height}`}
213235
role="img"
214236
aria-label="World map of live Freebuff users by country"
215-
className="relative h-[300px] w-full md:h-[520px]"
237+
className={cn(
238+
'relative w-full',
239+
compact ? 'h-[230px] md:h-[380px]' : 'h-[300px] md:h-[520px]',
240+
)}
216241
>
217242
<defs>
218243
<pattern
@@ -363,7 +388,7 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
363388
</div>
364389
</div>
365390
)}
366-
{unplottedCount > 0 && (
391+
{!compact && unplottedCount > 0 && (
367392
<div className="absolute bottom-4 right-4 rounded-md border border-white/10 bg-black/45 px-3 py-2 text-xs text-white/48 backdrop-blur">
368393
{unplottedCount} region{unplottedCount === 1 ? '' : 's'} listed
369394
off-map
@@ -373,6 +398,45 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
373398
)
374399
}
375400

401+
export function CompactLiveStats({
402+
initialStats = EMPTY_LIVE_STATS,
403+
}: {
404+
initialStats?: FreebuffLiveStats
405+
}) {
406+
const stats = useLiveStats(initialStats, { refreshOnMount: true })
407+
408+
return (
409+
<section className="relative overflow-hidden bg-black py-14 md:py-20">
410+
<div className="absolute inset-0 bg-[linear-gradient(rgba(124,255,63,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(34,211,238,0.035)_1px,transparent_1px)] bg-[size:56px_56px]" />
411+
<div className="relative container mx-auto px-4">
412+
<div className="mb-6 flex flex-col gap-3 md:mb-8 md:flex-row md:items-end md:justify-between">
413+
<div>
414+
<div className="flex items-center gap-3">
415+
<motion.span
416+
className="h-2.5 w-2.5 rounded-full bg-acid-matrix shadow-[0_0_20px_rgba(124,255,63,0.95)]"
417+
animate={{ opacity: [0.45, 1, 0.45], scale: [0.8, 1.2, 0.8] }}
418+
transition={{
419+
duration: 1.9,
420+
repeat: Infinity,
421+
ease: 'easeInOut',
422+
}}
423+
/>
424+
<span className="font-mono text-xs uppercase tracking-[0.22em] text-white/48">
425+
Active users
426+
</span>
427+
</div>
428+
<div className="mt-2 font-mono text-5xl font-medium leading-none text-acid-matrix neon-text md:text-7xl">
429+
{stats.totalLiveUsers.toLocaleString()}
430+
</div>
431+
</div>
432+
</div>
433+
434+
<WorldMap stats={stats} compact />
435+
</div>
436+
</section>
437+
)
438+
}
439+
376440
function ModelBars({ stats }: { stats: FreebuffLiveStats }) {
377441
const maxCount = Math.max(1, ...stats.models.map((model) => model.count))
378442

freebuff/web/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { siteConfig } from '@/lib/constant'
88

99
export async function generateMetadata(): Promise<Metadata> {
1010
const canonicalUrl = env.NEXT_PUBLIC_CODEBUFF_APP_URL
11-
const title = "Freebuff — the free coding agent"
11+
const title = 'Freebuff — the free coding agent'
1212
const description = siteConfig.description
1313

1414
return {

freebuff/web/src/server/live-stats.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface FreebuffLiveStats {
2121
generatedAt: string
2222
}
2323

24+
const LIVE_STATS_CACHE_MS = 60_000
25+
let cachedLiveStats: {
26+
expiresAt: number
27+
stats: FreebuffLiveStats
28+
} | null = null
29+
2430
const MODEL_LABELS = Object.fromEntries(
2531
SUPPORTED_FREEBUFF_MODELS.map(
2632
(model) => [model.id, model.displayName] as const,
@@ -48,24 +54,32 @@ function sortCounts<T extends { count: number }>(rows: T[]): T[] {
4854
}
4955

5056
export async function getFreebuffLiveStats(
51-
now = new Date(),
57+
now?: Date,
58+
options: { cache?: boolean } = {},
5259
): Promise<FreebuffLiveStats> {
60+
const useCache = options.cache ?? now === undefined
61+
const requestTime = now ?? new Date()
62+
63+
if (useCache && cachedLiveStats && cachedLiveStats.expiresAt > Date.now()) {
64+
return cachedLiveStats.stats
65+
}
66+
5367
const [countryRows, modelRows] = await Promise.all([
5468
db
5569
.select({
5670
countryCode: schema.freeSession.country_code,
5771
count: count(),
5872
})
5973
.from(schema.freeSession)
60-
.where(liveSessionWhere(now))
74+
.where(liveSessionWhere(requestTime))
6175
.groupBy(schema.freeSession.country_code),
6276
db
6377
.select({
6478
modelId: schema.freeSession.model,
6579
count: count(),
6680
})
6781
.from(schema.freeSession)
68-
.where(liveSessionWhere(now))
82+
.where(liveSessionWhere(requestTime))
6983
.groupBy(schema.freeSession.model),
7084
])
7185

@@ -84,10 +98,19 @@ export async function getFreebuffLiveStats(
8498
})),
8599
)
86100

87-
return {
101+
const stats = {
88102
totalLiveUsers: models.reduce((sum, row) => sum + row.count, 0),
89103
countries,
90104
models,
91-
generatedAt: now.toISOString(),
105+
generatedAt: requestTime.toISOString(),
92106
}
107+
108+
if (useCache) {
109+
cachedLiveStats = {
110+
expiresAt: Date.now() + LIVE_STATS_CACHE_MS,
111+
stats,
112+
}
113+
}
114+
115+
return stats
93116
}

0 commit comments

Comments
 (0)