Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
87f1880
chore(assets): move world map svg to public/maps
dergigi Dec 25, 2025
5e3ae2c
feat(map): add /map page rendering inline world svg
dergigi Dec 25, 2025
136c94f
feat(map): highlight grantee countries in OpenSats orange
dergigi Dec 25, 2025
c5088f1
chore(map): polish a11y and responsiveness
dergigi Dec 25, 2025
866bf0c
fix(map): update description to mention 40+ countries
dergigi Dec 25, 2025
0c14bcc
fix(map): remove heading
dergigi Dec 25, 2025
d966a8d
fix(map): use present tense for ongoing support
dergigi Dec 25, 2025
52ca48b
fix(map): remove horizontal divider line
dergigi Dec 25, 2025
44f247d
feat(map): add lifetime stats and countries count
dergigi Dec 25, 2025
b058d61
feat(map): show stats inline in descriptive sentence
dergigi Dec 25, 2025
85169bb
fix(map): show full USD amount with commas instead of abbreviated
dergigi Dec 25, 2025
6b81029
fix(map): spell out billion for sats amount
dergigi Dec 25, 2025
96b46b6
fix(map): revert wording to sent instead of mass-sent
dergigi Dec 25, 2025
f3e945e
fix(map): increase text size for stats sentence
dergigi Dec 25, 2025
2644233
fix(map): make 40+ countries bold
dergigi Dec 25, 2025
6124731
fix(map): prevent line break in sats amount
dergigi Dec 25, 2025
59f5feb
fix(map): round USD amount to whole number
dergigi Dec 25, 2025
4b1f612
fix(map): add 'to free and open-source projects' after USD
dergigi Dec 25, 2025
318b9bb
fix(map): prevent line break in 40+ countries
dergigi Dec 25, 2025
a0b69cb
feat(map): make stats numbers clickable links to /transparency
dergigi Dec 25, 2025
ed996d5
fix(map): use subtle dotted underline for stats links
dergigi Dec 25, 2025
5e9f7e6
fix(map): use dashed underline for stats links
dergigi Dec 25, 2025
4be9d55
fix(map): use thin bottom border for stats links
dergigi Dec 25, 2025
7d33a89
fix(map): use subtle background color for stats links
dergigi Dec 25, 2025
d8b68d9
fix(map): add orange background with white text to 40+ countries
dergigi Dec 25, 2025
69cf4e6
fix(map): include period inside highlight box
dergigi Dec 25, 2025
68a48e5
feat(map): add native browser tooltips showing country names on hover
dergigi Dec 25, 2025
ae59db3
style(map): apply linter formatting
dergigi Dec 25, 2025
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
188 changes: 188 additions & 0 deletions pages/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import fs from 'fs'
import path from 'path'
import { useState, useEffect } from 'react'
import { InferGetStaticPropsType } from 'next'
import { PageSEO } from '@/components/SEO'
import Link from '@/components/Link'
import PublicGoogleSheetsParser from 'public-google-sheets-parser'
import { formatNumber } from '@/components/LifetimeStats'

const OPENSATS_ORANGE = '#f97316' // tailwind orange-500

// ISO 3166-1 alpha-2 codes that match the SVG's path ids (id="US", id="DE", ...)
const GRANTEE_COUNTRY_CODES: string[] = [
'US', // USA
'CA', // Canada
'DE', // Germany
'GB', // United Kingdom
'IT', // Italy
'JP', // Japan
'NL', // Netherlands
'CH', // Switzerland
'CN', // China
'BR', // Brazil
'AR', // Argentina
'IE', // Ireland
'HK', // Hong Kong
'GE', // Georgia
'SE', // Sweden
'ES', // Spain
'PT', // Portugal
'NO', // Norway
'GR', // Greece
'AU', // Australia
'IN', // India
'SI', // Slovenia
'KR', // Republic of Korea
'FI', // Finland
'CZ', // Czech Republic
'UG', // Uganda
'BE', // Belgium
'FR', // France
'VN', // Vietnam
'UA', // Ukraine
'TR', // Turkey
'SV', // El Salvador
'NZ', // New Zealand
'HU', // Hungary
'SK', // Slovakia
'NG', // Nigeria
'PA', // Panama
'RO', // Romania
'GT', // Guatemala
'ID', // Indonesia
'AE', // UAE
]

export const getStaticProps = async () => {
const svgPath = path.join(process.cwd(), 'public', 'maps', 'world.svg')
let svg = fs.readFileSync(svgPath, 'utf8')

// Strip XML declaration - it doesn't belong in HTML
svg = svg.replace(/<\?xml[\s\S]*?\?>\s*/i, '').trim()

// Convert title attributes to <title> child elements for native browser tooltips
svg = svg.replace(
/<path([^>]*)\s+title="([^"]*)"([^>]*)\s*\/>/g,
'<path$1$3><title>$2</title></path>'
)

return { props: { svg } }
}

export default function MapPage({
svg,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const [stats, setStats] = useState<{ label: string; value: number }[]>([])

useEffect(() => {
const parser = new PublicGoogleSheetsParser(
'1mLEbHcrJibLN2PKxYq1LHJssq0CGuJRRoaZwot-ncZQ'
)
parser.parse().then((data) => {
setStats(data)
})
}, [])

// Generate CSS selector for highlighted countries (DRY: one selector string)
const highlightSelector = GRANTEE_COUNTRY_CODES.length
? GRANTEE_COUNTRY_CODES.map((c) => `.grant-map svg #${c}`).join(', ')
: ''

const highlightCss = highlightSelector
? `
${highlightSelector} {
fill: ${OPENSATS_ORANGE} !important;
}
`
: ''

// stats[0] = grants given, stats[1] = USD allocated, stats[2] = sats sent
const grantsGiven = stats[0]?.value ? formatNumber(stats[0].value) : '...'
const usdAllocated = stats[1]?.value
? Math.round(stats[1].value).toLocaleString()
: '...'
const satsSent = stats[2]?.value
? formatNumber(stats[2].value).replace('B', 'billion')
: '...'

return (
<>
<PageSEO
title="Map - OpenSats"
description="Countries where OpenSats has awarded grants."
/>

<div>
<div className="pb-6 pt-6">
<p className="text-2xl leading-9 text-gray-500 dark:text-gray-400 sm:text-3xl md:text-4xl md:leading-relaxed">
OpenSats has allocated{' '}
<Link
href="/transparency"
className="-mx-1 rounded bg-primary-100/50 px-1 hover:bg-primary-100 dark:bg-primary-900/20 dark:hover:bg-primary-900/40"
>
${usdAllocated} USD
</Link>{' '}
to free and open-source projects and sent{' '}
<Link
href="/transparency"
className="-mx-1 whitespace-nowrap rounded bg-primary-100/50 px-1 hover:bg-primary-100 dark:bg-primary-900/20 dark:hover:bg-primary-900/40"
>
~{satsSent} sats
</Link>{' '}
to{' '}
<Link
href="/transparency"
className="-mx-1 rounded bg-primary-100/50 px-1 hover:bg-primary-100 dark:bg-primary-900/20 dark:hover:bg-primary-900/40"
>
{grantsGiven} grantees
</Link>{' '}
in{' '}
<strong className="-mx-1 whitespace-nowrap rounded bg-primary-500 px-2 text-white">
40+ countries.
</strong>
</p>
</div>

<div className="overflow-x-auto pt-6">
<div
className="grant-map"
dangerouslySetInnerHTML={{ __html: svg }}
/>
</div>
</div>

<style jsx global>{`
.grant-map {
max-width: 100%;
overflow: hidden;
}

.grant-map svg {
width: 100%;
height: auto;
display: block;
max-width: 100%;
}

.grant-map svg path {
fill: rgb(229 231 235); /* gray-200 */
stroke: rgb(255 255 255);
stroke-width: 0.5;
transition: fill 120ms ease;
}

.dark .grant-map svg path {
fill: rgb(64 64 64); /* neutral-ish */
stroke: rgb(23 23 23);
}

.grant-map svg path:hover {
fill: rgb(253 186 116); /* orange-300 */
}

${highlightCss}
`}</style>
</>
)
}
Loading