├── src ├── react-app-env.d.ts ├── i18n │ ├── index.ts │ ├── messages │ │ ├── index.ts │ │ ├── sv-SE.ts │ │ ├── es-MX.ts │ │ ├── pl-PL.ts │ │ ├── hu-HU.ts │ │ ├── it_IT.ts │ │ ├── pt-BR.ts │ │ ├── de-DE.ts │ │ ├── en-CA.ts │ │ └── fr-FR.ts │ ├── locales.ts │ └── LocaleContext.tsx ├── lib │ ├── alternateNames.d.ts │ ├── localStorage.d.ts │ ├── locale.d.ts │ └── country.d.ts ├── util │ ├── svg.ts │ ├── dates.tsx │ ├── answer.ts │ ├── colour.ts │ ├── globe.ts │ └── distance.ts ├── index.css ├── hooks │ ├── useInterval.tsx │ └── useLocalStorage.tsx ├── index.tsx ├── transitions │ └── Fade.tsx ├── context │ └── ThemeContext.tsx ├── components │ ├── BodyStyle.tsx │ ├── LanguagePicker.tsx │ ├── Outline.tsx │ ├── Footer.tsx │ ├── Toggle.tsx │ ├── Message.tsx │ ├── Header.tsx │ ├── SnackAdUnit.tsx │ ├── Auxilliary.tsx │ ├── Guesser.tsx │ ├── List.tsx │ ├── Globe.tsx │ └── Statistics.tsx ├── data │ ├── social_svgs.json │ ├── symbol_svgs.json │ ├── alternate_names.json │ └── territories.json ├── pages │ ├── Help.tsx │ ├── Settings.tsx │ ├── Info.tsx │ └── Game.tsx └── App.tsx ├── public ├── robots.txt ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── images │ ├── preview.png │ ├── earth-day.webp │ ├── plurality.png │ ├── earth-night.webp │ ├── safari-14-earth-day.jpg │ └── safari-14-earth-night.jpg ├── index.html └── ads.txt ├── tailwind.config.js ├── netlify.toml ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md ├── CHANGELOG.md └── LICENSE /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/preview.png -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import LocaleProvider from "./LocaleContext"; 2 | 3 | export default LocaleProvider; 4 | -------------------------------------------------------------------------------- /public/images/earth-day.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/earth-day.webp -------------------------------------------------------------------------------- /public/images/plurality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/plurality.png -------------------------------------------------------------------------------- /public/images/earth-night.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/earth-night.webp -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/safari-14-earth-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/safari-14-earth-day.jpg -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/safari-14-earth-night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-abe-train/globle/HEAD/public/images/safari-14-earth-night.jpg -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}",], theme: { extend: {}, }, plugins: [], darkMode: 'class' } -------------------------------------------------------------------------------- /src/lib/alternateNames.d.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from "./locale"; 2 | 3 | export type AltPair = { 4 | real: string; 5 | alternative: string; 6 | }; 7 | 8 | export type AltNames = Record; 9 | -------------------------------------------------------------------------------- /src/lib/localStorage.d.ts: -------------------------------------------------------------------------------- 1 | export type Stats = { 2 | gamesWon: number; 3 | lastWin: string; 4 | currentStreak: number; 5 | maxStreak: number; 6 | usedGuesses: number[]; 7 | emojiGuesses: string; 8 | }; 9 | 10 | export type Guesses = { 11 | day: string; 12 | countries: string[]; 13 | } 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run build" 4 | ## functions = "netlify/functions" 5 | publish = "build" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | -------------------------------------------------------------------------------- /src/util/svg.ts: -------------------------------------------------------------------------------- 1 | import symbolPaths from "../data/symbol_svgs.json"; 2 | import countryPaths from "../data/country_outlines.json"; 3 | import socialPaths from "../data/social_svgs.json"; 4 | 5 | export function getPath(name: string) { 6 | const allPaths = [...countryPaths, ...symbolPaths, ...socialPaths]; 7 | const obj = allPaths.find((p) => p.name === name); 8 | const path = obj?.path || ""; 9 | return path; 10 | } 11 | -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Globle", 3 | "short_name": "Globle", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .idea/ 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | 27 | .prettierrc 28 | .env 29 | dist/ 30 | 31 | -------------------------------------------------------------------------------- /src/util/dates.tsx: -------------------------------------------------------------------------------- 1 | export function dateDiffInDays(day1: string, day2: string) { 2 | const MS_PER_DAY = 1000 * 60 * 60 * 24; 3 | const a = new Date(day1); 4 | const b = new Date(day2); 5 | const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); 6 | const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); 7 | const diff = Math.floor((utc2 - utc1) / MS_PER_DAY); 8 | return diff; 9 | } 10 | 11 | export const today = new Date().toLocaleDateString("en-CA"); 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url("https://fonts.googleapis.com/css?family=Montserrat&display=swap"); 6 | @import url("https://fonts.googleapis.com/css?family=Amaranth&display=swap"); 7 | 8 | html { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | height: 100%; 15 | } 16 | 17 | .globe { 18 | -webkit-tap-highlight-color: transparent; 19 | cursor: -webkit-grab; 20 | cursor: -moz-grab; 21 | } 22 | 23 | .globe:active { 24 | cursor: -webkit-grabbing; 25 | cursor: -moz-grabbing; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useInterval.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | type Callback = () => void; 4 | 5 | export default function useInterval(callback: Callback, delay: number) { 6 | const savedCallback = useRef(null!); 7 | 8 | // Remember the latest callback. 9 | useEffect(() => { 10 | savedCallback.current = callback; 11 | }, [callback]); 12 | 13 | // Set up the interval. 14 | useEffect(() => { 15 | function tick() { 16 | savedCallback.current(); 17 | } 18 | if (delay !== null) { 19 | let id = setInterval(tick, delay); 20 | return () => clearInterval(id); 21 | } 22 | }, [delay]); 23 | } 24 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { ThemeProvider } from "./context/ThemeContext"; 6 | import LocaleProvider from "./i18n"; 7 | import BodyStyle from "./components/BodyStyle"; 8 | import { BrowserRouter } from "react-router-dom"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById("root") 22 | ); 23 | -------------------------------------------------------------------------------- /src/i18n/messages/index.ts: -------------------------------------------------------------------------------- 1 | import { LocaleMessages } from "../../lib/locale"; 2 | import { English } from "./en-CA"; 3 | import { Spanish } from "./es-MX"; 4 | import { French } from "./fr-FR"; 5 | import { German } from "./de-DE"; 6 | import { Hungarian } from "./hu-HU"; 7 | import { Portuguese } from "./pt-BR"; 8 | import { Italian } from "./it_IT"; 9 | import { Polish } from "./pl-PL"; 10 | import { Swedish } from "./sv-SE"; 11 | 12 | const localeList: LocaleMessages = { 13 | "en-CA": English, 14 | "de-DE": German, 15 | "es-MX": Spanish, 16 | "fr-FR": French, 17 | "hu-HU": Hungarian, 18 | "it-IT": Italian, 19 | "pl-PL": Polish, 20 | "pt-BR": Portuguese, 21 | "sv-SE": Swedish, 22 | }; 23 | 24 | export default localeList; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve", 22 | "typeRoots": [ 23 | "./src/lib/*.ts" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules" 28 | ], 29 | "include": [ 30 | "./src/**/*.tsx", 31 | "./src/**/*.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/util/answer.ts: -------------------------------------------------------------------------------- 1 | import { Country } from "../lib/country"; 2 | import { today } from "./dates"; 3 | 4 | const countryData: Country[] = require("../data/country_data.json").features; 5 | 6 | countryData.sort((a, b) => { 7 | return a.properties.FLAG[1].localeCompare(b.properties.FLAG[1]); 8 | }); 9 | 10 | const shuffleAdjust = today < "2022-08-01" ? "5" : "6"; 11 | 12 | function generateKeyNew(list: any[], day: string) { 13 | const [year, month, date] = day.split("-"); 14 | const dayCode = Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(date)); 15 | const SHUFFLE_KEY = process.env.REACT_APP_SHUFFLE_KEY || "1"; 16 | const key = 17 | Math.floor(dayCode / parseInt(SHUFFLE_KEY + shuffleAdjust)) % list.length; 18 | return key; 19 | } 20 | 21 | const key = generateKeyNew(countryData, today); 22 | 23 | export const answerCountry = countryData[key]; 24 | export const answerName = answerCountry.properties.NAME; 25 | -------------------------------------------------------------------------------- /src/transitions/Fade.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type Props = { 4 | show: boolean; 5 | children: any; 6 | background?: string; 7 | // preexist?: boolean; 8 | }; 9 | 10 | export default function Fade({ 11 | show, 12 | children, 13 | background, 14 | }: // preexist = false, 15 | Props) { 16 | const [appear, setAppear] = useState(false); 17 | const [exist, setExist] = useState(false); 18 | 19 | useEffect(() => { 20 | if (show) { 21 | setExist(true); 22 | setTimeout(() => setAppear(true), 150); 23 | } 24 | if (!show) { 25 | setAppear(false); 26 | setTimeout(() => setExist(false), 150); 27 | } 28 | }, [show]); 29 | 30 | return exist ? ( 31 |
38 | {children} 39 |
40 | ) : null; 41 | } 42 | -------------------------------------------------------------------------------- /src/i18n/locales.ts: -------------------------------------------------------------------------------- 1 | import { LanguageName } from "../lib/country"; 2 | import { Locale } from "../lib/locale"; 3 | import { German } from "./messages/de-DE"; 4 | import { English } from "./messages/en-CA"; 5 | import { Spanish } from "./messages/es-MX"; 6 | import { French } from "./messages/fr-FR"; 7 | import { Hungarian } from "./messages/hu-HU"; 8 | import { Polish } from "./messages/pl-PL"; 9 | import { Portuguese } from "./messages/pt-BR"; 10 | import { Swedish } from "./messages/sv-SE"; 11 | 12 | export const LOCALES = { 13 | English: English, 14 | Spanish: Spanish, 15 | French: French, 16 | German: German, 17 | Hungarian: Hungarian, 18 | Portuguese: Portuguese, 19 | Polish: Polish, 20 | Swedish: Swedish, 21 | }; 22 | 23 | export const langNameMap: Record = { 24 | "es-MX": "NAME_ES", 25 | "en-CA": "NAME_EN", 26 | "fr-FR": "NAME_FR", 27 | "de-DE": "NAME_DE", 28 | "hu-HU": "NAME_HU", 29 | "pt-BR": "NAME_PT", 30 | "pl-PL": "NAME_PL", 31 | "it-IT": "NAME_IT", 32 | "sv-SE": "NAME_SV", 33 | }; 34 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { today } from "../util/dates"; 3 | 4 | interface IStorage extends Object { 5 | day?: string; 6 | } 7 | 8 | function getStorageValue(key: string, defaultValue?: T): T { 9 | const saved = localStorage.getItem(key); 10 | if (saved) { 11 | return JSON.parse(saved); 12 | } else if (defaultValue) { 13 | return defaultValue; 14 | } else { 15 | throw new Error("Local storage error"); 16 | } 17 | } 18 | 19 | export function useLocalStorage( 20 | key: string, 21 | defaultValue: T 22 | ): [T, React.Dispatch>] { 23 | let [value, setValue] = useState(() => { 24 | return getStorageValue(key, defaultValue); 25 | }); 26 | 27 | useEffect(() => { 28 | const ex = value?.day ? value.day : "9999-99-99"; 29 | if (today <= ex) { 30 | localStorage.setItem(key, JSON.stringify(value)); 31 | } else { 32 | localStorage.setItem(key, JSON.stringify(defaultValue)); 33 | } 34 | }, [key, value, defaultValue]); 35 | 36 | return [value, setValue]; 37 | } 38 | -------------------------------------------------------------------------------- /src/i18n/LocaleContext.tsx: -------------------------------------------------------------------------------- 1 | import messages from "./messages"; 2 | 3 | import { Locale } from "../lib/locale"; 4 | 5 | import { IntlProvider } from "react-intl"; 6 | import { useLocalStorage } from "../hooks/useLocalStorage"; 7 | import { createContext, useEffect, useState } from "react"; 8 | 9 | type Props = { 10 | children: any; 11 | locale?: Locale; 12 | }; 13 | 14 | type LocaleContextType = { 15 | locale: Locale; 16 | setLocale: React.Dispatch> | null; 17 | }; 18 | 19 | const initialLocale: Locale = "en-CA"; 20 | 21 | const initialLocaleContext: LocaleContextType = { 22 | locale: initialLocale, 23 | setLocale: null, 24 | }; 25 | 26 | export const LocaleContext = 27 | createContext(initialLocaleContext); 28 | 29 | export default function LocaleProvider({ children }: Props) { 30 | const [storedLocale, storeLocale] = useLocalStorage( 31 | "locale", 32 | initialLocale 33 | ); 34 | 35 | const [locale, setLocale] = useState(storedLocale); 36 | 37 | useEffect(() => { 38 | storeLocale(locale); 39 | }, [storeLocale, locale]); 40 | 41 | return ( 42 | 43 | 44 | {children} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/context/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useEffect, useState } from "react"; 2 | import { useLocalStorage } from "../hooks/useLocalStorage"; 3 | 4 | // Use context as follows: 5 | // ThemeProvider > ThemeContext > themeContext > theme & setTheme 6 | 7 | type ProviderProps = { 8 | children: ReactNode; 9 | }; 10 | 11 | type Theme = { 12 | nightMode: boolean; 13 | highContrast: boolean; 14 | prideMode: boolean; 15 | }; 16 | 17 | type ThemeContextType = { 18 | theme: Theme; 19 | setTheme: React.Dispatch> | null; 20 | }; 21 | 22 | const initialTheme: Theme = { 23 | nightMode: false, 24 | highContrast: false, 25 | prideMode: false, 26 | }; 27 | 28 | const initialThemeContext: ThemeContextType = { 29 | theme: initialTheme, 30 | setTheme: null, 31 | }; 32 | 33 | export const ThemeContext = 34 | createContext(initialThemeContext); 35 | 36 | export const ThemeProvider = ({ children }: ProviderProps) => { 37 | const [storedTheme, storeTheme] = useLocalStorage( 38 | "theme", 39 | initialTheme 40 | ); 41 | 42 | const [theme, setTheme] = useState(storedTheme); 43 | 44 | useEffect(() => { 45 | storeTheme(theme); 46 | }, [storeTheme, theme]); 47 | 48 | return ( 49 | 50 | {children} 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/BodyStyle.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ThemeContext } from "../context/ThemeContext"; 3 | 4 | export default function BodyStyle() { 5 | const { nightMode } = useContext(ThemeContext).theme; 6 | 7 | const daySky = { 8 | background: `radial-gradient(ellipse at top, rgba(63, 201, 255, 0.7), transparent), 9 | radial-gradient(ellipse at bottom, rgba(255, 196, 87, 0.7), transparent) no-repeat fixed`, 10 | margin: 0, 11 | }; 12 | 13 | const nightSky = { 14 | background: `radial-gradient(ellipse at top, #160152, black), 15 | radial-gradient(ellipse at bottom, #7D3074, black) no-repeat fixed`, 16 | margin: 0, 17 | }; 18 | 19 | const stars = { 20 | background: 21 | "transparent url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/1231630/stars.png) repeat", 22 | opacity: 0.5, 23 | }; 24 | 25 | const clouds = { 26 | backgroundImage: 27 | "url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/131045/clouds.png), url(https://assets.codepen.io/557388/clouds.png)", 28 | backgroundRepeat: "repeat repeat", 29 | marginTop: "8rem", 30 | opacity: 0.2 31 | }; 32 | 33 | return ( 34 |
35 |
39 | {/*
*/} 40 |
45 | {/* */} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/util/colour.ts: -------------------------------------------------------------------------------- 1 | import { scaleSequentialSqrt } from "d3-scale"; 2 | import { 3 | interpolateBuPu, 4 | interpolateOrRd, 5 | interpolateGreys, 6 | interpolateTurbo, 7 | } from "d3-scale-chromatic"; 8 | import { Country } from "../lib/country"; 9 | import { polygonDistance } from "./distance"; 10 | 11 | const GREEN_SQUARE = "🟩"; 12 | const ORANGE_SQUARE = "🟧"; 13 | const RED_SQUARE = "🟥"; 14 | const WHITE_SQUARE = "⬜"; 15 | const YELLOW_SQUARE = "🟨"; 16 | 17 | const MAX_DISTANCE = 15_000_000; 18 | 19 | export const getColour = ( 20 | guess: Country, 21 | answer: Country, 22 | nightMode: boolean, 23 | highContrast: boolean, 24 | prideMode: boolean 25 | ) => { 26 | if (guess.properties?.TYPE === "Territory") { 27 | if (highContrast) return "white"; 28 | return "#BBBBBB"; 29 | } 30 | if (guess.properties.NAME === answer.properties.NAME) return "green"; 31 | if (guess.proximity == null) { 32 | guess["proximity"] = polygonDistance(guess, answer); 33 | } 34 | const gradient = highContrast 35 | ? interpolateGreys 36 | : prideMode 37 | ? interpolateTurbo 38 | : nightMode 39 | ? interpolateBuPu 40 | : interpolateOrRd; 41 | const colorScale = scaleSequentialSqrt(gradient).domain([MAX_DISTANCE, 0]); 42 | const colour = colorScale(guess.proximity); 43 | return colour; 44 | }; 45 | 46 | export const getColourEmoji = (guess: Country, answer: Country) => { 47 | if (guess.properties.NAME === answer.properties.NAME) return GREEN_SQUARE; 48 | if (guess.proximity == null) { 49 | guess["proximity"] = polygonDistance(guess, answer); 50 | } 51 | const scale = guess.proximity / MAX_DISTANCE; 52 | if (scale < 0.1) { 53 | return RED_SQUARE; 54 | } else if (scale < 0.25) { 55 | return ORANGE_SQUARE; 56 | } else if (scale < 0.5) { 57 | return YELLOW_SQUARE; 58 | } else { 59 | return WHITE_SQUARE; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "globle", 3 | "version": "1.6.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.4.0", 10 | "@types/node": "^16.11.20", 11 | "@types/react": "^17.0.38", 12 | "@types/react-dom": "^17.0.11", 13 | "d3-scale": "^4.0.2", 14 | "d3-scale-chromatic": "^3.0.0", 15 | "react": "^17.0.2", 16 | "react-device-detect": "^2.1.2", 17 | "react-dom": "^17.0.2", 18 | "react-globe.gl": "^2.20.1", 19 | "react-intl": "^5.24.7", 20 | "react-router-dom": "^6.3.0", 21 | "react-scripts": "^5.0.1", 22 | "spherical-geometry-js": "^3.0.0", 23 | "typescript": "^4.5.4", 24 | "web-vitals": "^2.1.3" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "ts": "tsc -w", 32 | "analyze": "source-map-explorer 'build/static/js/*.js'", 33 | "style-check": "npx tailwindcss -i ./src/index.css -o ./dist/output.css --watch" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@types/d3-scale": "^4.0.2", 55 | "@types/d3-scale-chromatic": "^3.0.0", 56 | "@types/react-transition-group": "^4.4.4", 57 | "autoprefixer": "^10.4.2", 58 | "postcss": "^8.4.5", 59 | "source-map-explorer": "^2.5.2", 60 | "tailwindcss": "^3.0.15" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/util/globe.ts: -------------------------------------------------------------------------------- 1 | import { browserVersion, isSafari } from "react-device-detect"; 2 | import { GlobeMethods } from "react-globe.gl"; 3 | import { Country } from "../lib/country"; 4 | import { computeArea, LatLng } from "spherical-geometry-js"; 5 | 6 | export function altitudeFunction(area: number) { 7 | // This function may seem arbitrary but I made it with a spreadsheet 8 | // and it made sense there. 9 | if (area >= 10) { 10 | return 1.5; 11 | } 12 | return 1 / (-2.55 * area + 26); 13 | } 14 | 15 | export function findCentre(country: Country) { 16 | const { bbox } = country; 17 | const [lng1, lat1, lng2, lat2] = bbox; 18 | const latitude = (lat1 + lat2) / 2; 19 | const longitude = (lng1 + lng2) / 2; 20 | const path = [ 21 | new LatLng(lat1, lng1), 22 | new LatLng(lat1, lng2), 23 | new LatLng(lat2, lng2), 24 | new LatLng(lat2, lng1), 25 | ]; 26 | const area = computeArea(path); 27 | const areaOoM = Math.log10(area); 28 | const altitude = altitudeFunction(areaOoM); 29 | return { lat: latitude, lng: longitude, altitude }; 30 | } 31 | 32 | export function turnGlobe( 33 | coords: { 34 | lat: number; 35 | lng: number; 36 | altitude?: number; 37 | }, 38 | globeRef: React.MutableRefObject, 39 | source?: string 40 | ) { 41 | const controls: any = globeRef.current.controls(); 42 | controls.autoRotate = false; 43 | const currentAlt = globeRef.current.pointOfView().altitude; 44 | coords["altitude"] = 45 | source === "zoom" && "altitude" in coords 46 | ? coords["altitude"] 47 | : Math.max(currentAlt, 0.05); 48 | globeRef.current.pointOfView(coords, 250); 49 | } 50 | 51 | export const globeImg = (nightMode: boolean) => { 52 | const time = nightMode ? "night" : "day"; 53 | if (isSafari && browserVersion < "14") { 54 | return `images/safari-14-earth-${time}.jpg`; 55 | } else { 56 | return `images/earth-${time}.webp`; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notes about Globle 2 | 3 | **Version 1.6.0** - [Change log](CHANGELOG.md) 4 | 5 | ## Listed countries 6 | - The list of countries for this game is the same as that used by [Sporcle](https://www.sporcle.com/blog/2013/01/what-is-a-country/) 7 | - Some alternate spellings and previous names are accepted, e.g. Burma for Myanmar. 8 | - France and UK have many territories scattered throughout the globe, and they confuse the proximity algorithm, so they are not highlighted when those countries are guessed. 9 | - Geography can be a sensitive topic, and some countries' borders are disputed. If you believe a correction should be made, please politely raise an issue or DM me on [Twitter](https://twitter.com/theAbeTrain). 10 | 11 | ## Tip 12 | If you are really struggling to find the answer, I recommend going to Google Maps or Earth. Better to learn about a new country than never get the answer! 13 | 14 | ## Attributions 15 | - This game was inspired by Wordle and the "Secret Country" geography games from [Sporcle](https://sporcle.com) 16 | - Country outlines in the Help screen provided by Vemaps.com 17 | - Favicons are from favicon.io 18 | 19 | # Running the project on your local machine 20 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). If you want to run the project on your local machine, 21 | 1. Clone this repo 22 | 2. `npm install` 23 | 3. `npm start` 24 | 25 | # License 26 | Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] 27 | 28 | This work is licensed under a 29 | [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa]. 30 | 31 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa] 32 | 33 | [cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ 34 | [cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png 35 | [cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg 36 | -------------------------------------------------------------------------------- /src/lib/locale.d.ts: -------------------------------------------------------------------------------- 1 | export type Locale = 2 | | "en-CA" 3 | | "es-MX" 4 | | "pt-BR" 5 | | "de-DE" 6 | | "fr-FR" 7 | | "hu-HU" 8 | | "it-IT" 9 | | "pl-PL" 10 | | "sv-SE"; 11 | 12 | export type Messages = { 13 | name: sting; 14 | helpTitle: string; 15 | help1: string; 16 | help2: string; 17 | France: string; 18 | Nepal: string; 19 | Mongolia: string; 20 | "South Korea": string; 21 | help3: string; 22 | Aux1: string; 23 | Aux2: string; 24 | Aux3: string; 25 | Footer1: string; 26 | Footer2: string; 27 | Footer3: string; 28 | Loading: string; 29 | FAQTitle: string; 30 | q1: string; 31 | a1: string; 32 | q2: string; 33 | a2: string; 34 | q3: string; 35 | a3: string; 36 | q4: string; 37 | a4: string; 38 | q5: string; 39 | a5: string; 40 | q6: string; 41 | a6: string; 42 | q7: string; 43 | a7: string; 44 | q8?: string; 45 | a8?: string; 46 | GameTitle: string; 47 | Game1: string; 48 | Game2: string; 49 | Game3: string; 50 | Game4: string; 51 | Game5: string; 52 | Game6: string; 53 | Game7: string; 54 | Game8: string; 55 | StatsTitle: string; 56 | Stats1: string; 57 | Stats2: string; 58 | Stats3: string; 59 | Stats4: string; 60 | Stats5: string; 61 | Stats6: string; 62 | Stats7: string; 63 | Stats8: string; 64 | Stats9: string; 65 | Stats10: string; 66 | Stats11: string; 67 | Stats12: string; 68 | SettingsTitle: string; 69 | Settings1: string; 70 | Settings2: string; 71 | Settings3: string; 72 | Settings4: string; 73 | Settings5: string; 74 | Settings6: string; 75 | Settings7: string; 76 | Settings8: string; 77 | Settings9: string; 78 | Settings10: string; 79 | Settings11: string; 80 | Answer: string; 81 | Closest: string; 82 | Guessed: string; 83 | PracticeMode: string; 84 | PracticeExit: string; 85 | PracticeNew: string; 86 | SortByGuesses: string; 87 | SortByDistance: string; 88 | }; 89 | 90 | export type LocaleMessages = Record; 91 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | Globle 40 | 44 | 49 | 54 | 55 | 56 | 57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/LanguagePicker.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | import { LocaleContext } from "../i18n/LocaleContext"; 4 | import messages from "../i18n/messages"; 5 | import { Locale } from "../lib/locale"; 6 | 7 | const langMap = { 8 | "en-CA": "English", 9 | "de-DE": "Deutsch", 10 | "es-MX": "Español", 11 | "fr-FR": "Français", 12 | "it-IT": "Italiano", 13 | "hu-HU": "Magyar", 14 | "pl-PL": "Polski", 15 | "pt-BR": "Português", 16 | "sv-SE": "Swedish", 17 | }; 18 | 19 | const languages = Object.keys(messages) as Locale[]; 20 | 21 | export default function LanguagePicker() { 22 | const localeContext = useContext(LocaleContext); 23 | const [selected, setSelected] = useState(localeContext.locale); 24 | 25 | useEffect(() => { 26 | if (localeContext.setLocale) { 27 | localeContext.setLocale(selected); 28 | } 29 | }, [selected, localeContext]); 30 | 31 | return ( 32 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Outline.tsx: -------------------------------------------------------------------------------- 1 | import { Country } from "../lib/country"; 2 | import { polygonDistance } from "../util/distance"; 3 | import { getColour } from "../util/colour"; 4 | import { useContext } from "react"; 5 | import { ThemeContext } from "../context/ThemeContext"; 6 | import { getPath } from "../util/svg"; 7 | import { FormattedMessage } from "react-intl"; 8 | const countryData: Country[] = require("../data/country_data.json").features; 9 | 10 | type Props = { 11 | countryName: string; 12 | width: number; 13 | }; 14 | 15 | export default function Outline({ countryName, width }: Props) { 16 | const { nightMode, highContrast, prideMode } = useContext(ThemeContext).theme; 17 | 18 | const country = countryData.find((p) => p.properties.NAME === countryName); 19 | if (!country) 20 | throw new Error("Country in Help screen not found in Country Data"); 21 | const countryCopy: Country = { ...country }; 22 | 23 | const sampleAnswerName = "Japan"; 24 | const sampleAnswer = countryData.find( 25 | (p) => p.properties.NAME === sampleAnswerName 26 | ); 27 | if (!sampleAnswer) 28 | throw new Error("Country in Help screen not found in Country Data"); 29 | 30 | countryCopy["proximity"] = polygonDistance(countryCopy, sampleAnswer); 31 | 32 | const outline = getPath(countryName); 33 | 34 | const colour = getColour( 35 | countryCopy, 36 | sampleAnswer, 37 | nightMode, 38 | highContrast, 39 | prideMode 40 | ); 41 | 42 | return ( 43 |
47 | 56 | 57 | 58 | 59 | 60 |
61 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ThemeContext } from "../context/ThemeContext"; 3 | import { getPath } from "../util/svg"; 4 | 5 | import { FormattedMessage } from "react-intl"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | export default function Footer() { 9 | const navigate = useNavigate(); 10 | const iconWidth = 14; 11 | const { nightMode } = useContext(ThemeContext).theme; 12 | 13 | return ( 14 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/data/social_svgs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "twitter", 4 | "path": "M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" 5 | }, 6 | { 7 | "name": "github", 8 | "path": "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" 9 | }, 10 | { 11 | "name": "coffee", 12 | "path": "M1.55747 1.25866C1.74689 0.927047 2.0206 0.65139 2.35087 0.459621C2.68114 0.267853 3.05623 0.166787 3.43813 0.166664H14.5618C14.9437 0.166787 15.3188 0.267853 15.6491 0.459621C15.9793 0.65139 16.253 0.927047 16.4425 1.25866L17.6807 3.42533C18.3827 4.65383 17.7338 6.13691 16.4999 6.55291C16.5053 6.64175 16.5042 6.73058 16.4977 6.8205L15.5693 19.8205C15.5304 20.3671 15.2858 20.8787 14.8848 21.2522C14.4837 21.6257 13.9561 21.8333 13.4081 21.8333H4.5908C4.04279 21.8333 3.51512 21.6257 3.11409 21.2522C2.71306 20.8787 2.46846 20.3671 2.42955 19.8205L1.50113 6.8205C1.4946 6.73145 1.49352 6.64209 1.49788 6.55291C0.26505 6.13691 -0.383867 4.65383 0.31705 3.42533L1.55638 1.25866H1.55747ZM3.66347 6.66666L3.8953 9.91666H14.1046L14.3365 6.66666H3.66347ZM4.36005 16.4167L4.59188 19.6667H13.4081L13.6399 16.4167H4.36005ZM15.8001 4.5L14.5618 2.33333H3.43813L2.19988 4.5H15.8001Z" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | function Toggle({ checked }: { checked: boolean }) { 2 | if (checked) { 3 | return ( 4 |
5 |
6 |
11 |
12 | ); 13 | } else { 14 | return ( 15 |
16 |
17 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | type Props = { 28 | name: string; 29 | toggle: boolean; 30 | setToggle: React.Dispatch>; 31 | on: string; 32 | off: string; 33 | }; 34 | 35 | export default function Switch({ name, toggle, setToggle, on, off }: Props) { 36 | function keyPressToggle( 37 | e: React.KeyboardEvent, 38 | toggle: boolean, 39 | setToggle: React.Dispatch> 40 | ) { 41 | const keys = ["Enter", " ", "Return"]; 42 | if (keys.includes(e.key)) { 43 | setToggle(!toggle); 44 | } 45 | } 46 | return ( 47 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/data/symbol_svgs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "help", 4 | "path": "M12 22c-5.52-.006-9.994-4.48-10-10v-.2C2.11 6.305 6.635 1.928 12.13 2c5.497.074 9.904 4.569 9.868 10.065C21.962 17.562 17.497 22 12 22zm-.016-2H12a8 8 0 1 0-.016 0zM13 18h-2v-2h2v2zm0-3h-2a3.583 3.583 0 0 1 1.77-3.178C13.43 11.316 14 10.88 14 10a2 2 0 1 0-4 0H8v-.09a4 4 0 1 1 8 .09a3.413 3.413 0 0 1-1.56 2.645A3.1 3.1 0 0 0 13 15z" 5 | }, 6 | { 7 | "name": "info", 8 | "path": "M11 11h2v6h-2zm0-4h2v2h-2z M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10s10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8s8 3.589 8 8s-3.589 8-8 8z" 9 | }, 10 | { 11 | "name": "stats", 12 | "path": "M16,11V3H8v6H2v12h20V11H16z M10,5h4v14h-4V5z M4,11h4v8H4V11z M20,19h-4v-6h4V19z" 13 | }, 14 | { 15 | "name": "settings", 16 | "path": "M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" 17 | }, 18 | { 19 | "name": "x", 20 | "path": "M285.08,230.397L456.218,59.27c6.076-6.077,6.076-15.911,0-21.986L423.511,4.565c-2.913-2.911-6.866-4.55-10.992-4.55 c-4.127,0-8.08,1.639-10.993,4.55l-171.138,171.14L59.25,4.565c-2.913-2.911-6.866-4.55-10.993-4.55 c-4.126,0-8.08,1.639-10.992,4.55L4.558,37.284c-6.077,6.075-6.077,15.909,0,21.986l171.138,171.128L4.575,401.505 c-6.074,6.077-6.074,15.911,0,21.986l32.709,32.719c2.911,2.911,6.865,4.55,10.992,4.55c4.127,0,8.08-1.639,10.994-4.55 l171.117-171.12l171.118,171.12c2.913,2.911,6.866,4.55,10.993,4.55c4.128,0,8.081-1.639,10.992-4.55l32.709-32.719 c6.074-6.075,6.074-15.909,0-21.986L285.08,230.397z" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | // import useCheckMobile from "../hooks/useCheckMobile"; 2 | import { isMobile } from "react-device-detect"; 3 | import { answerCountry, answerName } from "../util/answer"; 4 | import { FormattedMessage } from "react-intl"; 5 | import { useContext } from "react"; 6 | import { LocaleContext } from "../i18n/LocaleContext"; 7 | import { langNameMap } from "../i18n/locales"; 8 | import { Country } from "../lib/country"; 9 | 10 | type Props = { 11 | win: boolean; 12 | error: any; 13 | guesses: number; 14 | practiceMode: boolean; 15 | }; 16 | 17 | export function Message({ win, error, guesses, practiceMode }: Props) { 18 | const { locale } = useContext(LocaleContext); 19 | 20 | let name = answerName; 21 | if (locale !== "en-CA") { 22 | const langName = langNameMap[locale]; 23 | name = answerCountry["properties"][langName]; 24 | } 25 | if (practiceMode) { 26 | const answerCountry = JSON.parse( 27 | localStorage.getItem("practice") as string 28 | ) as Country; 29 | name = answerCountry.properties.NAME; 30 | if (locale !== "en-CA") { 31 | const langName = langNameMap[locale]; 32 | name = answerCountry["properties"][langName]; 33 | } 34 | } 35 | 36 | if (error) { 37 | return

{error}

; 38 | } else if (win) { 39 | return ( 40 |

41 | 42 |

43 | ); 44 | } else if (guesses === 0) { 45 | return ( 46 |

47 | 48 |

49 | ); 50 | } else if (guesses === 1) { 51 | return ( 52 |

53 | { 57 | try { 58 | const [click, tap] = JSON.parse(chunks); 59 | return isMobile ? {tap} : {click}; 60 | } catch (e) { 61 | return {chunks}; 62 | } 63 | }, 64 | }} 65 | /> 66 |

67 | ); 68 | } else { 69 | return

; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/pages/Help.tsx: -------------------------------------------------------------------------------- 1 | // import useCheckMobile from "../hooks/useCheckMobile"; 2 | import { useContext, useEffect, useState } from "react"; 3 | import { isMobile } from "react-device-detect"; 4 | import { ThemeContext } from "../context/ThemeContext"; 5 | import Fade from "../transitions/Fade"; 6 | import Outline from "../components/Outline"; 7 | import { FormattedMessage } from "react-intl"; 8 | import useInterval from "../hooks/useInterval"; 9 | import Auxilliary from "../components/Auxilliary"; 10 | 11 | export default function Help() { 12 | // Theme 13 | const { nightMode } = useContext(ThemeContext).theme; 14 | 15 | const countrySize = isMobile ? 125 : 150; 16 | 17 | const [outlines, setOutlines] = useState([]); 18 | const [count, setCount] = useState(1); 19 | useInterval(() => setCount(count + 1), 1000); 20 | useEffect(() => { 21 | const countryOutlines = ["France", "Nepal", "Mongolia", "South Korea"]; 22 | setOutlines(countryOutlines.slice(0, count)); 23 | }, [count]); 24 | 25 | return ( 26 |
27 |

31 | 32 |

33 |

34 | ( 38 | 39 | {chunks} 40 | 41 | ), 42 | }} 43 | /> 44 |

45 |

46 | {chunks} }} 49 | /> 50 |

51 |
52 |
58 | {outlines.map((country, idx) => { 59 | return ( 60 | 61 | 62 | 63 | ); 64 | })} 65 |
66 |
67 |

68 | 69 |

70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { Route, Routes, useSearchParams } from "react-router-dom"; 3 | import Game from "./pages/Game"; 4 | import Header from "./components/Header"; 5 | import Help from "./pages/Help"; 6 | import Info from "./pages/Info"; 7 | import Settings from "./pages/Settings"; 8 | import Statistics from "./components/Statistics"; 9 | import { ThemeContext } from "./context/ThemeContext"; 10 | import Fade from "./transitions/Fade"; 11 | import { MobileOnlyView, TabletView, BrowserView } from "react-device-detect"; 12 | import SnackAdUnit from "./components/SnackAdUnit"; 13 | 14 | function App() { 15 | // State 16 | const [reSpin, setReSpin] = useState(false); 17 | const [showStats, setShowStats] = useState(false); 18 | const [params] = useSearchParams(); 19 | const practiceMode = Boolean(params.get("practice_mode")); 20 | 21 | // Context 22 | const themeContext = useContext(ThemeContext); 23 | 24 | // Re-render globe 25 | useEffect(() => { 26 | if (reSpin) setTimeout(() => setReSpin(false), 1); 27 | }, [reSpin]); 28 | 29 | const dark = themeContext.theme.nightMode ? "dark" : ""; 30 | 31 | return ( 32 |
36 |
37 | 38 | 45 | 46 | 47 | 48 | } /> 49 | } 52 | /> 53 | } /> 54 | } /> 55 | 56 | {!practiceMode && ( 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | )} 69 |
70 | ); 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /src/util/distance.ts: -------------------------------------------------------------------------------- 1 | import * as geometry from "spherical-geometry-js"; 2 | import { Country } from "../lib/country"; 3 | 4 | function pointToCoordinates(point: Array) { 5 | // In the data, coordinates are [E/W (lng), N/S (lat)] 6 | // In the function, coordinates are [N/S (lat), E/W (lng)] 7 | // For both, West and South are negative 8 | const [lng, lat] = point; 9 | const coord = new geometry.LatLng(lat, lng); 10 | return coord; 11 | } 12 | 13 | function polygonPoints(country: Country) { 14 | const { geometry } = country; 15 | switch (geometry.type) { 16 | case "Polygon": 17 | return geometry.coordinates[0]; 18 | case "MultiPolygon": 19 | let points: number[][] = []; 20 | for (const polygon of geometry.coordinates) { 21 | points = [...points, ...polygon[0]]; 22 | } 23 | return points; 24 | default: 25 | throw new Error("Country data error"); 26 | } 27 | } 28 | 29 | function calcProximity(points1: number[][], points2: number[][]) { 30 | // Find min distance between 2 sets of points 31 | const EARTH_CIRCUMFERENCE = 40_075_000; 32 | let distance = EARTH_CIRCUMFERENCE / 2; 33 | for (let i = 0; i < points1.length; i++) { 34 | const point1 = points1[i]; 35 | const coord1 = pointToCoordinates(point1); 36 | for (let j = 0; j < points2.length; j++) { 37 | const point2 = points2[j]; 38 | const coord2 = pointToCoordinates(point2); 39 | const pointDistance = geometry.computeDistanceBetween(coord1, coord2); 40 | distance = Math.min(distance, pointDistance); 41 | } 42 | } 43 | // console.log("Country 1 points:", points1.length); 44 | // console.log("Country 2 points:", points2.length); 45 | // console.log("Total paths measured:", points1.length * points2.length); 46 | // console.log("Proximity is:", distance); 47 | return distance; 48 | } 49 | 50 | export function polygonDistance(country1: Country, country2: Country) { 51 | // console.log("Country 1:", country1.properties.NAME); 52 | // console.log("Country 2", country2.properties.NAME); 53 | const name1 = country1.properties.NAME; 54 | const name2 = country2.properties.NAME; 55 | if (name1 === "South Africa" && name2 === "Lesotho") return 0; 56 | if (name1 === "Lesotho" && name2 === "South Africa") return 0; 57 | if (name1 === "Italy" && name2 === "Vatican") return 0; 58 | if (name1 === "Vatican" && name2 === "Italy") return 0; 59 | if (name1 === "Italy" && name2 === "San Marino") return 0; 60 | if (name1 === "San Marino" && name2 === "Italy") return 0; 61 | const points1 = polygonPoints(country1); 62 | const points2 = polygonPoints(country2); 63 | return calcProximity(points1, points2); 64 | } 65 | -------------------------------------------------------------------------------- /src/data/alternate_names.json: -------------------------------------------------------------------------------- 1 | { 2 | "en-CA": [ 3 | { "real": "eswatini", "alternative": "swaziland" }, 4 | { "real": "myanmar", "alternative": "burma" }, 5 | { "real": "north macedonia", "alternative": "macedonia" }, 6 | { "real": "congo", "alternative": "congo-brazzaville" }, 7 | { "real": "vatican", "alternative": "holy see" }, 8 | { "real": "vatican", "alternative": "vatican city" }, 9 | { "real": "cabo verde", "alternative": "cape verde" }, 10 | { 11 | "real": "democratic republic of the congo", 12 | "alternative": "democratic republic of congo" 13 | }, 14 | { "real": "democratic republic of the congo", "alternative": "dr congo" }, 15 | { "real": "bosnia and herzegovina", "alternative": "bosnia" }, 16 | { "real": "ivory coast", "alternative": "cote d'ivoire" }, 17 | { "real": "ivory coast", "alternative": "côte d'ivoire" }, 18 | { "real": "ivory coast", "alternative": "cote divoire" }, 19 | { "real": "turkey", "alternative": "turkiye" } 20 | ], 21 | "fr-FR": [ 22 | { "real": "eswatini", "alternative": "swaziland" }, 23 | { "real": "myanmar", "alternative": "byrmanie" }, 24 | { "real": "united arab emirates", "alternative": "eau" }, 25 | { "real": "united arab emirates", "alternative": "émirats" }, 26 | { "real": "czechia", "alternative": "république tchèque" } 27 | ], 28 | "es-MX": [{ "real": "netherlands", "alternative": "holanda" }], 29 | "de-DE": [], 30 | "hu-HU": [], 31 | "pt-BR": [ 32 | { "real": "czechia", "alternative": "república tcheca" }, 33 | { "real": "czechia", "alternative": "tchéquia" }, 34 | { "real": "democratic republic of the congo", "alternative": "rd congo" }, 35 | { "real": "democratic republic of the congo", "alternative": "rdc" }, 36 | { "real": "eswatini", "alternative": "suazilândia" }, 37 | { "real": "djibouti", "alternative": "djibuti" }, 38 | { "real": "malawi", "alternative": "malaui" }, 39 | { "real": "mauritius", "alternative": "maurício" }, 40 | { "real": "papua new guinea", "alternative": "papua nova guiné" }, 41 | { "real": "turkmenistan", "alternative": "turcomenistão" }, 42 | { "real": "vietnam", "alternative": "vietnã" }, 43 | { "real": "bahrain", "alternative": "barein" }, 44 | { "real": "bahrain", "alternative": "bareine" }, 45 | { "real": "bahrain", "alternative": "barém" }, 46 | { "real": "united arab emirates", "alternative": "emirados árabes" }, 47 | { "real": "ireland", "alternative": "irlanda" }, 48 | { "real": "seychelles", "alternative": "seicheles" } 49 | ], 50 | "it-IT": [], 51 | "pl-PL": [ 52 | { "real": "georgia", "alternative": "abchazja" }, 53 | { "real": "china", "alternative": "chiny" } 54 | ], 55 | "sv-SE": [] 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { useNavigate, useSearchParams } from "react-router-dom"; 3 | import { ThemeContext } from "../context/ThemeContext"; 4 | import { getPath } from "../util/svg"; 5 | 6 | type Props = { 7 | setReSpin: React.Dispatch>; 8 | setShowStats: React.Dispatch>; 9 | }; 10 | 11 | export default function Header({ setReSpin, setShowStats }: Props) { 12 | const { theme } = useContext(ThemeContext); 13 | const navigate = useNavigate(); 14 | // Set up practice mode 15 | const [params] = useSearchParams(); 16 | const practiceMode = !!params.get("practice_mode"); 17 | 18 | function reRenderGlobe() { 19 | setReSpin(true); 20 | if (practiceMode) { 21 | return navigate("/"); 22 | } 23 | navigate("/game"); 24 | } 25 | 26 | const svgColour = theme.nightMode ? "rgb(209 213 219)" : "black"; 27 | 28 | return ( 29 |
30 |
31 |
32 | 42 |
43 | 54 |
55 | 65 | 75 |
76 |
77 |
78 | {/* */} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/SnackAdUnit.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | const ensureSnackLoaderIsLoaded = (site_id: string) => { 4 | return new Promise((resolve, reject) => { 5 | const doc = document; 6 | const scriptId = "snack_loader_" + site_id; 7 | const existingScript = doc.getElementById(scriptId); 8 | const isExistingScriptLoaded = 9 | (existingScript && existingScript.getAttribute("snack-loaded")) === 10 | "true"; 11 | console.log("isExistingScriptLoaded:", isExistingScriptLoaded); 12 | 13 | const resolveScript = () => { 14 | let existingScript = doc.getElementById(scriptId); 15 | if (existingScript) { 16 | existingScript.setAttribute("snack-loaded", "true"); 17 | resolve(); 18 | } 19 | }; 20 | 21 | if (isExistingScriptLoaded) { 22 | // Script has been loaded before, promise can be resolved. 23 | console.log("Resolving - script has been loaded before."); 24 | resolveScript(); 25 | return; 26 | } 27 | 28 | if (existingScript && !isExistingScriptLoaded) { 29 | // Script has been added to DOM before, but it's not fully loaded yet. 30 | existingScript.addEventListener("load", function () { 31 | console.log( 32 | "Resolving - script has been added to DOM before, but only now has fully loaded." 33 | ); 34 | resolveScript(); 35 | }); 36 | return; 37 | } 38 | 39 | // Script is not added to the DOM. 40 | var scriptElm = doc.createElement("script"); 41 | scriptElm.id = scriptId; 42 | //https://header-bidding.snack-dev.co.uk/assets/js/snack-loader 43 | //https://cdn-header-bidding.snack-media.com/assets/js/snack-loader/ 44 | scriptElm.src = 45 | "https://cdn-header-bidding.snack-media.com/assets/js/snack-loader/" + 46 | site_id; 47 | scriptElm.async = true; 48 | scriptElm.setAttribute("snack-loaded", "false"); 49 | scriptElm.addEventListener("load", function () { 50 | console.log( 51 | "Resolving - script has been added to DOM for the first time, and has now fully loaded." 52 | ); 53 | resolveScript(); 54 | }); 55 | var scriptsRef = doc.getElementsByTagName("script")[0]; 56 | if (scriptsRef.parentNode) { 57 | scriptsRef.parentNode.insertBefore(scriptElm, scriptsRef); 58 | } 59 | }); 60 | }; 61 | 62 | type Props = { 63 | unitName: string; 64 | siteId: string; 65 | }; 66 | function SnackAdUnit({ unitName, siteId }: Props) { 67 | useEffect(() => { 68 | const win = window as any; 69 | console.log("Initialising slot: ", unitName); 70 | ensureSnackLoaderIsLoaded(siteId).then(() => { 71 | console.log("Promisse to REFRESH resolved in Initiation bit.", unitName); 72 | win.refreshBid([unitName]); 73 | }); 74 | 75 | return () => { 76 | console.log("Killing slot: ", unitName); 77 | ensureSnackLoaderIsLoaded(siteId).then(() => { 78 | console.log("Promisse to KILL resolved in Initiation bit.", unitName); 79 | win.killSlot([unitName]); 80 | }); 81 | }; 82 | }, [siteId, unitName]); 83 | 84 | return
; 85 | } 86 | 87 | export default SnackAdUnit; 88 | -------------------------------------------------------------------------------- /src/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { ThemeContext } from "../context/ThemeContext"; 3 | import { LocaleContext } from "../i18n/LocaleContext"; 4 | import LanguagePicker from "../components/LanguagePicker"; 5 | import localeList from "../i18n/messages"; 6 | import { FormattedMessage } from "react-intl"; 7 | import Auxilliary from "../components/Auxilliary"; 8 | import { useNavigate } from "react-router-dom"; 9 | import { Country } from "../lib/country"; 10 | import Toggle from "../components/Toggle"; 11 | const countryData: Country[] = require("../data/country_data.json").features; 12 | 13 | export default function Settings() { 14 | const themeContext = useContext(ThemeContext); 15 | const [toggleTheme, setToggleTheme] = useState(!themeContext.theme.nightMode); 16 | const [togglePride, setTogglePride] = useState(!themeContext.theme.prideMode); 17 | const [toggleHighContrast, setToggleHighContrast] = useState( 18 | !themeContext.theme.highContrast 19 | ); 20 | const { locale } = useContext(LocaleContext); 21 | 22 | // eslint-disable-next-line 23 | const [toggleScope, setToggleScope] = useState(true); 24 | 25 | const { setTheme } = themeContext; 26 | 27 | const navigate = useNavigate(); 28 | 29 | useEffect(() => { 30 | if (setTheme) { 31 | setTheme({ 32 | nightMode: !toggleTheme, 33 | highContrast: !toggleHighContrast, 34 | prideMode: !togglePride, 35 | }); 36 | } 37 | }, [toggleTheme, toggleHighContrast, setTheme, togglePride]); 38 | 39 | function enterPracticeMode() { 40 | const practiceAnswer = 41 | countryData[Math.floor(Math.random() * countryData.length)]; 42 | localStorage.setItem("practice", JSON.stringify(practiceAnswer)); 43 | navigate("/game?practice_mode=true"); 44 | } 45 | 46 | const options = [ 47 | { 48 | name: "theme", 49 | setToggle: setToggleTheme, 50 | toggle: toggleTheme, 51 | on: localeList[locale]["Settings2"], 52 | off: localeList[locale]["Settings1"], 53 | }, 54 | { 55 | name: "pride", 56 | setToggle: setTogglePride, 57 | toggle: togglePride, 58 | on: localeList[locale]["Settings10"], 59 | off: localeList[locale]["Settings11"], 60 | }, 61 | { 62 | name: "accessibility", 63 | setToggle: setToggleHighContrast, 64 | toggle: toggleHighContrast, 65 | on: localeList[locale]["Settings3"], 66 | off: localeList[locale]["Settings4"], 67 | }, 68 | ]; 69 | 70 | return ( 71 |
75 | 76 | {options.map((option) => { 77 | return ; 78 | })} 79 | 91 | {!toggleScope && ( 92 |

93 | 94 |

95 | )} 96 | 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/Auxilliary.tsx: -------------------------------------------------------------------------------- 1 | import { lazy, Suspense, useContext, useEffect, useRef } from "react"; 2 | import { GlobeMethods } from "react-globe.gl"; 3 | import { ThemeContext } from "../context/ThemeContext"; 4 | import Footer from "./Footer"; 5 | import { isMobile } from "react-device-detect"; 6 | import { globeImg } from "../util/globe"; 7 | import { FormattedMessage } from "react-intl"; 8 | import { Link, useNavigate } from "react-router-dom"; 9 | // import SnackAdUnit from "./SnackAdUnit"; 10 | const ReactGlobe = lazy(() => import("react-globe.gl")); 11 | 12 | type Props = { 13 | screen: string; 14 | }; 15 | 16 | export default function Auxilliary({ screen }: Props) { 17 | // Navigation 18 | const navigate = useNavigate(); 19 | 20 | // Globe size settings 21 | const globeSize = 150; 22 | const extraStyle = { 23 | width: `${globeSize}px`, 24 | clipPath: `circle(${globeSize / 2}px at ${globeSize / 2}px ${ 25 | globeSize / 2 26 | }px)`, 27 | }; 28 | const globeRef = useRef(null!); 29 | 30 | const { nightMode } = useContext(ThemeContext).theme; 31 | 32 | useEffect(() => { 33 | setTimeout(() => { 34 | if (globeRef.current) { 35 | const controls: any = globeRef.current.controls(); 36 | controls.autoRotate = true; 37 | } 38 | }, 500); 39 | }, [globeRef]); 40 | 41 | function goToGame() { 42 | navigate("/game"); 43 | } 44 | 45 | const renderLoader = () => ( 46 |

47 | 48 |

49 | ); 50 | 51 | function keyPressToggle(e: React.KeyboardEvent) { 52 | const keys = ["Enter", " ", "Return"]; 53 | if (keys.includes(e.key)) { 54 | goToGame(); 55 | } 56 | } 57 | 58 | return ( 59 |
60 |
65 | 66 |
72 | 73 | 81 | 82 |
83 | 84 | { 88 | try { 89 | const [click, tap] = JSON.parse(chunks); 90 | return isMobile ? {tap} : {click}; 91 | } catch (e) { 92 | return {chunks}; 93 | } 94 | }, 95 | }} 96 | /> 97 | 98 | 99 |
100 | {(screen === "Help" || screen === "Settings") && ( 101 |

102 | Globle: Capitals is now available.{" "} 103 | 104 | Click here to play! 105 | {" "} 106 |

107 | )} 108 | {(screen === "Help" || screen === "Info") && ( 109 |
110 |
111 |
112 | )} 113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## v1.6.0 - May 31, 2022 4 | - Added translations for Italian and Polish (thank you international translators!) 5 | - Added rainbow theme for pride 🏳️‍🌈 6 | - Increased zoom speed 7 | - You can now toggle the list of guessed countries between "sort by order of guesses" and "sort by distance" 8 | - Added switch for miles to km in closest border preview 9 | - Rounded closest border preview to nearest 10 km/miles due to imprecisions in the game's borders 10 | - Added (shameful self-promotion) link to [Plurality](https://plurality.fun) to the stats menu 11 | - Added fallback error message to share score functionality 12 | - Added alternate names for some French, Spanish, and Portuguese countries 13 | 14 | ## v1.5.0 - Apr 25, 2022 15 | - Added Practice mode so players can play unlimited times per day to hone their geography skills without affecting their score 16 | - Added translations for French, German, and Portuguese (thank you international translators!) 17 | - Guessing or clicking on small countries now zooms in 18 | - Removed the check boxes visible in the Settings page on Safari 19 | - Added routing for the different pages (React Router v6) 20 | - Added "along the Earth's surface" to FAQ about distance calculation 21 | - Adjusted borders for Andaman and Nicobar Islands 22 | - Improved translation methodology to make the code more flexible for future translators (Issue #68) 23 | 24 | ## v1.4.0 - Mar 21, 2022 25 | - Translated game into Spanish 26 | - Fixed bug that caused the globe not to display on older browsers 27 | - Added "Closest border" helper to Game screen 28 | - Changed share message to remove URL and include emojis and hashtag 29 | - Created new territories for Kaliningrad, Canary Islands, Western Sahara, Martinique, and New Caledonia 30 | - Adjusted Cyprus borders to include Northern Cyprus 31 | - Added fade animation to countries on Help screen 32 | - Added explicit line about "Closer countries are hotter" to the Help page 33 | - Changed "Invalid country name" to "Invalid guess" 34 | - Removed yes/no buttons from "Stats erased" popup 35 | - Fixed spelling of "possible" in the meta tag 36 | - Changed coffee emoji to svg 37 | - Replaced react-transition-group with custom HOC 38 | 39 | ## v1.3.0 - Feb 25, 2022 40 | - Added zoom buttons for mobile 41 | - Add today's guesses to statistics popup 42 | - Added Share API for mobile players 43 | - Added Cape Verde as alternate spelling for Cabo Verde 44 | - Made Corsica, Svalbard, and Andaman and Nicobar Islands into territories 45 | - Improved styling on globe and winning text 46 | - Added analytics disclaimer to README 47 | - Added "buy me a coffee" link to the footer of the Help page 48 | 49 | ## v1.2.0 - Feb 13, 2022 50 | 51 | - Added "territories" to the game, which appear in a neutral colour when their sovereign country is guessed 52 | - Restructured Greenland, French Guiana, and Puerto Rico into territories 53 | - Created a new high-contrast mode for colour blind players (thank you GitHub user AH-Dietrich for the contribution!) 54 | - Added an FAQ section to address frequent questions and concerns about the game 55 | - Removed the zoom limit on recentering 56 | - Fixed bug that jumbled the order of guesses upon refresh 57 | 58 | ## v1.1.0 - Feb 4, 2022 59 | 60 | **Statistics** 61 | - Include today's guesses in stats sharing 62 | 63 | **Countries** 64 | - Add smaller countries, including Singapore and Andorra 65 | - Switch Crimea from Russia to Ukraine 66 | - Combined Greenland into Denmark 67 | - Added alternative names for some countries, such as Burma and eSwatini 68 | - Add disclaimer to README about which countries are used, as well as a link to the README in the Help screen footer 69 | 70 | **Navigation Controls** 71 | - Fixed the centring logic for auto point-of-view change for some countries, including Fiji 72 | - Fixed answer to actually be any country 73 | - Reduced aggressive auto-zoom when clicking on a country 74 | 75 | 76 | ## v1.0.0 - Jan 26, 2022 77 | 78 | Initial release -------------------------------------------------------------------------------- /src/lib/country.d.ts: -------------------------------------------------------------------------------- 1 | export type Country = { 2 | type: string; 3 | proximity: number; 4 | properties: { 5 | scalerank: number; 6 | featurecla: string; 7 | LABELRANK: number; 8 | SOVEREIGNT: string; 9 | SOV_A3: string; // function pickCountry(name: string) { 10 | // const country = countries.find(c => { 11 | // return c.properties.NAME === name; 12 | // }) 13 | // return country; 14 | // } 15 | ADM0_DIF: // return c.properties.NAME === name; 16 | // }) 17 | // return country; 18 | // } 19 | number; 20 | LEVEL: number; 21 | TYPE: string; 22 | ADMIN: string; // return c.properties.NAME === name; 23 | ADM0_A3: string; 24 | GEOU_DIF: number; 25 | GEOUNIT: string; 26 | GU_A3: string; 27 | SU_DIF: number; 28 | SUBUNIT: string; 29 | SU_A3: string; 30 | BRK_DIFF: number; 31 | NAME: string; 32 | NAME_LONG: string; 33 | BRK_A3: string; 34 | BRK_NAME: string; 35 | BRK_GROUP: null; 36 | ABBREV: string; 37 | POSTAL: string; // For both, West and South are negative 38 | FORMAL_EN: string | null; 39 | FORMAL_FR: string | null; 40 | NAME_CIAWF: string | null; 41 | NOTE_ADM0: string | null; 42 | NOTE_BRK: string | null; 43 | NAME_SORT: string; 44 | NAME_ALT: string | null; 45 | MAPCOLOR7: number; 46 | MAPCOLOR8: number; 47 | MAPCOLOR9: number; 48 | MAPCOLOR13: number; 49 | POP_EST: number; 50 | POP_RANK: number; 51 | GDP_MD_EST: number; 52 | POP_YEAR: number; 53 | LASTCENSUS: number; 54 | GDP_YEAR: number; 55 | ECONOMY: string; 56 | INCOME_GRP: string; 57 | WIKIPEDIA: number; 58 | FIPS_10_: string; 59 | ISO_A2: string; 60 | ISO_A2_EH: string; 61 | FLAG: string; 62 | ISO_A3: string; 63 | ISO_A3_EH: string; 64 | ISO_N3: string; 65 | UN_A3: string; 66 | WB_A2: string; 67 | WB_A3: string; 68 | WOE_ID: number; 69 | WOE_ID_EH: number; 70 | WOE_NOTE: string; 71 | ADM0_A3_IS: string; 72 | ADM0_A3_US: string; 73 | ADM0_A3_UN: number; 74 | ADM0_A3_WB: number; 75 | CONTINENT: string; 76 | REGION_UN: string; 77 | SUBREGION: string; 78 | REGION_WB: string; 79 | NAME_LEN: number; 80 | LONG_LEN: number; 81 | ABBREV_LEN: number; 82 | TINY: number; 83 | HOMEPART: number; 84 | MIN_ZOOM: number; 85 | MIN_LABEL: number; 86 | MAX_LABEL: number; 87 | NAME_AR: string; 88 | NAME_BN: string; 89 | NAME_DE: string; 90 | NAME_EN: string; 91 | NAME_ES: string; 92 | NAME_FA: string; 93 | NAME_FR: string; 94 | NAME_EL: string; 95 | NAME_HE: string; 96 | NAME_HI: string; 97 | NAME_HU: string; 98 | NAME_ID: string; 99 | NAME_IT: string; 100 | NAME_JA: string; 101 | NAME_KO: string; 102 | NAME_NL: string; 103 | NAME_PL: string; 104 | NAME_PT: string; 105 | NAME_RU: string; 106 | NAME_SV: string; 107 | NAME_TR: string; 108 | NAME_UK: string; 109 | NAME_UR: string; 110 | NAME_VI: string; 111 | NAME_ZH: string; 112 | NAME_ZHT: string; 113 | }; 114 | bbox: number[]; 115 | geometry: 116 | | { 117 | type: "Polygon"; 118 | coordinates: number[][][]; 119 | } 120 | | { 121 | type: "MultiPolygon"; 122 | coordinates: number[][][][]; 123 | }; 124 | }; 125 | 126 | type LanguageName = 127 | | "NAME_AR" 128 | | "NAME_BN" 129 | | "NAME_DE" 130 | | "NAME_EN" 131 | | "NAME_ES" 132 | | "NAME_FA" 133 | | "NAME_FR" 134 | | "NAME_EL" 135 | | "NAME_HE" 136 | | "NAME_HI" 137 | | "NAME_HU" 138 | | "NAME_ID" 139 | | "NAME_IT" 140 | | "NAME_JA" 141 | | "NAME_KO" 142 | | "NAME_NL" 143 | | "NAME_PL" 144 | | "NAME_PT" 145 | | "NAME_RU" 146 | | "NAME_SV" 147 | | "NAME_TR" 148 | | "NAME_UK" 149 | | "NAME_UR" 150 | | "NAME_VI" 151 | | "NAME_ZH" 152 | | "NAME_ZHT"; 153 | -------------------------------------------------------------------------------- /src/i18n/messages/sv-SE.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Swedish: Messages = { 4 | name: "Svenska", 5 | helpTitle: "Så här spelar du", 6 | help1: `Varje dag finns det ett nytt mysterieland. Ditt mål är att gissa mysterielandet med minst antal gissningar. Varje felaktig gissning kommer att dyka upp på jordklotet med en färg som visar hur nära den är mysterielandet. Ju varmare färgen är, desto närmare svaret är du.`, 7 | help2: `Till exempel, om mysterielandet är Japan, då skulle följande länder visas med dessa färger om man gissade:`, 8 | help3: `Ett nytt mysterieland är tillgängligt varje dag!`, 9 | France: "Frankrike", 10 | Nepal: "Nepal", 11 | Mongolia: "Mongoliet", 12 | "South Korea": "Sydkorea", 13 | Aux1: `["Klicka", "Tryck"] på globen för att spela!`, 14 | Aux2: "Har du en fråga?", 15 | Aux3: "Läs vår FAQ", 16 | Footer1: "av The Abe Train", 17 | Footer2: "Gillar du spelet?", 18 | Footer3: "Bjud mig på en kaffe", 19 | Loading: "Laddar...", 20 | FAQTitle: "FAQ", 21 | q1: "1. Hur beräknas avståndet mellan svaret och min gissning?", 22 | a1: "Avstånd mellan länder definieras som det minsta avståndet mellan deras gränser längs jordens yta.", 23 | q2: "2. Hur kan jag spela spelet om jag är färgblind eller synskadad?", 24 | a2: "Ett läge för färgblinda med hög kontrast kan aktiveras i .", 25 | q3: "3. Hur avgör spelet vad som är ett giltigt land?", 26 | a3: "Globle använder detta ramverk för att avgöra vad som är en giltig gissning.", 27 | q4: "4. Är autonoma men inte suveräna länder med i spelet?", 28 | a4: "Vissa territorier kommer att visas i en neutral färg när deras suveräna land gissas, t.ex. Grönland för Danmark. Placeringen av dessa territorier påverkar inte färgen på det suveräna landet. De flesta små territorier visas inte i spelet, t.ex. Curaçao .", 29 | q5: "5. Jag hittade dagens mystiska land! När får jag spela igen?", 30 | a5: "Det mystiska landet ändras och dina gissningar återställs vid midnatt i din tidszon.", 31 | q6: "6. Är alternativa stavningar för länder acceptabla?", 32 | a6: "Det finns många länder med flera acceptabla namn. Vissa alternativa stavningar och tidigare namn accepteras, t.ex. Burma för Myanmar. Dessutom är akronymer acceptabla för vissa länder med flera ord, t.ex. UAE för Förenade Arabemiraten.", 33 | q7: "7. Ett land saknas eller en gräns är felaktig. Vad kan jag göra åt det?", 34 | a7: "Geografi kan vara ett känsligt ämne, och vissa länders gränser är ifrågasatta. Om du anser att en korrigering bör göras, vänligen ta upp ett ärende på {GitHub} eller skicka ett DM till mig på {Twitter}.", 35 | GameTitle: "Spel", 36 | Game1: "Skriv namnet på landet här", 37 | Game2: "Gissa", 38 | Game3: "Skriv namnet på vilket land som helst för din första gissning.", 39 | Game4: `Drag, ["klicka", "tryck"], och zooma in på globen för att hjälpa dig hitta nästa gissning.`, 40 | Game5: "Ogiltig gissning", 41 | Game6: "Landet redan gissat", 42 | Game7: "Mysterielandet är {answer}!", 43 | Game8: "Närmsta gräns", 44 | StatsTitle: "Statistik", 45 | Stats1: "Senaste vinst", 46 | Stats2: "Dagens gissningar", 47 | Stats3: "Spel vunna", 48 | Stats4: "Nuvarande streak", 49 | Stats5: "Max streak", 50 | Stats6: "Genomsnittliga gissningar", 51 | Stats7: "Genomsnittliga gissningar", 52 | Stats8: "Återställ", 53 | Stats9: "Dela", 54 | Stats10: "Är du säker på att du vill radera din poäng?", 55 | Stats11: "Statistik raderad.", 56 | Stats12: "Kopierad till urklipp!", 57 | SettingsTitle: "Inställningar", 58 | Settings1: "Dagtema", 59 | Settings2: "Nattema", 60 | Settings3: "Läge för färgblinda på", 61 | Settings4: "Läge för färgblinda av", 62 | Settings5: "Länder", 63 | Settings6: "Städer", 64 | Settings7: "Språk", 65 | Settings8: "Globle: Cities Edition kommer snart!", 66 | Settings9: "Öva", 67 | Settings10: "Regnbåge på", 68 | Settings11: "Regnbåge av", 69 | Answer: "Svar", 70 | Closest: "Närmaste", 71 | PracticeMode: "Du är nu i övningsläge", 72 | PracticeExit: "Stäng av övningsläge", 73 | PracticeNew: "Nytt övningsspel", 74 | Guessed: "gissade", 75 | SortByGuesses: "Sortera efter gissningsordning", 76 | SortByDistance: "Sortera efter avstånd", 77 | }; 78 | -------------------------------------------------------------------------------- /src/i18n/messages/es-MX.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Spanish: Messages = { 4 | name: "Español", 5 | helpTitle: "Como jugar", 6 | help1: `Cada día habrá un País Secreto. Tu objetivo es adivinar el país secreto en el menor número de intentos posibles. Cada respuesta incorrrecta aparecerá en el globo con un color indicando que tan cerca se encuentra del País Secreto. Entre más caliente sea el color, más cerca estarás de la respuesta.`, 7 | help2: `Por ejemplo, si el País Secreto es Japón, entonces los siguientes países aparecerían de los siguientes colores si fueran seleccionados:`, 8 | France: "Francia", 9 | Nepal: "Nepal", 10 | Mongolia: "Mongolia", 11 | "South Korea": "Corea del Sur", 12 | help3: `¡Un nuevo País Secreto estará disponible todos los días!`, 13 | Aux1: "¡Haz click en el globo para jugar!", 14 | Aux2: "¿Tienes alguna duda?", 15 | Aux3: "Visita Preguntas Frecuentes.", 16 | Footer1: "Creado por The Abe Train", 17 | Footer2: "¿Estas disfrutando el juego?", 18 | Footer3: "Cómprame un café!", 19 | Loading: "Cargando...", 20 | FAQTitle: "Preguntas Frecuentes", 21 | q1: "1. ¿Cómo se calcula la distancia entre mi intento y la respuesta correcta?", 22 | a1: "La distancia entre países se define como la mínima distancia entre sus fronteras.", 23 | q2: "2. ¿Cómo puedo jugar el juego si soy daltónico o si tengo una discapacidad visual?", 24 | a2: "Una modalidad de alto contraste puede ser activada en .", 25 | q3: "3. ¿Cómo decide el juego que países son válidos?", 26 | a3: "Globle utiliza este marco para determinar que constituye un intento válido.", 27 | q4: "4. ¿Hay países autónomos no soberanos en el juego?", 28 | a4: "Algunos territorios aparecerán en un color neutral cuando su país soberano sea adivinado, por ejemplo. Groenlandia para Dinamarca. La ubicación de estos territorios no impacta el color del país soberano. La mayoría de los territorios pequeños no aparecen en el juego, por ejemplo. Curazao.", 29 | q5: "5. ¡Encontré el país secreto de hoy! ¿Cuando puedo jugar otra vez?", 30 | a5: "El pais secreto cambia y tus intentos se reajustan a la media noche de tu zona horaria.", 31 | q6: "", 32 | a6: "", 33 | q7: "6. Hace falta un país o una frontera es incorrecta. ¿Qué puedo hacer al respecto?", 34 | a7: "La geografía puede ser un tema sensible, y las fronteras de algunos países pueden ser disputadas. Si tu crees que una corrección es necesaria, por favor de manera respetuosa plantea la cuestion con {GitHub} o enviame un MD en {Twitter}.", 35 | GameTitle: "Juego", 36 | Game1: "Ingresa el nombre del país aquí", 37 | Game2: "Ingresar", 38 | Game3: "Ingresa el nombre de cualquier pais para iniciar el juego.", 39 | Game4: 40 | "Arrastra, haz click, y zoom-in en el globo para ubicar tu siguiente país.", 41 | Game5: "Intento inválido", 42 | Game6: "Pais ya adivinado", 43 | Game7: "¡El País Secreto es {answer}!", 44 | Game8: "Frontera más cercana", 45 | StatsTitle: "Estadísticas", 46 | Stats1: "Última victoria ", 47 | Stats2: "Intentos de hoy", 48 | Stats3: "Victorias", 49 | Stats4: "Racha actual", 50 | Stats5: "Racha max", 51 | Stats6: "Promedio de intentos", 52 | Stats7: "Promedio de intentos", 53 | Stats8: "Reajustar", 54 | Stats9: "Compartir", 55 | Stats10: "¿Estás seguro de que quieres restablecer tu puntuación?", 56 | Stats11: "Estadísticas borradas.", 57 | Stats12: "Copiado", 58 | SettingsTitle: "Ajustes", 59 | Settings1: "Tema Diurno", 60 | Settings2: "Tema Nocturno", 61 | Settings3: "Modalidad de Alto Contraste Apagado", 62 | Settings4: "Modalidad de Alto Contraste Activado", 63 | Settings5: "Países", 64 | Settings6: "Ciudades", 65 | Settings7: "Idioma", 66 | Settings8: "Próximamente Globle Edición de Ciudades", 67 | Settings9: "Practicar", 68 | Settings10: "Arco iris activado", 69 | Settings11: "Arco iris desactivado", 70 | Answer: "Respuesta", 71 | Closest: "Los países más cercanos", 72 | Guessed: "Guessed", //TODO: Translate 73 | PracticeMode: "You are in practice mode.", //TODO: Translate 74 | PracticeExit: "Exit practice mode", //TODO: Translate 75 | PracticeNew: "New practice game", //TODO: Translate 76 | SortByGuesses: "Sort by order of guesses", //TODO: Translate 77 | SortByDistance: "Sort by distance", //TODO: Translate 78 | }; 79 | 80 | // export default { 81 | // [LOCALES.Spanish]: esMessages, 82 | // }; 83 | -------------------------------------------------------------------------------- /src/i18n/messages/pl-PL.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Polish: Messages = { 4 | name: "Polski", 5 | helpTitle: "Jak grać", 6 | help1: `Każdego dnia pojawia się nowy Tajemniczy Kraj. Twoim zadaniem jest go odgadnąć przy jak najmniejszej liczbie prób. Każda nieprawidłowa odpowiedź pojawia się na kuli ziemskiej w kolorze wskazującym jak blisko jest do Tajemniczego Kraju. Im cieplejszy kolor, tym bliżej jesteś prawidłowej odpowiedzi.`, 7 | help2: `Na przykład, jeśli Tajemniczym Krajem jest Japonia, to poniższe kraje pojawią się w takich kolorach:`, 8 | help3: `Codziennie będzie dostępny nowy Tajemniczny Kraj!`, 9 | France: "Francja", 10 | Nepal: "Nepal", 11 | Mongolia: "Mongolia", 12 | "South Korea": "Korea Południowa", 13 | Aux1: `["Kliknij", "Dotknij"] kulę ziemską, żeby zagrać!`, 14 | Aux2: "Masz pytania?", 15 | Aux3: "Zobacz FAQ", 16 | Footer1: "stworzone przez The Abe Train", 17 | Footer2: "Podoba Ci się gra?", 18 | Footer3: "Kup mi kawę", 19 | Loading: "Ładowanie...", 20 | FAQTitle: "FAQ", 21 | q1: "1. Jak obliczana jest odległość pomiędzy zgadywaną a poprawną odpowiedzią?", 22 | a1: "Odległość pomiędzy krajami jest określona jako minimalna odległość między ich granicami na powierzchni Ziemi.", 23 | q2: "2. Jak mogę grać, jeśli jestem daltonistą lub osobą słabowidzącą?", 24 | a2: "W można aktywować tryb dla daltonistów.", 25 | q3: "3. W jaki sposób gra decyduje, które terytoria są państwem?", 26 | a3: "Globle używa tego frameworka, aby określić, jakie terytorium uzna za kraj.", 27 | q4: "4. Czy w grze są autonomiczne, ale niesuwerenne kraje?", 28 | a4: "Niektóre terytoria pojawią się w neutralnym kolorze, gdy odgadnie się ich suwerenne państwo, np. Grenlandia dla Danii. Położenie tych terytoriów nie wpływa na kolor suwerennego państwa. Większość małych terytoriów nie pojawia się w grze, m.in. Curaçao.", 29 | q5: "5. Znalazłem dzisiejszy tajemniczy kraj! Kiedy mogę znowu zagrać?", 30 | a5: "Tajemniczy kraj zmienia się, a Twoje odpowiedzi resetują, o północy w Twojej strefie czasowej.", 31 | q6: "6. Czy dopuszczalne są alternatywne nazwy krajów?", 32 | a6: "Istnieje wiele krajów o wielu dopuszczalnych nazwach. Akceptowana jest pewna alternatywna pisownia i poprzednie nazwy, np. Burma dla Myanmar. Również w niektórych krajach zawierających wiele słów dopuszczalne są akronimy, np. UAE dla United Arabe Emirates (Zjednoczonych Emiratów Arabskich).", 33 | q7: "7. Brak kraju lub granica jest nieprawidłowa. Co mogę z tym zrobić?", 34 | a7: "Geografia może być delikatnym tematem, a granice niektórych krajów są sporne. Jeśli uważasz, że należy wprowadzić poprawki, dodaj proszę issue na {GitHub} lub napisz do mnie na {Twitter}.", 35 | GameTitle: "Gra", 36 | Game1: "Wpisz nazwę kraju", 37 | Game2: "Wpisz", 38 | Game3: "Wpisz nazwę jakiegoś kraju, aby zacząć grę", 39 | Game4: `Przeciągnij, ["kliknij", "dotknij"] i powiększ kulę ziemską, żeby ułatwić sobie dalsze zgadywanie.`, 40 | Game5: "Nieprawidłowa nazwa", 41 | Game6: "Ten kraj już się pojawił", 42 | Game7: "Tajemniczy Kraj to {answer}!", 43 | Game8: "Najbliższa granica", 44 | StatsTitle: "Statystyki", 45 | Stats1: "Ostatnie zwycięstwo", 46 | Stats2: "Dzisiejsze próby", 47 | Stats3: "Wygrane gry", 48 | Stats4: "Wygrane gry z rzędu", 49 | Stats5: "Najwięcej wygranych gier z rzędu", 50 | Stats6: "Średnia liczba prób", 51 | Stats7: "Średnia liczba prób", 52 | Stats8: "Zresetuj", 53 | Stats9: "Udostępnij", 54 | Stats10: "Jesteś pewny, że chcesz zresetować swoje wyniki?", 55 | Stats11: "Statystyki zostały usunięte.", 56 | Stats12: "Skopiowane do schowka!", 57 | SettingsTitle: "Ustawienia", 58 | Settings1: "Tryb dzienny", 59 | Settings2: "Tryb nocny", 60 | Settings3: "Tryb dla daltonistów włączony", 61 | Settings4: "Tryb dla daltonistów wyłączony", 62 | Settings5: "Kraje", 63 | Settings6: "Miasta", 64 | Settings7: "Język", 65 | Settings8: "Globle: Miasta już wkrótce!", 66 | Settings9: "Trenuj", 67 | Settings10: "Tęcza aktywowana", 68 | Settings11: "Tęcza dezaktywowana", 69 | Answer: "Odpowiadać", 70 | Closest: "Najbliższy", 71 | Guessed: "Guessed", //TODO: Translate 72 | PracticeMode: "Jesteś w trybie ćwiczeń.", 73 | PracticeExit: "Wyjdź z trybu ćwiczeń", 74 | PracticeNew: "Nowa gra treningowa", 75 | SortByGuesses: "Sort by order of guesses", //TODO: Translate 76 | SortByDistance: "Sort by distance", //TODO: Translate 77 | }; 78 | -------------------------------------------------------------------------------- /src/i18n/messages/hu-HU.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Hungarian: Messages = { 4 | name: "Magyar", 5 | helpTitle: "Hogy játsszunk", 6 | help1: `Minden nap van egy új ország. A te dolgod, hogy a legkevesebb találgatással 7 | megtaláld ezt az országot. Minden hibás találgatás megjelenik a földgömbön. 8 | Az országok színe attól függ, hogy milyen messze vannak a kitalálandó országtól. 9 | Minél közelebb van az ország annál melegebb lesz a szín.`, 10 | help2: `Például ha Japán a kitalálandó ország akkor 11 | az alábbi országok ilyen szinekkel jelennek majd meg:`, 12 | help3: `Minden nap egy másik országot kell kitalálni!`, 13 | France: "Franciaország", 14 | Nepal: "Nepál", 15 | Mongolia: "Mongólia", 16 | "South Korea": "Dél-Korea", 17 | Aux1: `["Kattincs", "Tap"] a földgömbre!`, 18 | Aux2: "Van kérdésed?", 19 | Aux3: "Nézd meg a gyakori kérdéseket", 20 | Footer1: "The Abe Train által", 21 | Footer2: "Élvezed a játékot?", 22 | Footer3: "Hívj meg egy kávera", 23 | Loading: "Betöltés...", 24 | FAQTitle: "FAQ", 25 | q1: "1. How is the distance between the answer and my guess calculated?", 26 | a1: "Distance between countries is defined as the minimum distance between their land borders along the Earth's surface. Territories (areas that appear in gray when their parent country is guessed) and water borders are not included in distance calculations.", 27 | q2: "2. How can I play the game if I am colour blind or visually impaired?", 28 | a2: "A high-contrast Colour Blind mode can be activated in .", 29 | q3: "3. How does the game decide what is a valid country?", 30 | a3: "Globle uses this framework to determine what constitutes a valid guess.", 31 | q4: "4. Are autonomous but not sovereign countries in the game?", 32 | a4: "Some territories will appear in a neutral colour when their sovereign country is guessed, e.g. Greenland for Denmark. The location of these territories does not impact the colour of the sovereign country. Most small territories do not appear in the game, e.g. Curaçao.", 33 | q5: "5. I found today's mystery country! When do I get to play again?", 34 | a5: "The mystery country changes and your guesses reset at midnight in your time zone.", 35 | q6: "6. Are alternative spellings for countries acceptable?", 36 | a6: "There are many countries with multiple acceptable names. Some alternate spellings and previous names are accepted, e.g. Burma for Myanmar. As well, acronyms are acceptable for some multi-word countries, e.g. UAE for United Arab Emirates.", 37 | q7: "7. A country is missing or a border is incorrect. What can I do about it?", 38 | a7: "Geography can be a sensitive topic, and some countries' borders are disputed. If you believe a correction should be made, please politely raise an issue on {GitHub} or DM me on {Twitter}.", 39 | GameTitle: "A játék", 40 | Game1: "Ide írd be az ország nevét", 41 | Game2: "Enter", 42 | Game3: "Írj be egy országnevet", 43 | Game4: `Húzd, ["kattincs", "tap"], nagyítsd meg a földgömböt hogy megtaláld a következő országot.`, 44 | Game5: "Hibás találgatás", 45 | Game6: "Ezt az országot már próbáltad", 46 | Game7: "A keresett ország {answer}!", 47 | Game8: "Legközelebbi határ:", 48 | StatsTitle: "Statisztika", 49 | Stats1: "Utolsó nyerés", 50 | Stats2: "Mai próbálkozások", 51 | Stats3: "Nyert játékok", 52 | Stats4: "Mostani sorozat", 53 | Stats5: "Legosszabb sorozat", 54 | Stats6: "Próbálkozások átlaga", 55 | Stats7: "Átlag próbálkozások", 56 | Stats8: "Újrakezdés", 57 | Stats9: "Megosztás", 58 | Stats10: "Biztos hogy törölni akarod a pontjaidat?", 59 | Stats11: "Statisztika törölve.", 60 | Stats12: "Vágólapba másolva!", 61 | SettingsTitle: "Beállítások", 62 | Settings1: "Nappali verzió", 63 | Settings2: "Éjszakai verzió", 64 | Settings3: "Színtévesztő mód be", 65 | Settings4: "Színtévesztő mód ki", 66 | Settings5: "Országok", 67 | Settings6: "Városok", 68 | Settings7: "Nyelv", 69 | Settings8: "Globle: A városok változat jön nemsokára!", 70 | Settings9: "Gyakorlás", 71 | Settings10: "Szivárvány be", 72 | Settings11: "Szivárvány ki", 73 | Answer: "Válasz", 74 | Closest: "Legközelebbi", 75 | PracticeMode: "Gyakorló üzemmódban vagy.", 76 | PracticeExit: "Lépj ki a gyakorló üzemmódból", 77 | PracticeNew: "Új gyakorló játék", 78 | Guessed: "sejtette", 79 | SortByGuesses: "Rendezés a találgatások sorrendje szerint", 80 | SortByDistance: "Rendezés távolság szerint", 81 | }; 82 | -------------------------------------------------------------------------------- /src/i18n/messages/it_IT.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Italian: Messages = { 4 | name: "Italiano", 5 | helpTitle: "Come giocare", 6 | help1: `Ogni giorno c'è un nuovo Paese Misterioso. Il tuo obiettivo è indovinare il 7 | paese misterioso usando il minor numero di ipotesi. Ogni ipotesi errata 8 | apparirà sul globo con un colore che indica quanto sia vicino al 9 | Paese misterioso. Più il colore è caldo, più sei vicino alla risposta.`, 10 | help2: `Ad esempio, se il paese misterioso è Giappone, appariranno i seguenti paesi 11 | con questi colori:`, 12 | help3: `Ogni giorno sarà disponibile una nuova Paese Misterioso!`, 13 | France: "Francia", 14 | Nepal: "Nepal", 15 | Mongolia: "Mongolia", 16 | "South Korea": "Corea del Sud", 17 | Aux1: `["Clicca", "Tap"] sul globo per giocare!`, 18 | Aux2: "Hai una domanda?", 19 | Aux3: "Vai a vedere le FAQ", 20 | Footer1: "di The Abe Train", 21 | Footer2: "Ti piace il gioco?", 22 | Footer3: "Offrimi un caffè", 23 | Loading: "Caricamento...", 24 | FAQTitle: "FAQ", 25 | q1: "1. Come viene calcolata la distanza tra la risposta e la mia ipotesi?", 26 | a1: "La distanza tra paesi è definita come la distanza minima tra i loro confini lungo la superficie terrestre.", 27 | q2: "2. Come posso giocare se sono daltonico o ipovedente?", 28 | a2: "È possibile attivare una modalità daltonico ad alto contrasto nelle .", 29 | q3: "3. In che modo il gioco decide quale è un paese valido?", 30 | a3: "Globe utilizza questo framework tper determinare cosa costituisce un'ipotesi valida.", 31 | q4: "4. Ci sono paesi autonomi ma non indipendenti nel gioco?", 32 | a4: "Alcuni territori appariranno in un colore neutro quando viene indovinato il loro paese sovrano, ad es. Groenlandia per la Danimarca. La posizione di questi territori non influisce sul colore del paese sovrano. La maggior parte dei piccoli territori non compare nel gioco, ad es. Curacao.", 33 | q5: "5. Ho trovato il paese misterioso di oggi! Quando potrò giocare di nuovo?", 34 | a5: "Il paese misterioso cambia e le tue ipotesi vengono ripristinate a mezzanotte nel tuo fuso orario.", 35 | q6: "6. Sono accettati nomi di paesi alternativi?", 36 | a6: "Esistono molti paesi con più nomi accettabili. Sono accettate alcune grafie alternative e nomi precedenti, ad es. Birmania per il Myanmar. Inoltre, gli acronimi sono accettabili per alcuni paesi con più parole, ad es. UAE per gli Emirati Arabi Uniti.", 37 | q7: "7. Un paese è mancante o un confine non è corretto. Cosa posso fare?", 38 | a7: "La geografia può essere un argomento delicato e alcuni confini sono controversi. Se ritieni che sia necessario apportare una correzione, segnala un errore su {GitHub} o inviami un DM su {Twitter}.", 39 | GameTitle: "Gioco", 40 | Game1: "Inserisci qui il nome di un paese", 41 | Game2: "Inserire", 42 | Game3: 43 | "Inserisci il nome di qualsiasi paese per fare il tuo primo tentativo.", 44 | Game4: `Scorri, ["click", "tap"] e ingrandisci il globo per trovare il tuo prossimo tentativo..`, 45 | Game5: "Tentativo non valido", 46 | Game6: "Paese già provato", 47 | Game7: "Il paese misterioso è {answer}!", 48 | Game8: "Confine più vicino", 49 | StatsTitle: "Statistiche", 50 | Stats1: "Ultima vittoria", 51 | Stats2: "Tentativi di oggi", 52 | Stats3: "Partite vinte", 53 | Stats4: "Serie attuale", 54 | Stats5: "Serie max", 55 | Stats6: "Tentativo medio", 56 | Stats7: "Avg. Medi", 57 | Stats8: "Ripristina", 58 | Stats9: "Condividi", 59 | Stats10: "Sei sicuro di voler azzerare il tuo punteggio?", 60 | Stats11: "Statistiche cancellate.", 61 | Stats12: "Copiato negli appunti!", 62 | SettingsTitle: "Impostazioni", 63 | Settings1: "Tema diurno", 64 | Settings2: "Tema notturno", 65 | Settings3: "Modalità daltonici abilitata", 66 | Settings4: "Modalità daltonica disattivata", 67 | Settings5: "Paesi", 68 | Settings6: "Città", 69 | Settings7: "Lingua", 70 | Settings8: "Globle: Edizione Città in arrivo!", 71 | Settings9: "Pratica", 72 | Settings10: "Arcobaleno attivato", 73 | Settings11: "Arcobaleno disattivato", 74 | Answer: "Risposta", 75 | Closest: "Paesi più vicini", 76 | Guessed: "Guessed", //TODO: Translate 77 | PracticeMode: "Sei in modalità allenamento.", 78 | PracticeExit: "Esci dalla modalità allenamento", 79 | PracticeNew: "Nuovo gioco di pratica", 80 | SortByGuesses: "Sort by order of guesses", //TODO: Translate 81 | SortByDistance: "Sort by distance", //TODO: Translate 82 | }; 83 | -------------------------------------------------------------------------------- /src/i18n/messages/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const Portuguese: Messages = { 4 | name: "Português Brasileiro", 5 | helpTitle: "Como jogar", 6 | help1: `Todo dia, há um novo País Misterioso. Seu objetivo é advinhar 7 | o País Misterioso usando o menor número de tentativas. Cada tentativa incorreta 8 | aparecerá no globo com uma cor indicando o quão próximo está do país misterioso. 9 | Quanto mais quente a cor, mais próximo você está da resposta.`, 10 | help2: `Por exemplo, se o País Misterioso for Japão, então os países a seguir 11 | apareceriam com as seguintes cores, se chutados:`, 12 | help3: `Um novo País Misterioso estará disponível a cada dia!`, 13 | France: "França", 14 | Nepal: "Nepal", 15 | Mongolia: "Mongólia", 16 | "South Korea": "Coréia do Sul", 17 | Aux1: `["Clique", "Toque"] no globo para jogar`, 18 | Aux2: "Alguma dúvida?", 19 | Aux3: "Cheque a área Perguntas Frequentes", 20 | Footer1: "por The Abe Train", 21 | Footer2: "Curtindo o jogo?", 22 | Footer3: "Pague meu cafezinho", 23 | Loading: "Carregando...", 24 | FAQTitle: "Perguntas Frequentes", 25 | q1: "1. Como a distância entre a resposta e meu palpite é calculada?", 26 | a1: "A distância entre países é definida como a menor distância entre suas bordas", 27 | q2: "2. Como eu posso jogar se eu for daltônico ou tiver problemas de visão?", 28 | a2: "Um modo de alto contraste para Daltônicos pode ser ativado em .", 29 | q3: "3. Como o jogo decide quais países são válidos?", 30 | a3: "Globle usa este framework para determinar o que constitue um palpite válido.", 31 | q4: "4. Países autônomos, mas não soberanos, estão no jogo?", 32 | a4: "Alguns territórios vão aparecer numa cor neutra quando seu país soberano for chutado, ex. Greenland (Groênlandia) para Denmark (Dinamarca). A localização destes territórios não impacta a cor do país soberano. A maioria dos territórios pequenos não aparecem no jogo, ex. Curaçao.", 33 | q5: "5. Acertei o País Misterioso de hoje! Quando posso jogar de novo?", 34 | a5: "O país misterioso muda e seus palpites são reiniciados à meia-noite no seu horário local.", 35 | q6: "6. Grafias alternativas dos nomes dos países são aceitas?", 36 | a6: "Há vários países com múltiplos nomes aceitáveis. Algumas grafias alternativas e nomes anteriores são aceitos, ex. Burma (Birmânia) para Myanmar . Acrônimos também são aceitáveis para nomes com mais de uma palavra, ex. UAE para United Arabe Emirates (Emirados Árabes Unidos).", 37 | q7: "7. Um país está faltando ou uma fronteira está incorreta. O que eu posso fazer sobre isto?", 38 | a7: "Geografia pode ser um tema sensível e as fronteiras de alguns países são disputadas. Se você acredita que uma correção deve ser feita, por favor, educadamente, crie um issue no {GitHub} mande uma mensagem direta no {Twitter}.", 39 | GameTitle: "Jogo", 40 | Game1: "Insira o nome de um país aqui", 41 | Game2: "Inserir", 42 | Game3: "Insira o nome de qualquer país para fazer sua primeira tentativa.", 43 | Game4: `Arraste, ["clique", "toque"], e dê zoom no globo para te ajudar a decidir seu próximo palpite.`, 44 | Game5: "Tentativa inválida", 45 | Game6: "Você já chutou este país", 46 | Game7: "O país misterioso é {answer}!", 47 | Game8: "Fronteira mais próxima", 48 | StatsTitle: "Estatísticas", 49 | Stats1: "Última vitória", 50 | Stats2: "Palpites de hoje", 51 | Stats3: "Jogos ganhos", 52 | Stats4: "Sequência atual", 53 | Stats5: "Maior sequência", 54 | Stats6: "Média de tentativa", 55 | Stats7: "Méd. Tentativas", 56 | Stats8: "Reiniciar", 57 | Stats9: "Compartilhar", 58 | Stats10: "Tem certeza que quer reiniciar sua pontuação?", 59 | Stats11: "Estatísticas apagadas.", 60 | Stats12: "Copiado para a área de transferência!", 61 | SettingsTitle: "Configurações", 62 | Settings1: "Tema Diurno", 63 | Settings2: "Tema Noturno", 64 | Settings3: "Modo Daltônico Ligado", 65 | Settings4: "Modo Daltônico Desligado", 66 | Settings5: "Países", 67 | Settings6: "Cidades", 68 | Settings7: "Idioma", 69 | Settings8: "Globle: Edição Cidades em breve!", 70 | Settings9: "Praticar", 71 | Settings10: "Arco-íris ativado", 72 | Settings11: "Arco-íris desativado", 73 | Answer: "Resposta", 74 | Closest: "Mais próximo", 75 | Guessed: "Chutados", 76 | PracticeMode: "Você está no modo de treino.", 77 | PracticeExit: "Sair do modo de treino", 78 | PracticeNew: "Novo jogo de trino", 79 | SortByGuesses: "Listar pela ordem dos chutes", 80 | SortByDistance: "Ordenar pela distância", 81 | }; 82 | -------------------------------------------------------------------------------- /src/pages/Info.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | 3 | import { FormattedMessage } from "react-intl"; 4 | import { Link } from "react-router-dom"; 5 | import { LocaleContext } from "../i18n/LocaleContext"; 6 | 7 | type ItemProps = { 8 | q: JSX.Element; 9 | a: JSX.Element; 10 | }; 11 | 12 | function Item({ q, a }: ItemProps, idx: number) { 13 | const [open, setOpen] = useState(false); 14 | 15 | const question = ( 16 |
setOpen(!open)} 20 | > 21 | {q} 22 |
23 | ); 24 | if (open) { 25 | return ( 26 |
27 | {[question, a]} 28 |
29 | ); 30 | } else { 31 | return
{question}
; 32 | } 33 | } 34 | 35 | export default function Info() { 36 | const localeContext = useContext(LocaleContext); 37 | 38 | const faqs = [ 39 | { 40 | q: , 41 | a: ( 42 |
43 | {" "} 44 |
45 | ), 46 | }, 47 | { 48 | q: , 49 | a: ( 50 |
51 | { 55 | return ( 56 | 57 | 58 | 59 | ); 60 | }, 61 | }} 62 | /> 63 |
64 | ), 65 | }, 66 | { 67 | q: , 68 | a: ( 69 |
70 | { 74 | return ( 75 | 79 | {chunks} 80 | 81 | ); 82 | }, 83 | }} 84 | /> 85 |
86 | ), 87 | }, 88 | { 89 | q: , 90 | a: ( 91 |
92 | 93 |
94 | ), 95 | }, 96 | { 97 | q: , 98 | a: ( 99 |
100 | 101 |
102 | ), 103 | }, 104 | { 105 | q: , 106 | a: ( 107 |
108 | 109 |
110 | ), 111 | }, 112 | { 113 | q: , 114 | a: ( 115 |
116 | 124 | GitHub 125 | 126 | ), 127 | Twitter: ( 128 | 133 | Twitter 134 | 135 | ), 136 | }} 137 | /> 138 |
139 | ), 140 | }, 141 | { 142 | q: , 143 | a: ( 144 |
145 | 146 |
147 | ), 148 | }, 149 | ]; 150 | 151 | // Need to skip question 6 if not English or French because it doesn't apply. 152 | if (localeContext.locale !== "en-CA" && localeContext.locale !== "fr-FR") { 153 | faqs.splice(5, 1); 154 | } 155 | 156 | return ( 157 |
158 |

162 | 163 |

164 |
165 | {faqs.map((faq, idx) => Item(faq, idx))} 166 |
167 |
168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /src/i18n/messages/de-DE.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const German: Messages = { 4 | name: "Deutsch", 5 | helpTitle: "Spielanleitung", 6 | help1: `Jeden Tag gibt es ein neues geheimes Land. Dein Ziel ist es, dieses Land 7 | in so wenig Versuchen wie möglich zu erraten. Jeder falsche Versuch wir die auf 8 | dem Globus mit einer Farbe angezeigt, die die Nähe zum gesuchten Land verdeutlicht. 9 | Je wärmer die Farbe, desto näher bist du an der richtigen Lösung.`, 10 | help2: `Als Beispiel, wenn Japan das gesuchte Land ist, dann würden die folgenden 11 | Länder mit diesen Farben angezeigt werden:`, 12 | help3: `Es gibt jeden Tag ein neues geheimes Land!`, 13 | France: "Frankreich", 14 | Nepal: "Nepal", 15 | Mongolia: "Mongolei", 16 | "South Korea": "Süd Korea", 17 | Aux1: `["Klick auf", "Drücke"] den Globus zum Spielen!`, 18 | Aux2: "Hast du eine Frage?", 19 | Aux3: "Schau dir das FAQ an", 20 | Footer1: "von The Abe Train", 21 | Footer2: "Gefällt dir das Spiel?", 22 | Footer3: "Kauf mir einen Kaffee", 23 | Loading: "Laden...", 24 | FAQTitle: "FAQ", 25 | q1: "1. Wie wird die Distanz zwischen der richtigen Lösung und meinem Versuch berechnet?", 26 | a1: "Distanz zwischen Ländern ist der minimale Abstand zwischen ihren Grenzen.", 27 | q2: "2. Wie kann ich das Spiel spielen, wenn ich farbenblind oder anders visuell eingeschränkt bin?", 28 | a2: "Ein kontrastreicher Farbenfehlsichtigkeitsmodus kann in den aktiviert werden.", 29 | q3: "3. Wie entscheidet das Spiel was ein Land ist?", 30 | a3: "Globle nutzt dieses Framework um zu entscheiden, was ein gültiger Versuch ist.", 31 | q4: "4. Sind unsouveräne aber eigenständige Länder mit im Spiel?", 32 | a4: "Manche Gebiete erscheinen in einer neutralen Farbe wenn ihre souveränen Länder geraten wurden, z.B. Grönland bei Dänemark. Der Ort dieser Gebiete beeinflusst nicht die Farbe des souveränen Landes. Die meisten kleinen Gebiete tauchen nicht im Spiel auf, wie z.B. Curaçao.", 33 | q5: "5. Ich habe das heutige geheime Land gefunden! Wann kann ich wieder spielen?", 34 | a5: "Das geheime Land und deine Versuche setzen sich um Mitternacht in deiner Zeitzone zurück.", 35 | q6: "6. Werden alternative Schreibweisen akzeptiert?", 36 | a6: "Viele Länder haben unterschiedliche Namen. Manche alternative und ehemalige Namen werden akzeptiert, z.B. Burma für Myanmar. Zu dem werden Abkürzungen für einige Länder akzeptiert, z.B. UAE für United Arab Emirates.", 37 | q7: "7. Ein Land fehlt oder ist fehlerhaft. Was kann ich tun?", 38 | a7: "Geographie kann ein schwieriges Thema sein, einige Ländergrenzen sind umstritten. Wenn du glaubst, dass eine Änderung notwendig ist, dann öffne bitte höflich ein Issue auf {GitHub} oder schreib mir eine DM auf {Twitter}.", 39 | GameTitle: "Spiel", 40 | Game1: "Gib dein Land hier ein", 41 | Game2: "Eingeben", 42 | Game3: "Gib hier den Namen des Landes ein um zu beginnen.", 43 | Game4: `Zieh, ["klick auf", "drücke"], und zoom in den Globus um dir beim nächsten Versuch zu helfen.`, 44 | Game5: "Ungültiger Versuch", 45 | Game6: "Das Land wurde schon versucht", 46 | Game7: "Das geheime Land ist {answer}!", 47 | Game8: "Nächste Grenze", 48 | StatsTitle: "Statistiken", 49 | Stats1: "Letzter Sieg", 50 | Stats2: "Heutige Versuche", 51 | Stats3: "Gewonnene Spiele", 52 | Stats4: "Erfolgsserie", 53 | Stats5: "Längste Erfolgsserie", 54 | Stats6: "Durchschnittliche Versuche", 55 | Stats7: "Ø. Versuche", 56 | Stats8: "Zurücksetzen", 57 | Stats9: "Teilen", 58 | Stats10: "Bist du dir sicher, dass du deine Statistiken löschen möchtest?", 59 | Stats11: "Statistiken gelöscht.", 60 | Stats12: "In die Zwischenablage kopiert!", 61 | SettingsTitle: "Einstellungen", 62 | Settings1: "Tag Theme", 63 | Settings2: "Nacht Theme", 64 | Settings3: "Farbenfehlsichtigkeitsmodus On", 65 | Settings4: "Farbenfehlsichtigkeitsmodus Off", 66 | Settings5: "Länder", 67 | Settings6: "Städte", 68 | Settings7: "Sprache", 69 | Settings8: "Globle: Städte Edition kommt bald!", 70 | Settings9: "Üben", 71 | Settings10: "Regenbogen aktiviert", 72 | Settings11: "Regenbogen deaktiviert", 73 | Answer: "Answer", //TODO: Translate 74 | Closest: "Closest", //TODO: Translate 75 | Guessed: "Guessed", //TODO: Translate 76 | PracticeMode: "You are in practice mode.", //TODO: Translate 77 | PracticeExit: "Exit practice mode", //TODO: Translate 78 | PracticeNew: "New practice game", //TODO: Translate 79 | SortByGuesses: "Sort by order of guesses", //TODO: Translate 80 | SortByDistance: "Sort by distance", //TODO: Translate 81 | }; 82 | -------------------------------------------------------------------------------- /src/i18n/messages/en-CA.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const English: Messages = { 4 | name: "English", 5 | helpTitle: "How to play", 6 | help1: `Every day, there is a new Mystery Country. Your goal is to guess which country it is using the fewest number of guesses. Each incorrect guess 7 | will appear on the globe with a colour indicating how close it is to the 8 | Mystery Country. The hotter the colour, the closer you are to the answer.`, 9 | help2: `For example, if the Mystery Country is Japan, then the following 10 | countries would appear with these colours if guessed:`, 11 | help3: `A new Mystery Country will be available every day!`, 12 | France: "France", 13 | Nepal: "Nepal", 14 | Mongolia: "Mongolia", 15 | "South Korea": "South Korea", 16 | Aux1: `["Click", "Tap"] the globe to play!`, 17 | Aux2: "Have a question?", 18 | Aux3: "Check out the FAQ", 19 | Footer1: "by The Abe Train", 20 | Footer2: "Enjoying the game?", 21 | Footer3: "Buy me a coffee", 22 | Loading: "Loading...", 23 | FAQTitle: "FAQ", 24 | q1: "1. How is the distance between the answer and my guess calculated?", 25 | a1: "Distance between countries is defined as the minimum distance between their land borders along the Earth's surface. Territories (areas that appear in gray when their parent country is guessed) and water borders are not included in distance calculations.", 26 | q2: "2. How can I play the game if I am colour blind or visually impaired?", 27 | a2: "A high-contrast Colour Blind mode can be activated in .", 28 | q3: "3. How does the game decide what is a valid country?", 29 | a3: "Globle uses this framework to determine what constitutes a valid guess.", 30 | q4: "4. Are autonomous but not sovereign countries in the game?", 31 | a4: "Some territories will appear in a neutral colour when their sovereign country is guessed, e.g. Greenland for Denmark. The location of these territories does not impact the colour of the sovereign country. Most small territories do not appear in the game, e.g. Curaçao.", 32 | q5: "5. I found today's mystery country! When do I get to play again?", 33 | a5: "The mystery country changes and your guesses reset at midnight in your time zone.", 34 | q6: "6. Are alternative spellings for countries acceptable?", 35 | a6: "There are many countries with multiple acceptable names. Some alternate spellings and previous names are accepted, e.g. Burma for Myanmar. As well, acronyms are acceptable for some multi-word countries, e.g. UAE for United Arab Emirates.", 36 | q7: "7. A country is missing or a border is incorrect. What can I do about it?", 37 | a7: "Geography can be a sensitive topic, and some countries' borders are disputed. If you believe a correction should be made, please politely raise an issue on {GitHub} or DM me on {Twitter}.", 38 | q8: "8. Why are my friend and I getting different mystery countries?", 39 | a8: "Sometimes updates to the game don't reach everyone's browsers/devices at the same time. To fix this issue, you can get the latest code by doing a hard refresh (Ctrl + Shift + R on desktop, instructions vary for mobile devices).", 40 | GameTitle: "Game", 41 | Game1: "Enter country name here", 42 | Game2: "Enter", 43 | Game3: "Enter the name of any country to make your first guess.", 44 | Game4: `Drag, ["click", "tap"], and zoom-in on the globe to help you find your next guess.`, 45 | Game5: "Invalid guess", 46 | Game6: "Country already guessed", 47 | Game7: "The Mystery Country is {answer}!", 48 | Game8: "Closest border", 49 | StatsTitle: "Statistics", 50 | Stats1: "Last win", 51 | Stats2: "Today's guesses", 52 | Stats3: "Games won", 53 | Stats4: "Current streak", 54 | Stats5: "Max streak", 55 | Stats6: "Average guesses", 56 | Stats7: "Avg. Guesses", 57 | Stats8: "Reset", 58 | Stats9: "Share", 59 | Stats10: "Are you sure you want to reset your score?", 60 | Stats11: "Stats erased.", 61 | Stats12: "Copied to clipboard!", 62 | SettingsTitle: "Settings", 63 | Settings1: "Day Theme", 64 | Settings2: "Night Theme", 65 | Settings3: "Colour Blind Mode On", 66 | Settings4: "Colour Blind Mode Off", 67 | Settings5: "Countries", 68 | Settings6: "Cities", 69 | Settings7: "Language", 70 | Settings8: "Globle: Cities Edition coming soon!", 71 | Settings9: "Practice", 72 | Settings10: "Rainbow On", 73 | Settings11: "Rainbow Off", 74 | Answer: "Answer", 75 | Closest: "Closest", 76 | Guessed: "Guessed", 77 | PracticeMode: "You are in practice mode.", 78 | PracticeExit: "Exit practice mode", 79 | PracticeNew: "New practice game", 80 | SortByGuesses: "Sort by order of guesses", 81 | SortByDistance: "Sort by distance", 82 | }; 83 | -------------------------------------------------------------------------------- /src/i18n/messages/fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "../../lib/locale"; 2 | 3 | export const French: Messages = { 4 | name: "Français", 5 | helpTitle: "Comment jouer", 6 | help1: `Chaque jour, il y a un nouveau pays mystère. Votre but est de deviner 7 | le pays mystère avec le moins d'essais possible. Chaque tentative apparaîtra sur 8 | le globe avec une couleur indiquant la distance avec le pays mystère. Plus la couleur 9 | est chaude, plus vous êtes proche de la réponse.`, 10 | help2: `Par exemple, si le pays mystère est Japon, les pays suivant apparaitront 11 | avec ces couleurs:`, 12 | help3: `Un nouveau pays mystère sera disponible chaque jour!`, 13 | France: "France", 14 | Nepal: "Népal", 15 | Mongolia: "Mongolie", 16 | "South Korea": "Corée du Sud", 17 | Aux1: `["Cliquez sur", "Touchez"] le globe pour jouer!`, 18 | Aux2: "Vous avez une question?", 19 | Aux3: "Consultez la FAQ", 20 | Footer1: "Auteur: The Abe Train", 21 | Footer2: "Le jeu vous plaît?", 22 | Footer3: "Payez-moi un café.", 23 | Loading: "Chargement...", 24 | FAQTitle: "FAQ", 25 | q1: "1. De quelle manière est calculée la distance entre ma tentative et la réponse?", 26 | a1: "La distance entre les pays est définie comme étant la distance minimale entre leurs frontières.", 27 | q2: "2. Comment puis-je jouer si je suis daltonien ou malvoyant?", 28 | a2: "La section contient un mode de jeu à contraste élevé.", 29 | q3: "3. Qu'est-ce qui détermine la validité d'un pays dans le jeu?", 30 | a3: "Globle s'appuie sur cette convention afin de déterminer ce qui constitue une tentative valide.", 31 | q4: "4. Y a-t-il des pays autonomes mais non souverains dans le jeu?", 32 | a4: "Certains territoires apparaitront avec une couleur neutre lorsque leur pays souverain est deviné, par exemple le Groenland pour le Danemark. L'emplacement de ces territoires n'affecte pas la couleur attribuée au pays souverain. La plupart des petits territoires n'apparaissent pas dans le jeu, par exemple Curaçao.", 33 | q5: "5. J'ai trouvé le pays mystère d'aujourd'hui! Quand vais-je pouvoir rejouer?", 34 | a5: "Le pays mystère change et vos tentatives sont réinitialisées à minuit dans votre fuseau horaire.", 35 | q6: "6. Est-ce que les noms alternatifs de pays sont acceptés?", 36 | a6: "Il y a beaucoup de pays avec plusieurs noms acceptables. Certains noms alternatifs ou anciens sont acceptés, par exemple la Birmanie pour le Myanmar. Aussi, les acronymes sont acceptés pour les pays avec des noms composés de plusieurs mots, par exemple EAU pour les Émirats arabes unis.", 37 | q7: "7. Un pays est manquant ou une frontière est incorrecte. Que puis-je faire?", 38 | a7: "La géographie peut être un sujet sensible, et certaines frontières sont contestées. Si vous pensez qu'une correction s'impose, s'il vous plait veuillez m'en informer en ouvrant un billet (\"issue\") sur {GitHub} ou en m'envoyant un message privé sur {Twitter}.", 39 | GameTitle: "Jeu", 40 | Game1: "Entrez le nom d'un pays ici.", 41 | Game2: "Entrer", 42 | Game3: 43 | "Entrez le nom de n'importe quel pays pour faire votre première tentative.", 44 | Game4: `Vous pouvez faire glisser le globe, ["cliquer", "appuyer"] dessus ou effectuer un zoom pour vous aider à trouver votre prochaine tentative.`, 45 | Game5: "Tentative invalide", 46 | Game6: "Pays déjà tenté", 47 | Game7: "Le pays mystère est: {answer}!", 48 | Game8: "Frontière la plus proche", 49 | StatsTitle: "Statistiques", 50 | Stats1: "Dernière victoire", 51 | Stats2: "Tentatives d'aujourd'hui", 52 | Stats3: "Parties gagnées", 53 | Stats4: "Série actuelle", 54 | Stats5: "Série max", 55 | Stats6: "Moyenne de tentatives", 56 | Stats7: "Moy. tentatives", 57 | Stats8: "Réinitialiser", 58 | Stats9: "Partager", 59 | Stats10: "Voulez-vous vraiment réinitialiser vos statistiques?", 60 | Stats11: "Statistiques réinitialisées.", 61 | Stats12: "Copié dans le presse-papier!", 62 | SettingsTitle: "Paramètres", 63 | Settings1: "Thème diurne", 64 | Settings2: "Thème nocturne", 65 | Settings3: "Mode contraste élevé activé", 66 | Settings4: "Mode contraste élevé désactivé", 67 | Settings5: "Pays", 68 | Settings6: "Villes", 69 | Settings7: "Langue", 70 | Settings8: "Globle: Édition Ville arrive bientôt!", 71 | Settings9: "S'entraîner", 72 | Settings10: "Mode arc-en-ciel activé", 73 | Settings11: "Mode arc-en-ciel désactivé", 74 | Answer: "Réponse", 75 | Closest: "Selon la distance", 76 | Guessed: "Selon les tentatives", 77 | PracticeMode: "Vous êtes dans le mode entraînement.", 78 | PracticeExit: "Quitter le mode entraînement", 79 | PracticeNew: "Nouvelle partie d'entraînement", 80 | SortByGuesses: "Trier par ordre des tentatives", 81 | SortByDistance: "Trier par distance", 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/Guesser.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, useContext, useState, useRef, useEffect } from "react"; 2 | import { Country } from "../lib/country"; 3 | import { answerCountry, answerName } from "../util/answer"; 4 | import { Message } from "./Message"; 5 | import { polygonDistance } from "../util/distance"; 6 | // import alternateNames from "../data/alternate_names.json"; 7 | import { LocaleContext } from "../i18n/LocaleContext"; 8 | import localeList from "../i18n/messages"; 9 | import { FormattedMessage } from "react-intl"; 10 | import { langNameMap } from "../i18n/locales"; 11 | import { AltNames } from "../lib/alternateNames"; 12 | const countryData: Country[] = require("../data/country_data.json").features; 13 | const alternateNames: AltNames = require("../data/alternate_names.json"); 14 | 15 | type Props = { 16 | guesses: Country[]; 17 | setGuesses: React.Dispatch>; 18 | win: boolean; 19 | setWin: React.Dispatch>; 20 | practiceMode: boolean; 21 | }; 22 | 23 | export default function Guesser({ 24 | guesses, 25 | setGuesses, 26 | win, 27 | setWin, 28 | practiceMode, 29 | }: Props) { 30 | const [guessName, setGuessName] = useState(""); 31 | const [error, setError] = useState(""); 32 | const { locale } = useContext(LocaleContext); 33 | 34 | const langName = langNameMap[locale]; 35 | 36 | const ref = useRef(null); 37 | useEffect(() => { 38 | ref.current?.focus(); 39 | }, [ref]); 40 | 41 | function findCountry(countryName: string, list: Country[]) { 42 | return list.find((country) => { 43 | const { NAME, NAME_LONG, ABBREV, ADMIN, BRK_NAME, NAME_SORT } = 44 | country.properties; 45 | 46 | return ( 47 | NAME.toLowerCase() === countryName || 48 | NAME_LONG.toLowerCase() === countryName || 49 | ADMIN.toLowerCase() === countryName || 50 | ABBREV.toLowerCase() === countryName || 51 | ABBREV.replace(/\./g, "").toLowerCase() === countryName || 52 | NAME.replace(/-/g, " ").toLowerCase() === countryName || 53 | BRK_NAME.toLowerCase() === countryName || 54 | NAME_SORT.toLowerCase() === countryName || 55 | country.properties[langName].toLowerCase() === countryName 56 | ); 57 | }); 58 | } 59 | 60 | // Check territories function 61 | function runChecks() { 62 | const trimmedName = guessName 63 | .trim() 64 | .toLowerCase() 65 | .replace(/&/g, "and") 66 | .replace(/^st\s/g, "st. "); 67 | 68 | const oldNamePair = alternateNames[locale].find((pair) => { 69 | return pair.alternative === trimmedName; 70 | }); 71 | const userGuess = oldNamePair ? oldNamePair.real : trimmedName; 72 | const alreadyGuessed = findCountry(userGuess, guesses); 73 | if (alreadyGuessed) { 74 | setError(localeList[locale]["Game6"]); 75 | ref.current?.select(); 76 | return; 77 | } 78 | const guessCountry = findCountry(userGuess, countryData); 79 | if (!guessCountry) { 80 | setError(localeList[locale]["Game5"]); 81 | ref.current?.select(); 82 | return; 83 | } 84 | if (practiceMode) { 85 | const answerCountry = JSON.parse( 86 | localStorage.getItem("practice") as string 87 | ) as Country; 88 | const answerName = answerCountry.properties.NAME; 89 | if (guessCountry.properties.NAME === answerName) { 90 | setWin(true); 91 | } 92 | } else if (guessCountry.properties.NAME === answerName) { 93 | setWin(true); 94 | } 95 | return guessCountry; 96 | } 97 | 98 | function addGuess(e: FormEvent) { 99 | e.preventDefault(); 100 | setError(""); 101 | let guessCountry = runChecks(); 102 | if (practiceMode) { 103 | const answerCountry = JSON.parse( 104 | localStorage.getItem("practice") as string 105 | ); 106 | if (guessCountry && answerCountry) { 107 | guessCountry["proximity"] = polygonDistance( 108 | guessCountry, 109 | answerCountry 110 | ); 111 | setGuesses([...guesses, guessCountry]); 112 | setGuessName(""); 113 | return; 114 | } 115 | } 116 | if (guessCountry && answerCountry) { 117 | guessCountry["proximity"] = polygonDistance(guessCountry, answerCountry); 118 | setGuesses([...guesses, guessCountry]); 119 | setGuessName(""); 120 | } 121 | } 122 | 123 | return ( 124 |
125 |
129 | setGuessName(e.currentTarget.value)} 141 | ref={ref} 142 | disabled={win} 143 | placeholder={guesses.length === 0 ? localeList[locale]["Game1"] : ""} 144 | autoComplete="new-password" 145 | /> 146 | 155 |
156 | 162 |
163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent, useContext, useEffect, useState } from "react"; 2 | import { GlobeMethods } from "react-globe.gl"; 3 | import { FormattedMessage } from "react-intl"; 4 | import { LocaleContext } from "../i18n/LocaleContext"; 5 | import { Country, LanguageName } from "../lib/country"; 6 | import { Locale } from "../lib/locale"; 7 | import { answerName } from "../util/answer"; 8 | import { findCentre, turnGlobe } from "../util/globe"; 9 | import Toggle from "./Toggle"; 10 | 11 | type Props = { 12 | guesses: Country[]; 13 | win: boolean; 14 | globeRef: React.MutableRefObject; 15 | practiceMode: boolean; 16 | }; 17 | 18 | function reorderGuesses(guessList: Country[], practiceMode: boolean) { 19 | return [...guessList].sort((a, b) => { 20 | // practice 21 | if (practiceMode) { 22 | const answerCountry = JSON.parse( 23 | localStorage.getItem("practice") as string 24 | ) as Country; 25 | const answerName = answerCountry.properties.NAME; 26 | if (a.properties.NAME === answerName) { 27 | return -1; 28 | } else if (b.properties.NAME === answerName) { 29 | return 1; 30 | } else { 31 | return a.proximity - b.proximity; 32 | } 33 | } 34 | 35 | // daily 36 | if (a.properties.NAME === answerName) { 37 | return -1; 38 | } else if (b.properties.NAME === answerName) { 39 | return 1; 40 | } else { 41 | return a.proximity - b.proximity; 42 | } 43 | }); 44 | } 45 | 46 | export default function List({ guesses, win, globeRef, practiceMode }: Props) { 47 | const [orderedGuesses, setOrderedGuesses] = useState( 48 | reorderGuesses(guesses, practiceMode) 49 | ); 50 | const [miles, setMiles] = useState(false); 51 | const { locale } = useContext(LocaleContext); 52 | const langNameMap: Record = { 53 | "pt-BR": "NAME_PT", 54 | "es-MX": "NAME_ES", 55 | "en-CA": "NAME_EN", 56 | "fr-FR": "NAME_FR", 57 | "de-DE": "NAME_DE", 58 | "hu-HU": "NAME_HU", 59 | "pl-PL": "NAME_PL", 60 | "it-IT": "NAME_IT", 61 | "sv-SE": "NAME_SV", 62 | }; 63 | const langName = langNameMap[locale]; 64 | 65 | useEffect(() => { 66 | setOrderedGuesses(reorderGuesses(guesses, practiceMode)); 67 | }, [guesses, practiceMode]); 68 | 69 | function formatKm(m: number, miles: boolean) { 70 | const METERS_PER_MILE = 1609.34; 71 | const BIN = 10; 72 | const value = miles ? m / METERS_PER_MILE : m / 1000; 73 | if (value < BIN) return "< " + BIN; 74 | 75 | const rounded = Math.round(value / BIN) * BIN; 76 | // const max = min + BIN; 77 | const format = (num: number) => 78 | num.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ","); 79 | 80 | return `~ ${format(rounded)}`; 81 | } 82 | 83 | const qualifier = win ? "Answer" : "Closest"; 84 | 85 | function turnToCountry(e: SyntheticEvent, idx: number) { 86 | const clickedCountry = isSortedByDistance 87 | ? orderedGuesses[idx] 88 | : guesses[idx]; 89 | const { lat, lng, altitude } = findCentre(clickedCountry); 90 | turnGlobe({ lat, lng, altitude }, globeRef, "zoom"); 91 | } 92 | 93 | const closest = orderedGuesses[0]; 94 | const farthest = orderedGuesses[orderedGuesses.length - 1]; 95 | 96 | const [isSortedByDistance, setIsSortedByDistance] = useState(true); 97 | const guessesToDisplay = isSortedByDistance ? orderedGuesses : guesses; 98 | 99 | return ( 100 |
101 | {orderedGuesses.length > 0 && ( 102 |

103 | {isSortedByDistance ? ( 104 | 105 | 106 | 107 | ) : ( 108 | 109 | 110 | 111 | )} 112 |

113 | )} 114 |
    115 | {guessesToDisplay.map((guess, idx) => { 116 | const { NAME_LEN, ABBREV, NAME, FLAG } = guess.properties; 117 | const flag = (FLAG || "").toLocaleLowerCase(); 118 | let name = NAME_LEN >= 10 ? ABBREV : NAME; 119 | if (locale !== "en-CA") { 120 | name = guess.properties[langName]; 121 | } 122 | 123 | return ( 124 |
  • 125 | 136 |
  • 137 | ); 138 | })} 139 |
140 | {closest && farthest && ( 141 |
142 |
143 |

144 | :{" "} 145 | {formatKm(closest?.proximity, miles)} 146 |

147 | 154 |
155 |

156 | 166 |

167 |
168 | )} 169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /src/pages/Game.tsx: -------------------------------------------------------------------------------- 1 | import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react"; 2 | import { GlobeMethods } from "react-globe.gl"; 3 | import { Country } from "../lib/country"; 4 | import { answerCountry, answerName } from "../util/answer"; 5 | import { useLocalStorage } from "../hooks/useLocalStorage"; 6 | import { Guesses, Stats } from "../lib/localStorage"; 7 | import { dateDiffInDays, today } from "../util/dates"; 8 | import { polygonDistance } from "../util/distance"; 9 | import { getColourEmoji } from "../util/colour"; 10 | import { useNavigate, useSearchParams } from "react-router-dom"; 11 | import { FormattedMessage } from "react-intl"; 12 | 13 | const Globe = lazy(() => import("../components/Globe")); 14 | const Guesser = lazy(() => import("../components/Guesser")); 15 | const List = lazy(() => import("../components/List")); 16 | const countryData: Country[] = require("../data/country_data.json").features; 17 | 18 | type Props = { 19 | reSpin: boolean; 20 | setShowStats: React.Dispatch>; 21 | }; 22 | 23 | export default function Game({ reSpin, setShowStats }: Props) { 24 | // Get data from local storage 25 | const [storedGuesses, storeGuesses] = useLocalStorage("guesses", { 26 | day: today, 27 | countries: [], 28 | }); 29 | 30 | const firstStats = { 31 | gamesWon: 0, 32 | lastWin: new Date(0).toLocaleDateString("en-CA"), 33 | currentStreak: 0, 34 | maxStreak: 0, 35 | usedGuesses: [], 36 | emojiGuesses: "", 37 | }; 38 | const [storedStats, storeStats] = useLocalStorage( 39 | "statistics", 40 | firstStats 41 | ); 42 | 43 | // Set up practice mode 44 | const [params] = useSearchParams(); 45 | const navigate = useNavigate(); 46 | const practiceMode = !!params.get("practice_mode"); 47 | 48 | function enterPracticeMode() { 49 | const practiceAnswer = 50 | countryData[Math.floor(Math.random() * countryData.length)]; 51 | localStorage.setItem("practice", JSON.stringify(practiceAnswer)); 52 | navigate("/game?practice_mode=true"); 53 | setGuesses([]); 54 | setWin(false); 55 | } 56 | 57 | const storedCountries = useMemo(() => { 58 | if (today <= storedGuesses.day && !practiceMode) { 59 | const names = storedGuesses.countries; 60 | return names.map((guess) => { 61 | const foundCountry = countryData.find((country) => { 62 | return country.properties.NAME === guess; 63 | }); 64 | if (!foundCountry) throw new Error("Country mapping broken"); 65 | foundCountry["proximity"] = polygonDistance( 66 | foundCountry, 67 | answerCountry 68 | ); 69 | return foundCountry; 70 | }); 71 | } 72 | return []; 73 | // eslint-disable-next-line 74 | }, [practiceMode]); 75 | 76 | // Check if win condition already met 77 | const alreadyWon = practiceMode 78 | ? false 79 | : storedCountries?.map((c) => c.properties.NAME).includes(answerName); 80 | 81 | // Now we're ready to start the game! Set up the game states with the data we 82 | // already know from the stored info. 83 | const [guesses, setGuesses] = useState( 84 | practiceMode ? [] : storedCountries 85 | ); 86 | const [win, setWin] = useState(alreadyWon); 87 | const globeRef = useRef(null!); 88 | 89 | // Whenever there's a new guess 90 | useEffect(() => { 91 | if (!practiceMode) { 92 | const guessNames = guesses.map((country) => country.properties.NAME); 93 | storeGuesses({ 94 | day: today, 95 | countries: guessNames, 96 | }); 97 | } 98 | }, [guesses, storeGuesses, practiceMode]); 99 | 100 | // When the player wins! 101 | useEffect(() => { 102 | if (win && storedStats.lastWin !== today && !practiceMode) { 103 | // Store new stats in local storage 104 | const lastWin = today; 105 | const gamesWon = storedStats.gamesWon + 1; 106 | const streakBroken = dateDiffInDays(storedStats.lastWin, lastWin) > 1; 107 | const currentStreak = streakBroken ? 1 : storedStats.currentStreak + 1; 108 | const maxStreak = 109 | currentStreak > storedStats.maxStreak 110 | ? currentStreak 111 | : storedStats.maxStreak; 112 | const usedGuesses = [...storedStats.usedGuesses, guesses.length]; 113 | const chunks = []; 114 | for (let i = 0; i < guesses.length; i += 8) { 115 | chunks.push(guesses.slice(i, i + 8)); 116 | } 117 | const emojiGuesses = chunks 118 | .map((each) => 119 | each 120 | .map((guess) => getColourEmoji(guess, guesses[guesses.length - 1])) 121 | .join("") 122 | ) 123 | .join("\n"); 124 | const newStats = { 125 | lastWin, 126 | gamesWon, 127 | currentStreak, 128 | maxStreak, 129 | usedGuesses, 130 | emojiGuesses, 131 | }; 132 | storeStats(newStats); 133 | 134 | // Show stats 135 | setTimeout(() => setShowStats(true), 3000); 136 | } 137 | }, [win, guesses, setShowStats, storeStats, storedStats, practiceMode]); 138 | 139 | // Practice mode 140 | 141 | // Fallback while loading 142 | const renderLoader = () => ( 143 |

144 | 145 |

146 | ); 147 | 148 | return ( 149 | 150 | 157 | {!reSpin && ( 158 |
159 | 164 | 170 | {practiceMode && ( 171 |
172 | 173 | 174 | 175 | 185 | 194 |
195 | )} 196 |
197 | )} 198 |
199 | ); 200 | } 201 | -------------------------------------------------------------------------------- /src/components/Globe.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from "react"; 2 | import ReactGlobe, { GlobeMethods } from "react-globe.gl"; 3 | import { Country } from "../lib/country"; 4 | import { answerCountry } from "../util/answer"; 5 | import { findCentre, globeImg, turnGlobe } from "../util/globe"; 6 | import { ThemeContext } from "../context/ThemeContext"; 7 | import { getColour } from "../util/colour"; 8 | import { isMobile } from "react-device-detect"; 9 | const territoryData: Country[] = require("../data/territories.json").features; 10 | 11 | type Props = { 12 | guesses: Country[]; 13 | globeRef: React.MutableRefObject; 14 | practiceMode: boolean; 15 | }; 16 | 17 | const ZOOM_SPEED = 1; 18 | 19 | export default function Globe({ guesses, globeRef, practiceMode }: Props) { 20 | // State 21 | const [places, setPlaces] = useState(guesses); 22 | 23 | // Theme 24 | const { nightMode, prideMode, highContrast } = useContext(ThemeContext).theme; 25 | 26 | // Globe size settings 27 | const size = isMobile ? 320 : 600; // px on one side 28 | const extraStyle = { 29 | width: `${size}px`, 30 | clipPath: `circle(${size / 2}px at ${size / 2}px ${size / 2}px)`, 31 | }; 32 | 33 | // On first render 34 | useEffect(() => { 35 | const controls: any = globeRef.current.controls(); 36 | controls.autoRotate = true; 37 | controls.autoRotateSpeed = 1; 38 | setTimeout(() => { 39 | globeRef.current.pointOfView({ lat: 0, lng: 0, altitude: 1.5 }); 40 | }, 400); 41 | }, [globeRef]); 42 | 43 | // After each guess 44 | useEffect(() => { 45 | // Add territories to guesses to make shapes 46 | const territories: Country[] = []; 47 | guesses.forEach((guess) => { 48 | const foundTerritories = territoryData.filter((territory) => { 49 | return guess.properties.NAME === territory.properties.SOVEREIGNT; 50 | }); 51 | if (foundTerritories) territories.push(...foundTerritories); 52 | }); 53 | setPlaces(guesses.concat(territories)); 54 | 55 | // Turn globe to new spot 56 | const newGuess = [...guesses].pop(); 57 | if (newGuess) { 58 | const controls: any = globeRef.current.controls(); 59 | controls.autoRotate = false; 60 | const newSpot = findCentre(newGuess); 61 | turnGlobe(newSpot, globeRef, "zoom"); 62 | } 63 | }, [guesses, globeRef]); 64 | 65 | // Stop rotate on drag 66 | const containerRef = useRef(null!); 67 | useEffect(() => { 68 | const controls: any = globeRef.current.controls(); 69 | containerRef.current.addEventListener("mouseup", () => { 70 | controls.autoRotate = false; 71 | }); 72 | containerRef.current.addEventListener("touchend", () => { 73 | controls.autoRotate = false; 74 | }); 75 | }, [globeRef]); 76 | 77 | // Polygon colour 78 | function polygonColour(country: Country) { 79 | if (practiceMode) { 80 | const answerCountry = JSON.parse( 81 | localStorage.getItem("practice") as string 82 | ); 83 | return getColour( 84 | country, 85 | answerCountry, 86 | nightMode, 87 | highContrast, 88 | prideMode 89 | ); 90 | } 91 | return getColour( 92 | country, 93 | answerCountry, 94 | nightMode, 95 | highContrast, 96 | prideMode 97 | ); 98 | } 99 | 100 | // Label colour 101 | function getLabel(country: Country) { 102 | const name = country.properties.ADMIN; 103 | const prox = country.proximity; 104 | const dayColour = prox < 750_000 ? "gray-300" : "gray-900"; 105 | const nightColour = "gray-300"; 106 | const label = `${name}`; 107 | return label; 108 | } 109 | 110 | // Polygon altitude 111 | function getAltitude(country: Country) { 112 | if (!highContrast || country.properties.TYPE === "Territory") return 0.01; 113 | const prox = country.proximity; 114 | let proxFraction = prox / 2_000_000; 115 | proxFraction = Math.min(Math.max(proxFraction, 0.01), 0.95); 116 | let alt = (1 - proxFraction) / 10; 117 | return alt; 118 | } 119 | 120 | // Clicking the zoom buttons on mobile 121 | function zoom(z: number) { 122 | const controls: any = globeRef.current.controls(); 123 | controls.autoRotate = false; 124 | const coords = globeRef.current.pointOfView(); 125 | const { altitude } = globeRef.current.pointOfView(); 126 | coords["altitude"] = Math.max(altitude + z, 0.05); 127 | globeRef.current.pointOfView(coords, 250); 128 | } 129 | 130 | // Called when the globe position changes 131 | function globeOnZoom() { 132 | overrideGlobeZooming(); 133 | } 134 | 135 | // Override the zoomSpeed mutation in globe.gl by calling this in the globe's 136 | // onZoom callback. 137 | // 138 | // By the time this callback is called, an onchange event handler on 139 | // `controls` defined in globe.gl's `globe.js` source file will have changed 140 | // the zoomSpeed based on altitude. We will counteract that to get back the 141 | // nice zooming implemented in the three.js library (`OrbitControls.js`). 142 | function overrideGlobeZooming() { 143 | const controls: any = globeRef.current?.controls(); 144 | if (controls != null) controls.zoomSpeed = ZOOM_SPEED; 145 | } 146 | 147 | const btnFill = nightMode ? "bg-[#582679]" : "bg-[#F3BC63]"; 148 | const btnBorder = nightMode ? "border-[#350a46]" : "border-[#FF8E57]"; 149 | const btnText = nightMode ? "text-white font-bold" : ""; 150 | 151 | return ( 152 |
153 |
158 | turnGlobe(d, globeRef)} 175 | onPolygonClick={(p, e, c) => turnGlobe(c, globeRef)} 176 | polygonStrokeColor="#00000000" 177 | atmosphereColor={nightMode ? "rgba(63, 201, 255)" : "lightskyblue"} 178 | onZoom={globeOnZoom} 179 | /> 180 |
181 | {isMobile && ( 182 |
183 | 189 | 195 |
196 | )} 197 |
198 | ); 199 | } 200 | -------------------------------------------------------------------------------- /src/components/Statistics.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from "react"; 2 | import { useLocalStorage } from "../hooks/useLocalStorage"; 3 | import { Stats } from "../lib/localStorage"; 4 | import { isMobile } from "react-device-detect"; 5 | import { getPath } from "../util/svg"; 6 | import { today } from "../util/dates"; 7 | import { isFirefox } from "react-device-detect"; 8 | import { FormattedMessage } from "react-intl"; 9 | import { LocaleContext } from "../i18n/LocaleContext"; 10 | import localeList from "../i18n/messages"; 11 | import Fade from "../transitions/Fade"; 12 | 13 | type Props = { 14 | setShowStats: React.Dispatch>; 15 | }; 16 | 17 | export default function Statistics({ setShowStats }: Props) { 18 | const localeContext = useContext(LocaleContext); 19 | const { locale } = localeContext; 20 | 21 | // Stats data 22 | const firstStats = { 23 | gamesWon: 0, 24 | lastWin: new Date(0).toLocaleDateString("en-CA"), 25 | currentStreak: 0, 26 | maxStreak: 0, 27 | usedGuesses: [], 28 | emojiGuesses: "", 29 | }; 30 | 31 | const [storedStats, storeStats] = useLocalStorage( 32 | "statistics", 33 | firstStats 34 | ); 35 | const { 36 | gamesWon, 37 | lastWin, 38 | currentStreak, 39 | maxStreak, 40 | usedGuesses, 41 | emojiGuesses, 42 | } = storedStats; 43 | 44 | const sumGuesses = usedGuesses.reduce((a, b) => a + b, 0); 45 | const avgGuesses = Math.round((sumGuesses / usedGuesses.length) * 100) / 100; 46 | const showAvgGuesses = usedGuesses.length === 0 ? "--" : avgGuesses; 47 | const todaysGuesses = 48 | lastWin === today ? usedGuesses[usedGuesses.length - 1] : "--"; 49 | 50 | const showLastWin = lastWin >= "2022-01-01" ? lastWin : "--"; 51 | 52 | const avgShorthand = isMobile 53 | ? localeList[locale]["Stats7"] 54 | : localeList[locale]["Stats6"]; 55 | 56 | const statsTable = [ 57 | { label: localeList[locale]["Stats1"], value: showLastWin }, 58 | { label: localeList[locale]["Stats2"], value: todaysGuesses }, 59 | { label: localeList[locale]["Stats3"], value: gamesWon }, 60 | { label: localeList[locale]["Stats4"], value: currentStreak }, 61 | { label: localeList[locale]["Stats5"], value: maxStreak }, 62 | { label: avgShorthand, value: showAvgGuesses }, 63 | ]; 64 | 65 | // Closing the modal 66 | const modalRef = useRef(null!); 67 | 68 | useEffect(() => { 69 | function closeModal(e: MouseEvent) { 70 | const target = e.target as HTMLElement; 71 | if (!modalRef.current?.contains(target)) { 72 | setShowStats(false); 73 | } 74 | } 75 | document.addEventListener("click", closeModal); 76 | return () => { 77 | document.removeEventListener("click", closeModal); 78 | }; 79 | }, [setShowStats]); 80 | 81 | // Reset stats 82 | const [msg, setMsg] = useState(""); 83 | const [showResetMsg, setShowResetMsg] = useState(false); 84 | const [resetComplete, setResetComplete] = useState(false); 85 | // const [question, setQuestion] = useState(false); 86 | function promptReset() { 87 | setMsg(localeList[locale]["Stats10"]); 88 | // setQuestion(true); 89 | setResetComplete(false); 90 | setShowResetMsg(true); 91 | } 92 | function resetStats() { 93 | storeStats(firstStats); 94 | setShowResetMsg(false); 95 | setTimeout(() => { 96 | setMsg(localeList[locale]["Stats11"]); 97 | setShowCopyMsg(true); 98 | }, 200); 99 | setTimeout(() => setShowCopyMsg(false), 2200); 100 | } 101 | 102 | // Clipboard 103 | const [showCopyMsg, setShowCopyMsg] = useState(false); 104 | const options = { year: "numeric", month: "short", day: "numeric" }; 105 | const event = new Date(); 106 | // @ts-ignore 107 | const unambiguousDate = event.toLocaleDateString(locale, options); 108 | const date = unambiguousDate === "Invalid Date" ? today : unambiguousDate; 109 | async function copyToClipboard() { 110 | const shareString = `🌎 ${date} 🌍 111 | 🔥 ${currentStreak} | ${localeList[locale]["Stats7"]}: ${showAvgGuesses} 112 | ${lastWin === today ? emojiGuesses : "--"} = ${todaysGuesses} 113 | 114 | #globle`; 115 | 116 | try { 117 | if ("canShare" in navigator && isMobile && !isFirefox) { 118 | await navigator.share({ title: "Plurality Stats", text: shareString }); 119 | setMsg("Shared!"); 120 | setShowCopyMsg(true); 121 | return setTimeout(() => setShowCopyMsg(false), 2000); 122 | } else if (navigator.clipboard && window.isSecureContext) { 123 | await navigator.clipboard.writeText(shareString); 124 | setMsg("Copied!"); 125 | setShowCopyMsg(true); 126 | return setTimeout(() => setShowCopyMsg(false), 2000); 127 | } else { 128 | document.execCommand("copy", true, shareString); 129 | setMsg("Copied!"); 130 | setShowCopyMsg(true); 131 | return setTimeout(() => setShowCopyMsg(false), 2000); 132 | } 133 | } catch (e) { 134 | setMsg("This browser cannot share"); 135 | setShowCopyMsg(true); 136 | return setTimeout(() => setShowCopyMsg(false), 2000); 137 | } 138 | } 139 | 140 | return ( 141 |
142 | 156 |

160 | 161 |

162 | 167 | 168 | {statsTable.map((row, idx) => { 169 | return ( 170 | 171 | 177 | 183 | 184 | ); 185 | })} 186 | 187 |
175 | {row.label} 176 | 181 | {row.value} 182 |
188 |
189 | 197 | 206 |
207 |
208 |

212 | New game from the creator of Globle! 213 |

214 | 250 |
251 | 257 |

{msg}

258 |
259 | 267 | 275 |
276 |
277 | 283 |

{msg}

284 |
285 |
286 | ); 287 | } 288 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | snack-media.com, SNM_2902, DIRECT 2 | google.com, pub-5372661266361105, RESELLER, f08c47fec0942fa0 3 | # 4 | rubiconproject.com, 19080, DIRECT, 0bfd66d529a55807 5 | # 6 | indexexchange.com, 189011, DIRECT, 50b1c356f2c5c8fc 7 | # 8 | openx.com, 540310766, DIRECT, 6a698e2ec38604c6 9 | # 10 | sonobi.com, 71b80deecc, DIRECT, d1a215d9eb5aee9e 11 | # 12 | pubmatic.com, 158423, DIRECT, 5d62403b186f2ace 13 | rhythmone.com, 1059622079, RESELLER, a670c89d4a324e47 14 | contextweb.com, 560606, RESELLER, 89ff185a4c4e857c 15 | # 16 | improvedigital.com, 1576, RESELLER 17 | # 18 | sovrn.com, 51991, RESELLER, fafdf38b16bf6b2b 19 | lijit.com, 51991, RESELLER, fafdf38b16bf6b2b 20 | appnexus.com, 1360, RESELLER, f5ab79cb980f11d1 21 | gumgum.com, 11645, RESELLER, ffdef49475d318a9 22 | openx.com, 538959099, RESELLER, 6a698e2ec38604c6 23 | openx.com, 539924617, RESELLER, 6a698e2ec38604c6 24 | pubmatic.com, 137711, RESELLER, 5d62403b186f2ace 25 | pubmatic.com, 156212, RESELLER, 5d62403b186f2ace 26 | pubmatic.com, 156700, RESELLER, 5d62403b186f2ace 27 | rubiconproject.com, 17960, RESELLER, 0bfd66d529a55807 28 | # 29 | rhythmone.com, 3116314521, DIRECT, a670c89d4a324e47 30 | video.unrulymedia.com, 3116314521, DIRECT 31 | # 32 | smartadserver.com, 3786, DIRECT 33 | contextweb.com, 560288, RESELLER, 89ff185a4c4e857c 34 | pubmatic.com, 156439, RESELLER, 5d62403b186f2ace 35 | pubmatic.com, 154037, RESELLER, 5d62403b186f2ace 36 | rubiconproject.com, 16114, RESELLER, 0bfd66d529a55807 37 | openx.com, 537149888, RESELLER, 6a698e2ec38604c6 38 | appnexus.com, 3703, RESELLER, f5ab79cb980f11d1 39 | districtm.io, 101760, RESELLER, 3fd707be9c4527c3 40 | loopme.com, 5679, RESELLER, 6c8d5f95897a5a3b 41 | xad.com, 958, RESELLER, 81cbf0a75a5e0e9a 42 | rhythmone.com, 2564526802, RESELLER, a670c89d4a324e47 43 | smaato.com, 1100044045, RESELLER, 07bcf65f187117b4 44 | pubnative.net, 1006576, RESELLER, d641df8625486a7b 45 | adyoulike.com, b4bf4fdd9b0b915f746f6747ff432bde, RESELLER, 4ad745ead2958bf7 46 | axonix.com, 57264, RESELLER 47 | admanmedia.com, 43, RESELLER 48 | # 49 | appnexus.com, 7582, DIRECT, f5ab79cb980f11d1 50 | # 51 | rubiconproject.com, 11106, DIRECT, 0bfd66d529a55807 52 | # 53 | indexexchange.com, 183188, DIRECT, 50b1c356f2c5c8fc 54 | indexexchange.com, 194995, DIRECT, 50b1c356f2c5c8fc 55 | # 56 | openx.com, 539874140, DIRECT, 6a698e2ec38604c6 57 | # 58 | pubmatic.com,160628, DIRECT, 5d62403b186f2ace 59 | # 60 | triplelift.com, 7613, DIRECT, 6c33edb13117fd86 61 | # 62 | improvedigital.com, 1700, RESELLER 63 | # 64 | teads.tv, 13582, DIRECT, 15a9c44f6d26cbe1 65 | # 66 | amxrtb.com, 105199546, DIRECT 67 | appnexus.com, 12290, RESELLER, f5ab79cb980f11d1 68 | appnexus.com, 11786, RESELLER, f5ab79cb980f11d1 69 | indexexchange.com, 191503, RESELLER, 50b1c356f2c5c8fc 70 | lijit.com, 260380, RESELLER, fafdf38b16bf6b2b 71 | sovrn.com, 260380, RESELLER, fafdf38b16bf6b2b 72 | pubmatic.com, 158355, RESELLER, 5d62403b186f2ace 73 | pubmatic.com, 161527, RESELLER, 5d62403b186f2ace 74 | appnexus.com, 9393, RESELLER, f5ab79cb980f11d1 #Video #Display 75 | appnexus.com, 11924, RESELLER, f5ab79cb980f11d1 76 | adform.com, 2865, RESELLER 77 | yahoo.com, 49648, RESELLER 78 | # 79 | sublime.xyz, 850, DIRECT 80 | smartadserver.com, 1827, RESELLER 81 | improvedigital.com, 335, RESELLER 82 | improvedigital.com, 1220, RESELLER 83 | appnexus.com, 3538, RESELLER, f5ab79cb980f11d1 84 | appnexus.com, 3539, RESELLER, f5ab79cb980f11d1 85 | appnexus.com, 3540, RESELLER, f5ab79cb980f11d1 86 | appnexus.com, 2579, RESELLER, f5ab79cb980f11d1 87 | freewheel.tv, 850, RESELLER 88 | criteo.com, B-062032, RESELLER, 9fac4a4a87c2a44f 89 | criteo.com, B-062033, RESELLER, 9fac4a4a87c2a44f 90 | criteo.com, B-062035, RESELLER, 9fac4a4a87c2a44f 91 | criteo.com, B-062031, RESELLER, 9fac4a4a87c2a44f 92 | themediagrid.com, PIFK3H, RESELLER, 35d5010d7789b49d 93 | themediagrid.com, JALYWI, RESELLER, 35d5010d7789b49d 94 | themediagrid.com, BQ9PNM, RESELLER, 35d5010d7789b49d 95 | themediagrid.com, OWYXGT, RESELLER, 35d5010d7789b49d 96 | quantum-advertising.com, 6228, RESELLER 97 | # 98 | appnexus.com, 7118, RESELLER, f5ab79cb980f11d1 99 | spotx.tv, 108933, RESELLER, 7842df1d2fe2db34 100 | spotxchange.com, 108933, RESELLER, 7842df1d2fe2db34 101 | improvedigital.com, 185, RESELLER 102 | adform.com, 183, RESELLER 103 | freewheel.tv, 33081, RESELLER 104 | freewheel.tv, 33601, RESELLER 105 | google.com, pub-8172268348509349, RESELLER, f08c47fec0942fa0 106 | indexexchange.com, 189872, RESELLER, 50b1c356f2c5c8fc 107 | openx.com, 539519545, RESELLER, 6a698e2ec38604c6 108 | rhythmone.com, 4116102010, RESELLER, a670c89d4a324e47 109 | video.unrulymedia.com, 4116102010, RESELLER 110 | # 111 | revcontent.com, 1223, DIRECT 112 | appnexus.com, 7666, RESELLER, f5ab79cb980f11d1 113 | my6sense.com, 9732, RESELLER 114 | engagebdr.com, 10304, RESELLER 115 | synacor.com, 82291, RESELLER, e108f11b2cdf7d5b 116 | indexexchange.com, 192143, RESELLER, 50b1c356f2c5c8fc 117 | # 118 | gumgum.com, 13135, DIRECT, ffdef49475d318a9 119 | gumgum.com, 15434, RESELLER, ffdef49475d318a9 120 | appnexus.com, 2758, RESELLER, f5ab79cb980f11d1 121 | openx.com, 537149485, RESELLER, 6a698e2ec38604c6 122 | 33across.com, 0013300001r0t9mAAA, RESELLER, bbea06d9c4d2853c 123 | pubmatic.com, 157897, RESELLER, 5d62403b186f2ace 124 | emxgdt.com, 1284, RESELLER, 1e1d41537f7cad7f 125 | improvedigital.com, 1884, RESELLER 126 | rubiconproject.com, 23434, RESELLER, 0bfd66d529a55807 127 | smartadserver.com, 4005, RESELLER 128 | justpremium.com, 936, RESELLER 129 | # 130 | advertising.com, 23598, RESELLER 131 | advertising.com, 28918, RESELLER 132 | appnexus.com, 10062, RESELLER, f5ab79cb980f11d1 133 | emxdgt.com, 1745, RESELLER, 1e1d41537f7cad7f 134 | indexexchange.com, 192106, RESELLER, 50b1c356f2c5c8fc 135 | indexexchange.com, 193375, RESELLER, 50b1c356f2c5c8fc 136 | pubmatic.com, 159970, RESELLER, 5d62403b186f2ace 137 | pubmatic.com, 160454, RESELLER, 5d62403b186f2ace 138 | pubmatic.com, 160265, RESELLER, 5d62403b186f2ace 139 | contextweb.com, 562357, RESELLER, 89ff185a4c4e857c 140 | rhythmone.com, 871930104, RESELLER, a670c89d4a324e47 141 | video.unrulymedia.com, 871930104, RESELLER 142 | google.com, pub-5717092533913515, RESELLER, f08c47fec0942fa0 143 | indexexchange.com, 185192, RESELLER, 50b1c356f2c5c8fc 144 | rhythmone.com, 1149317856, RESELLER, a670c89d4a324e47 145 | rubiconproject.com, 17346, RESELLER, 0bfd66d529a55807 146 | pubmatic.com, 157904, RESELLER, 5d62403b186f2ace 147 | advertising.com, 28613, RESELLER 148 | openx.com, 538929384, RESELLER, 6a698e2ec38604c6 149 | spotx.tv, 108706, RESELLER, 7842df1d2fe2db34 150 | spotxchange.com, 108706, RESELLER, 7842df1d2fe2db34 151 | video.unrulymedia.com, 1149317856, RESELLER 152 | adform.com, 2794, RESELLER, 9f5210a2f0999e32 153 | yahoo.com, 59115, RESELLER 154 | yahoo.com, 59249, RESELLER 155 | google.com, pub-2930805104418204, RESELLER, f08c47fec0942fa0 156 | google.com, pub-4903453974745530, RESELLER, f08c47fec0942fa0 157 | onetag.com, 6b93c95cd63d264, RESELLER 158 | supply.colossusssp.com, 278, DIRECT 159 | appnexus.com, 10490, RESELLER, f5ab79cb980f11d1 160 | google.com, pub-2024690810381654, RESELLER, f08c47fec0942fa0 161 | spotxchange.com, 165877, RESELLER, 7842df1d2fe2db34 162 | spotx.tv, 165877, RESELLER, 7842df1d2fe2db34 163 | avantisvideo.com, 7746, DIRECT 164 | improvedigital.com, 1980, RESELLER 165 | # 166 | primis.tech, 18260, DIRECT, b6b21d256ef43532 167 | sekindo.com, 18260, DIRECT, b6b21d256ef43532 168 | spotxchange.com, 84294, RESELLER, 7842df1d2fe2db34 169 | spotx.tv, 84294, RESELLER, 7842df1d2fe2db34 170 | advertising.com, 7372, RESELLER 171 | pubmatic.com, 156595, RESELLER, 5d62403b186f2ace 172 | google.com, pub-1320774679920841, RESELLER, f08c47fec0942fa0 173 | openx.com, 540258065, RESELLER, 6a698e2ec38604c6 174 | rubiconproject.com, 20130, RESELLER, 0bfd66d529a55807 175 | freewheel.tv, 19129, RESELLER, 74e8e47458f74754 176 | freewheel.tv, 19133, RESELLER, 74e8e47458f74754 177 | freewheel.tv, 1374626, RESELLER, 74e8e47458f74754 178 | freewheel.tv, 1374642, RESELLER, 74e8e47458f74754 179 | smartadserver.com, 3436, RESELLER, 060d053dcf45cbf3 180 | indexexchange.com, 191923, RESELLER, 50b1c356f2c5c8fc 181 | contextweb.com, 562350, RESELLER, 89ff185a4c4e857c 182 | tremorhub.com, mb9eo-oqsbf, RESELLER, 1a4e959a1b50034a 183 | telaria.com, mb9eo-oqsbf, RESELLER, 1a4e959a1b50034a 184 | adform.com, 2078, RESELLER 185 | xandr.com, 13171, RESELLER 186 | supply.colossusssp.com, 290, RESELLER, 6c5b49d96ec1b458 187 | emxdgt.com, 1349, RESELLER, 1e1d41537f7cad7f 188 | yahoo.com, 59260, RESELLER 189 | media.net, 8CU695QH7, RESELLER 190 | smartadserver.com, 4071, RESELLER 191 | sharethrough.com, flUyJowI, RESELLER, d53b998a7bd4ecd2 192 | triplelift.com, 8210, RESELLER, 6c33edb13117fd86 193 | spotx.tv, 97361, RESELLER, 7842df1d2fe2db34 194 | spotxchange.com, 97361, RESELLER, 7842df1d2fe2db34 195 | rhythmone.com, 1296649015, DIRECT, a670c89d4a324e47 196 | video.unrulymedia.com, 1296649015, DIRECT 197 | # 198 | aps.amazon.com,8f617f6d-4231-4918-8ca1-9ba68186e8c5,DIRECT 199 | openx.com,543818292,DIRECT,6a698e2ec38604c6 200 | pubmatic.com,160006,RESELLER,5d62403b186f2ace 201 | pubmatic.com,160096,RESELLER,5d62403b186f2ace 202 | rubiconproject.com,18020,RESELLER,0bfd66d529a55807 203 | pubmatic.com,157150,RESELLER,5d62403b186f2ace 204 | appnexus.com,1908,RESELLER,f5ab79cb980f11d1 205 | smaato.com,1100044650,RESELLER,07bcf65f187117b4 206 | ad-generation.jp,12474,RESELLER,7f4ea9029ac04e53 207 | districtm.io,100962,RESELLER,3fd707be9c4527c3 208 | yieldmo.com,2719019867620450718,RESELLER 209 | appnexus.com,3663,RESELLER,f5ab79cb980f11d1 210 | rhythmone.com,1654642120,RESELLER,a670c89d4a324e47 211 | yahoo.com,55029,RESELLER,e1a5b5b6e3255540 212 | gumgum.com,14141,RESELLER,ffdef49475d318a9 213 | admanmedia.com,726,RESELLER 214 | sharethrough.com,7144eb80,RESELLER,d53b998a7bd4ecd2 215 | emxdgt.com,2009,RESELLER,1e1d41537f7cad7f 216 | appnexus.com,1356,RESELLER,f5ab79cb980f11d1 217 | contextweb.com,562541,RESELLER,89ff185a4c4e857c 218 | smartadserver.com,4125,RESELLER,060d053dcf45cbf3 219 | themediagrid.com,JTQKMP,RESELLER,35d5010d7789b49d 220 | sovrn.com,375328,RESELLER,fafdf38b16bf6b2b 221 | lijit.com,375328,RESELLER,fafdf38b16bf6b2b 222 | beachfront.com,14804,RESELLER,e2541279e8e2ca4d 223 | nativo.com,5708,DIRECT,59521ca7cc5e9fee 224 | improvedigital.com,2050,RESELLER 225 | mintegral.com,10043,RESELLER,0aeed750c80d6423 226 | sonobi.com,d893965a2d,DIRECT,d1a215d9eb5aee9e 227 | onetag.com,770a440e65869c2,RESELLER 228 | sharethrough.com,a289162e,DIRECT,d53b998a7bd4ecd2 229 | # 230 | seedtag.com, 5a0ad809c74bcd070025516a, DIRECT 231 | sovrn.com, 397546, DIRECT, fafdf38b16bf6b2b 232 | lijit.com, 397546, DIRECT, fafdf38b16bf6b2b 233 | onetag.com, 75601b04186d260, DIRECT 234 | advertising.com, 28246, RESELLER 235 | rubiconproject.com, 11006, RESELLER, 0bfd66d529a55807 236 | yahoo.com, 58905, RESELLER, e1a5b5b6e3255540 237 | appnexus.com, 13099, RESELLER, f5ab79cb980f11d1 238 | smartadserver.com, 4111, RESELLER 239 | richaudience.com, ns9qrKJLKD, DIRECT 240 | adform.com, 1942, DIRECT 241 | adform.com, 1941, DIRECT 242 | appnexus.com, 2928, DIRECT, f5ab79cb980f11d1 243 | appnexus.com, 8233, DIRECT, f5ab79cb980f11d1 244 | contextweb.com, 560520, RESELLER, 89ff185a4c4e857c 245 | lijit.com, 249425, RESELLER, fafdf38b16bf6b2b 246 | openx.com, 539625136, RESELLER, 6a698e2ec38604c6 247 | pubmatic.com, 156538, RESELLER, 5d62403b186f2ace 248 | pubmatic.com, 81564, RESELLER, 5d62403b186f2ace 249 | rubiconproject.com, 13510, DIRECT, 0bfd66d529a55807 250 | smartadserver.com, 2640, RESELLER 251 | smartadserver.com, 2441, RESELLER 252 | sovrn.com, 249425, RESELLER, fafdf38b16bf6b2b 253 | yahoo.com, 57857, RESELLER, e1a5b5b6e3255540 254 | xandr.com, 4009, DIRECT, f5ab79cb980f11d1 255 | smartadserver.com, 3050, DIRECT 256 | verve.com, 15503, RESELLER, 0c8f5958fc2d6270 257 | yahoo.com, 58578, DIRECT, e1a5b5b6e3255540 258 | appnexus.com, 11664, RESELLER, f5ab79cb980f11d1 #banner 259 | indexexchange.com, 184349, RESELLER, 50b1c356f2c5c8fc 260 | openx.com, 544096208, RESELLER, 6a698e2ec38604c6 261 | pubmatic.com, 155967, RESELLER, 5d62403b186f2ace 262 | rubiconproject.com, 17250, RESELLER, 0bfd66d529a55807 263 | triplelift.com, 2792, RESELLER, 6c33edb13117fd86 264 | pubmatic.com, 157743, DIRECT, 5d62403b186f2ace 265 | rubiconproject.com, 17280, DIRECT, 0bfd66d529a55807 266 | adform.com, 1889, RESELLER 267 | indexexchange.com, 191730, RESELLER, 50b1c356f2c5c8fc 268 | improvedigital.com, 1680, DIRECT 269 | adyoulike.com, 83d15ef72d387a1e60e5a1399a2b0c03, DIRECT, 4ad745ead2958bf7 270 | rubiconproject.com, 20736, RESELLER, 0bfd66d529a55807 271 | pubmatic.com, 160925, RESELLER, 5d62403b186f2ace 272 | ogury.com, e23b74b2-0627-45c2-9eba-f8cede07f4cc, DIRECT 273 | appnexus.com, 11470, RESELLER 274 | google.com, pub-1386280613967939, RESELLER, f08c47fec0942fa0 275 | 276 | infolinks.com, 3248224, DIRECT 277 | appnexus.com, 3251, RESELLER, f5ab79cb980f11d1 278 | pubmatic.com, 156872, RESELLER, 5d62403b186f2ace 279 | sonobi.com, 0e0a64d7d3, RESELLER, d1a215d9eb5aee9e 280 | sovrn.com, 268479, RESELLER, fafdf38b16bf6b2b 281 | lijit.com, 268479, RESELLER, fafdf38b16bf6b2b 282 | google.com, pub-6373315980741255, RESELLER, f08c47fec0942fa0 283 | google.com, pub-4299156005397946, RESELLER, f08c47fec0942fa0 284 | indexexchange.com, 191306, RESELLER, 50b1c356f2c5c8fc 285 | openx.com, 543174347, RESELLER, 6a698e2ec38604c6 286 | yahoo.com, 58723, RESELLER 287 | advertising.com, 6202, RESELLER 288 | pubmatic.com, 60809, RESELLER, 5d62403b186f2ace 289 | emxdgt.com, 1918, RESELLER, 1e1d41537f7cad7f 290 | xandr.com, 3251, RESELLER 291 | lijit.com, 268479-eb, RESELLER, fafdf38b16bf6b2b 292 | google.com, pub-7858377917172490, RESELLER, f08c47fec0942fa0 293 | google.com, pub-1463455084986126, RESELLER, f08c47fec0942fa0 294 | rhythmone.com, 2221906906, RESELLER, a670c89d4a324e47 295 | video.unrulymedia.com, 2221906906, RESELLER 296 | criteo.com, B-059206, RESELLER, 9fac4a4a87c2a44f 297 | appnexus.com, 7666, RESELLER, f5ab79cb980f11d1 298 | rubiconproject.com, 156042, RESELLER, 0bfd66d529a55807 299 | 33across.com, 0010b00002cpyheaav, RESELLER, bbea06d9c4d2853c 300 | rubiconproject.com, 16414, RESELLER, 0bfd66d529a55807 301 | pubmatic.com, 156423, RESELLER, 5d62403b186f2ace 302 | appnexus.com, 10239, RESELLER, f5ab79cb980f11d1 303 | openx.com, 537120563, RESELLER, 6a698e2ec38604c6 304 | yahoo.com, 57289, RESELLER, e1a5b5b6e3255540 305 | advertising.com, 12543, RESELLER 306 | appnexus.com, 1356, RESELLER, f5ab79cb980f11d1 307 | advertising.com, 28246, RESELLER 308 | rubiconproject.com, 11006, RESELLER, 0bfd66d529a55807 309 | onetag.com, 598ce3ddaee8c90, RESELLER 310 | engagebdr.com, 10475, RESELLER 311 | e-planning.net, 901a809a65f984a4, RESELLER, c1ba615865ed87b2 312 | improvedigital.com, 2016, RESELLER 313 | inmobi.com, b4821b1325c44dc581b04d743ca46575, RESELLER, 83e75a7ae333ca9d 314 | rubiconproject.com, 11726, RESELLER, 0bfd66d529a55807 315 | 33across.com,0010b00002CpYhEAAV,reseller,bbea06d9c4d2853c 316 | themediagrid.com,V19ZSB,reseller,35d5010d7789b49d 317 | yahoo.com,59875,reseller 318 | triplelift.com,11582,reseller,6c33edb13117fd86 319 | media.net,8CUY6IX4H,RESELLER 320 | triplelift.com, 11582, reseller, 6c33edb13117fd86 321 | conversantmedia.com, 100232, reseller, 03113cd04947736d 322 | media.net, 8CUY6IX4H, RESELLER -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /src/data/territories.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "MultiPolygon", 8 | "coordinates": [ 9 | [ 10 | [ 11 | [-40.87580318899995, 65.09650299700007], 12 | [-41.05455481699994, 64.87738678600005], 13 | [-40.561105923999946, 64.51496002800008], 14 | [-41.387277798999946, 64.17853424700007], 15 | [-40.56094316299993, 64.10687897300005], 16 | [-40.614125128999945, 63.80756256700005], 17 | [-41.47288977799991, 63.225775458000044], 18 | [-41.750111456999946, 62.84393952000005], 19 | [-42.423085089999915, 62.81582265800006], 20 | [-42.117176886999914, 62.006740627000056], 21 | [-42.70750891799992, 61.29181549700007], 22 | [-42.826283331999946, 60.61127350500004], 23 | [-43.31541907499991, 60.437486070000034], 24 | [-43.13817298099991, 60.08075592700004], 25 | [-43.99058997299994, 60.19086334800005], 26 | [-44.97484290299991, 60.03974030200004], 27 | [-44.92638098899994, 60.32709381700005], 28 | [-46.37572180899991, 61.08466217700004], 29 | [-46.86229407499991, 60.800116278000075], 30 | [-47.93211829299992, 60.84174225500004], 31 | [-48.07335364499994, 61.08844635600008], 32 | [-48.619130011999914, 61.212144273000035], 33 | [-49.241078253999945, 61.60757070500006], 34 | [-49.31041419199994, 61.996161200000074], 35 | [-50.30772864499994, 62.49127838700008], 36 | [-50.17845618399991, 62.76642487200007], 37 | [-51.056304490999935, 63.184759833000044], 38 | [-51.53921464799993, 63.68504466400003], 39 | [-51.43993079299992, 64.08226146000004], 40 | [-50.89952551999994, 64.40668366100005], 41 | [-51.25255286399994, 64.76019928600005], 42 | [-51.83507239499994, 64.22504303600005], 43 | [-52.15615800699993, 64.67877838700008], 44 | [-52.27285722599993, 65.44651927300004], 45 | [-53.45539303299995, 65.96076080900008], 46 | [-53.695301886999914, 66.35883209800005], 47 | [-53.095773891999954, 66.93195221600007], 48 | [-53.964914516999954, 67.10443756700005], 49 | [-53.576730923999946, 67.51886627800008], 50 | [-53.68765214799993, 67.81183502800008], 51 | [-52.616444464999915, 68.52594635600008], 52 | [-51.56883704299992, 68.52387116100005], 53 | [-51.10187740799995, 68.86619700700004], 54 | [-50.84715735599991, 69.62628815300008], 55 | [-50.332875128999945, 69.88996002800008], 56 | [-52.32949785099993, 70.05361562700006], 57 | [-53.23184160099993, 70.35309479400007], 58 | [-54.01870683499993, 70.41323476800005], 59 | [-54.58812415299991, 70.68256256700005], 60 | [-53.97964433499993, 70.82965729400007], 61 | [-52.45034745999993, 70.70258209800005], 62 | [-51.54942786399994, 70.43158600500004], 63 | [-51.18976803299995, 70.88690827000005], 64 | [-51.91486568899995, 71.02142975500004], 65 | [-52.70091712099992, 71.52594635600008], 66 | [-53.678944464999915, 71.74249909100007], 67 | [-53.95177161399994, 71.43781159100007], 68 | [-55.51447506399995, 71.44916413000004], 69 | [-55.84752356699994, 71.72199127800008], 70 | [-54.88345292899993, 72.25116608300004], 71 | [-54.65025794199994, 72.84495677300004], 72 | [-55.465687628999945, 73.07294342700004], 73 | [-55.26911373599995, 73.38662344000005], 74 | [-56.60338294199994, 74.24481842700004], 75 | [-56.84601803299995, 74.81122467700004], 76 | [-58.13149980399993, 75.10822174700007], 77 | [-58.16673743399991, 75.50043366100005], 78 | [-59.27220618399991, 75.88011302300004], 79 | [-60.87254798099991, 76.15814850500004], 80 | [-63.47691809799994, 76.37689850500004], 81 | [-65.35448157499991, 76.02680084800005], 82 | [-66.48713131399995, 75.94741445500006], 83 | [-68.43252519399994, 76.07941315300008], 84 | [-69.57725989499994, 76.43764883000006], 85 | [-68.91030839799993, 76.66486237200007], 86 | [-70.66014563699991, 76.80198802300004], 87 | [-70.90330969999991, 77.19135163000004], 88 | [-69.06187903599994, 77.25726959800005], 89 | [-72.78429114499994, 78.10761139500005], 90 | [-72.59019934799994, 78.52122630400004], 91 | [-69.43862870999993, 78.80878327000005], 92 | [-69.03392493399991, 78.98004791900007], 93 | [-66.11709550699993, 79.10276927300004], 94 | [-64.85985266799992, 79.50775788000004], 95 | [-64.92601477799991, 80.07265859600005], 96 | [-67.0501195949999, 80.06118398600006], 97 | [-67.45543372299994, 80.34251536700003], 98 | [-62.97679602799991, 81.22833893400008], 99 | [-61.50153561099995, 81.10785553600005], 100 | [-60.78685462099992, 81.50885651200008], 101 | [-61.28107662699995, 81.81513092700004], 102 | [-54.440744594999956, 82.37270742400005], 103 | [-53.02375240799995, 82.00804271000004], 104 | [-50.890695766999954, 81.87811920800004], 105 | [-51.10724850199995, 82.50991445500006], 106 | [-46.00816809799994, 82.03620026200008], 107 | [-44.39879309799994, 82.10521067900004], 108 | [-43.72720292899993, 82.40688711100006], 109 | [-46.557525193999936, 82.89288971600007], 110 | [-45.15379798099991, 83.16233958500004], 111 | [-41.74071204299992, 83.19464752800008], 112 | [-39.437611456999946, 82.96181875200006], 113 | [-37.62832597599993, 83.15355052300004], 114 | [-38.690297003999945, 83.42548248900005], 115 | [-32.49071204299992, 83.63410065300008], 116 | [-25.649647589999915, 83.29755280200004], 117 | [-23.662220831999946, 82.83828359600005], 118 | [-21.318511522999927, 82.60553620000007], 119 | [-22.437326626999948, 82.33999258000006], 120 | [-29.852284308999913, 82.13934967700004], 121 | [-24.619862433999913, 81.98749420800004], 122 | [-21.388417120999918, 82.08344147300005], 123 | [-21.02562415299991, 81.77163320500006], 124 | [-17.146839972999942, 81.41339752800008], 125 | [-16.485218878999945, 81.74945709800005], 126 | [-13.920480923999946, 81.81220123900005], 127 | [-12.02765865799995, 81.63727448100008], 128 | [-11.63857988199993, 81.38841380400004], 129 | [-15.420399542999917, 80.64838288000004], 130 | [-16.447336391999954, 80.21930573100008], 131 | [-19.474354620999918, 80.26019928600005], 132 | [-20.423085089999915, 79.83958567900004], 133 | [-19.207753058999913, 79.70848216400003], 134 | [-19.56590735599991, 79.38593170800004], 135 | [-18.955189581999946, 79.14899323100008], 136 | [-20.978993292999917, 78.76683177300004], 137 | [-21.62999426999994, 77.99811432500007], 138 | [-20.020375128999945, 77.69253164300005], 139 | [-20.230132615999935, 77.37250397300005], 140 | [-18.32445227799991, 77.20848216400003], 141 | [-18.50218665299991, 76.74872467700004], 142 | [-20.513417120999918, 76.94570547100005], 143 | [-21.93545488199993, 76.62270742400005], 144 | [-21.567250128999945, 76.38471100500004], 145 | [-19.672434048999946, 76.12726471600007], 146 | [-19.42723548099991, 75.22553131700005], 147 | [-20.66783606699994, 75.29315827000005], 148 | [-20.758168097999942, 74.83128489800004], 149 | [-18.974761522999927, 74.48436107000003], 150 | [-21.771636522999927, 74.43870677300004], 151 | [-22.229400193999936, 74.10130442900004], 152 | [-20.68858801999994, 73.88646067900004], 153 | [-20.54328365799995, 73.44904205900008], 154 | [-22.314076300999943, 73.25043366100005], 155 | [-24.375396287999934, 73.55955638200004], 156 | [-25.292225714999915, 73.47353750200006], 157 | [-25.70494544199994, 73.27220286700003], 158 | [-25.72061113199993, 73.13812897300005], 159 | [-24.98428300699993, 73.02578359600005], 160 | [-24.34203040299991, 72.33173248900005], 161 | [-22.50340735599991, 71.90078359600005], 162 | [-22.531402147999927, 71.46454498900005], 163 | [-21.667958136999914, 71.40143463700008], 164 | [-21.73005123599995, 70.57904694200005], 165 | [-23.345448370999918, 70.44057851800005], 166 | [-23.929798956999946, 70.61318594000005], 167 | [-24.66942298099991, 71.27195872600004], 168 | [-25.601918097999942, 71.16958242400005], 169 | [-26.425607876999948, 70.97455475500004], 170 | [-27.50617428299995, 70.94212474200003], 171 | [-28.310292120999918, 70.56386953300006], 172 | [-28.232899542999917, 70.37201569200005], 173 | [-26.743316209999932, 70.48411692900004], 174 | [-26.326161261999914, 70.19830963700008], 175 | [-25.305287238999938, 70.41323476800005], 176 | [-23.559803839999915, 70.11286041900007], 177 | [-23.70303300699993, 69.71808502800008], 178 | [-24.61510169199994, 69.28009674700007], 179 | [-26.416737433999913, 68.65892161700003], 180 | [-29.006662563999953, 68.34760163000004], 181 | [-30.008168097999942, 68.12604401200008], 182 | [-32.12539628799993, 68.05068594000005], 183 | [-33.18667558499993, 67.55768463700008], 184 | [-33.750884568999936, 66.98834870000007], 185 | [-34.64329993399991, 66.38043854400007], 186 | [-36.07380123599995, 65.93549225500004], 187 | [-37.21011308499993, 65.77529531500005], 188 | [-37.79792232999995, 66.04873281500005], 189 | [-38.22057044199994, 65.64642975500004], 190 | [-39.87450110599991, 65.50592682500007], 191 | [-40.18431555899991, 65.04189687700006], 192 | [-40.87580318899995, 65.09650299700007] 193 | ] 194 | ], 195 | [ 196 | [ 197 | [-52.902902798999946, 69.34511953300006], 198 | [-53.58739173099991, 69.23908112200007], 199 | [-53.88109290299991, 69.56110260600008], 200 | [-54.992746548999946, 69.70197174700007], 201 | [-54.82917232999995, 70.20083242400005], 202 | [-54.45303300699993, 70.31329987200007], 203 | [-53.26598059799994, 70.20229726800005], 204 | [-51.998158331999946, 69.80508047100005], 205 | [-52.14159094999991, 69.47728099200003], 206 | [-52.902902798999946, 69.34511953300006] 207 | ] 208 | ], 209 | [ 210 | [ 211 | [-25.317250128999945, 70.74697500200006], 212 | [-26.575103318999936, 70.52383047100005], 213 | [-28.067697719999956, 70.43073151200008], 214 | [-27.06749426999994, 70.89354075700004], 215 | [-26.662505662999934, 70.89647044500003], 216 | [-25.70376542899993, 71.08584219000005], 217 | [-25.317250128999945, 70.74697500200006] 218 | ] 219 | ], 220 | [ 221 | [ 222 | [-21.94908606699994, 72.42869700700004], 223 | [-22.250355597999942, 72.12099844000005], 224 | [-24.35179602799991, 72.59129466400003], 225 | [-24.485259568999936, 72.82656484600005], 226 | [-23.14679928299995, 72.84019603100006], 227 | [-21.94908606699994, 72.42869700700004] 228 | ] 229 | ], 230 | [ 231 | [ 232 | [-24.524281378999945, 73.04791901200008], 233 | [-25.70726477799991, 73.18471914300005], 234 | [-25.243519660999937, 73.41111888200004], 235 | [-24.46007239499994, 73.42503489800004], 236 | [-23.230213995999918, 73.25112539300005], 237 | [-24.524281378999945, 73.04791901200008] 238 | ] 239 | ] 240 | ] 241 | }, 242 | "properties": { 243 | "SOVEREIGNT": "Denmark", 244 | "ADMIN": "Greenland \n (Denmark)", 245 | "TYPE": "Territory" 246 | } 247 | }, 248 | { 249 | "type": "Feature", 250 | "geometry": { 251 | "type": "Polygon", 252 | "coordinates": [ 253 | [ 254 | [-54.615292114999875, 2.3262673960000484], 255 | [-54.21252600199995, 2.776420797000057], 256 | [-54.190460164999934, 3.1781017050000884], 257 | [-53.98881872599986, 3.6109951780001097], 258 | [-54.35515295399989, 4.066522929000087], 259 | [-54.48212194899989, 4.912802022000022], 260 | [-54.17096920499995, 5.3483747420000896], 261 | [-53.94434961699994, 5.744640779000065], 262 | [-52.9945805669999, 5.457623554000065], 263 | [-51.93126380099994, 4.590399481000077], 264 | [-51.68321692599994, 4.03937409100007], 265 | [-51.98842403199993, 3.7045553590000395], 266 | [-52.70770829299997, 2.3589269010001175], 267 | [-52.95968257699994, 2.175217184000104], 268 | [-53.3441035569999, 2.3496251430000825], 269 | [-53.866810668999904, 2.304925028000042], 270 | [-54.13490799999994, 2.1106733200000605], 271 | [-54.615292114999875, 2.3262673960000484] 272 | ] 273 | ] 274 | }, 275 | "properties": { 276 | "SOVEREIGNT": "France", 277 | "ADMIN": "French Guiana \n (France)", 278 | "TYPE": "Territory" 279 | } 280 | }, 281 | { 282 | "type": "Feature", 283 | "geometry": { 284 | "type": "Polygon", 285 | "coordinates": [ 286 | [ 287 | [9.560016, 42.152492], 288 | [9.229752, 41.380007], 289 | [8.775723, 41.583612], 290 | [8.544213, 42.256517], 291 | [8.746009, 42.628122], 292 | [9.390001, 43.009985], 293 | [9.560016, 42.152492] 294 | ] 295 | ] 296 | }, 297 | "properties": { 298 | "SOVEREIGNT": "France", 299 | "ADMIN": "Corsica \n (France)", 300 | "TYPE": "Territory" 301 | } 302 | }, 303 | { 304 | "type": "Feature", 305 | "geometry": { 306 | "type": "MultiPolygon", 307 | "coordinates": [ 308 | [ 309 | [ 310 | [24.42476468500007, 77.82593585100005], 311 | [23.23454060800009, 77.26368345100008], 312 | [22.191742384000065, 77.50413646000004], 313 | [21.66179446700005, 77.91803620000007], 314 | [23.12045332100007, 77.98590729400007], 315 | [24.42476468500007, 77.82593585100005] 316 | ] 317 | ], 318 | [ 319 | [ 320 | [20.20875084700009, 78.63808828300006], 321 | [18.993011915000068, 78.46698639500005], 322 | [18.194102410000085, 77.49017975500004], 323 | [17.323741082000083, 76.97894928600005], 324 | [16.336761915000068, 76.61644114800004], 325 | [15.040212436000047, 77.13475169500003], 326 | [14.373708530000044, 77.19867584800005], 327 | [13.589121941000087, 78.05585358300004], 328 | [15.034678582000083, 78.11664459800005], 329 | [15.795095248000052, 78.34292226800005], 330 | [15.209320509000065, 78.65619538000004], 331 | [13.855967644000089, 78.21287669500003], 332 | [11.643239780000044, 78.74725983300004], 333 | [10.708018425000091, 79.56142812700006], 334 | [11.822276238000086, 79.84503815300008], 335 | [13.797048373000052, 79.88190338700008], 336 | [15.262380405000044, 79.61884186400005], 337 | [16.616465691000087, 79.98505280200004], 338 | [17.562510613000086, 79.89785390800006], 339 | [18.88184655000009, 79.44110748900005], 340 | [18.94507897200009, 79.15696849200003], 341 | [21.522146030000044, 78.85370514500005], 342 | [20.20875084700009, 78.63808828300006] 343 | ] 344 | ], 345 | [ 346 | [ 347 | [27.171071811000047, 80.07343170800004], 348 | [27.149668816000087, 79.85496653900003], 349 | [25.645681186000047, 79.39769114800004], 350 | [22.925629102000073, 79.22284577000005], 351 | [20.81690514400009, 79.37807851800005], 352 | [18.783864780000044, 79.72308991100005], 353 | [18.007823113000086, 80.19037506700005], 354 | [20.88257897200009, 80.21124909100007], 355 | [24.37598717500009, 80.33234284100007], 356 | [27.171071811000047, 80.07343170800004] 357 | ] 358 | ] 359 | ] 360 | }, 361 | "properties": { 362 | "SOVEREIGNT": "Norway", 363 | "ADMIN": "Svalbard \n (Norway)", 364 | "TYPE": "Territory" 365 | } 366 | }, 367 | { 368 | "type": "Feature", 369 | "geometry": { 370 | "type": "Polygon", 371 | "coordinates": [ 372 | [ 373 | [-65.62881425699993, 18.27920156500005], 374 | [-65.83026947699994, 18.026906814000085], 375 | [-66.16323808499993, 17.92914459800005], 376 | [-67.2147711799999, 17.966335022000067], 377 | [-67.10171464799993, 18.522772528000075], 378 | [-65.98888098899994, 18.46308014500005], 379 | [-65.62881425699993, 18.27920156500005] 380 | ] 381 | ] 382 | }, 383 | "properties": { 384 | "SOVEREIGNT": "United States of America", 385 | "ADMIN": "Puerto Rico \n (U.S.A.)", 386 | "TYPE": "Territory" 387 | } 388 | }, 389 | { 390 | "type": "Feature", 391 | "geometry": { 392 | "type": "Polygon", 393 | "coordinates": [ 394 | [ 395 | [92.64230042056583, 13.717389598439842], 396 | [93.22896926955723, 13.701735956019666], 397 | [93.93539003388427, 12.250521520446439], 398 | [92.53133810417198, 10.460955676551686], 399 | [92.34566956119083, 10.457444232446672], 400 | [92.15011305467628, 11.558042918811969], 401 | [92.64230042056583, 13.717389598439842] 402 | ] 403 | ] 404 | }, 405 | "properties": { 406 | "SOVEREIGNT": "India", 407 | "ADMIN": "Andaman and Nicobar Islands \n (India)", 408 | "TYPE": "Territory" 409 | } 410 | }, 411 | { 412 | "type": "Feature", 413 | "geometry": { 414 | "type": "Polygon", 415 | "coordinates": [ 416 | [ 417 | [-60.826269531250006, 14.494482421874991], 418 | [-60.83662109374998, 14.437402343750009], 419 | [-60.86210937499996, 14.426269531250043], 420 | [-60.8994140625, 14.473779296875037], 421 | [-61.063720703125, 14.467089843750017], 422 | [-61.08886718749997, 14.509570312500045], 423 | [-61.09033203125, 14.5296875], 424 | [-61.01132812499998, 14.601904296875034], 425 | [-61.10429687499999, 14.62124023437498], 426 | [-61.14111328124994, 14.652392578125017], 427 | [-61.21972656249997, 14.804394531249983], 428 | [-61.21333007812501, 14.848583984375011], 429 | [-61.18081054687494, 14.871923828124977], 430 | [-61.12739257812498, 14.875292968750045], 431 | [-61.027099609375, 14.826171874999986], 432 | [-60.952539062499994, 14.75625], 433 | [-60.927148437499966, 14.755175781249989], 434 | [-60.91865234375001, 14.735351562500043], 435 | [-60.93369140624998, 14.686181640624966], 436 | [-60.88916015624994, 14.644531250000014], 437 | [-60.86997070312495, 14.61372070312504], 438 | [-60.826269531250006, 14.494482421874991] 439 | ] 440 | ] 441 | }, 442 | "properties": { 443 | "SOVEREIGNT": "France", 444 | "ADMIN": "Martinique \n (France)", 445 | "TYPE": "Territory" 446 | } 447 | }, 448 | { 449 | "type": "Feature", 450 | "geometry": { 451 | "type": "Polygon", 452 | "coordinates": [ 453 | [ 454 | [22.76721968600006, 54.356269837000056], 455 | [20.92960575300009, 54.397817688000046], 456 | [19.609548373000052, 54.456732489000046], 457 | [20.03370201900009, 54.94818756700005], 458 | [21.23015384200005, 54.93891022300005], 459 | [21.267588738000086, 55.24868398600006], 460 | [22.808870891000083, 54.89375640900009], 461 | [22.76721968600006, 54.356269837000056] 462 | ] 463 | ] 464 | }, 465 | "properties": { 466 | "SOVEREIGNT": "Russia", 467 | "ADMIN": "Kaliningrad \n (Russia)", 468 | "TYPE": "Territory" 469 | } 470 | }, 471 | { 472 | "type": "Feature", 473 | "geometry": { 474 | "type": "Polygon", 475 | "coordinates": [ 476 | [ 477 | [-8.68238521299989, 27.66143931100008], 478 | [-8.68238521299989, 27.285415751000116], 479 | [-8.680809081999882, 26.013141989000033], 480 | [-9.698653930999939, 25.994951884000088], 481 | [-12.015308389999888, 25.99490020800006], 482 | [-12.015308389999888, 23.495181986000077], 483 | [-12.619431721999916, 23.27082875600003], 484 | [-13.015247354999872, 23.018001811000133], 485 | [-13.16549658199989, 22.752695008000032], 486 | [-13.093330444999879, 22.495475566000053], 487 | [-13.015247354999872, 21.333427633000028], 488 | [-15.002257853999936, 21.333169251000058], 489 | [-16.958830932999973, 21.332859192000072], 490 | [-17.05687415299991, 20.766913153000075], 491 | [-17.013743325393165, 21.419971097583428], 492 | [-15.749964151999905, 21.49031728100006], 493 | [-14.83981298899991, 21.450268046000062], 494 | [-14.62985164399987, 21.860423889000103], 495 | [-14.220186726999941, 22.30964711500006], 496 | [-14.019992227999865, 23.410251771000006], 497 | [-13.76998164899993, 23.790125224000022], 498 | [-13.310009724999873, 23.98055287700005], 499 | [-13.060024983999938, 24.400475566000054], 500 | [-12.430140949999895, 24.830165101000105], 501 | [-12.029751952999902, 26.030350241000107], 502 | [-11.717238728999888, 26.1035757450001], 503 | [-11.336900186999856, 26.632897441000026], 504 | [-11.391573852999898, 26.882908021000063], 505 | [-10.921835082999877, 27.009825338000027], 506 | [-10.250454873999956, 26.860428773000038], 507 | [-9.734362345999926, 26.860428773000038], 508 | [-9.412056436999961, 27.08796010400006], 509 | [-8.752871866999953, 27.190486146000026], 510 | [-8.68238521299989, 27.66143931100008] 511 | ] 512 | ] 513 | }, 514 | "properties": { 515 | "SOVEREIGNT": "Morocco", 516 | "ADMIN": "Western Sahara \n (Morocco)", 517 | "TYPE": "Territory" 518 | } 519 | }, 520 | { 521 | "type": "Feature", 522 | "geometry": { 523 | "type": "Polygon", 524 | "coordinates": [ 525 | [ 526 | [-16.427695488999916, 28.149669540000048], 527 | [-16.676339731999917, 27.99709648700008], 528 | [-16.91605808999992, 28.354969806000042], 529 | [-16.507135176999952, 28.424134052000056], 530 | [-16.427695488999916, 28.149669540000048] 531 | ] 532 | ] 533 | }, 534 | "properties": { 535 | "SOVEREIGNT": "Spain", 536 | "ADMIN": "Canary Islands \n (Spain)", 537 | "TYPE": "Territory" 538 | } 539 | }, 540 | { 541 | "type": "Feature", 542 | "geometry": { 543 | "type": "MultiPolygon", 544 | "coordinates": [ 545 | [ 546 | [ 547 | [164.20234375000004, -20.246093749999957], 548 | [164.3151367187501, -20.30888671874996], 549 | [164.4359375, -20.282226562499957], 550 | [164.5880859375001, -20.38115234375003], 551 | [164.97568359375012, -20.681054687500023], 552 | [165.11191406250006, -20.74453125], 553 | [165.191796875, -20.768847656249974], 554 | [165.25234375, -20.817968750000034], 555 | [165.30664062500003, -20.887011718749974], 556 | [165.3805664062501, -20.935839843749974], 557 | [165.4125, -20.98134765625001], 558 | [165.42050781250006, -21.042773437500003], 559 | [165.44716796875005, -21.08056640624997], 560 | [165.582421875, -21.179980468749974], 561 | [165.66279296875004, -21.267187499999977], 562 | [165.77460937500004, -21.311718749999983], 563 | [165.82285156250012, -21.36376953125003], 564 | [165.88535156250006, -21.389160156249957], 565 | [165.94951171875007, -21.442382812499957], 566 | [166.05781250000004, -21.483886718749986], 567 | [166.30332031250006, -21.637207031249957], 568 | [166.49296875000002, -21.782812500000034], 569 | [166.58750000000012, -21.872851562500003], 570 | [166.68964843750004, -21.953027343749994], 571 | [166.82011718750007, -22.016992187499966], 572 | [166.94238281250003, -22.09013671875003], 573 | [167.00429687500005, -22.26152343749996], 574 | [166.97031250000012, -22.32285156250002], 575 | [166.90000000000012, -22.353320312500003], 576 | [166.83496093749997, -22.35546875], 577 | [166.77412109375004, -22.37617187500004], 578 | [166.57060546875007, -22.265527343749966], 579 | [166.52216796875004, -22.249218750000026], 580 | [166.4679687500001, -22.256054687499997], 581 | [166.43769531250004, -22.231542968749977], 582 | [166.41640625, -22.196191406249966], 583 | [166.29228515625002, -22.15507812500003], 584 | [166.17666015625, -22.089160156250017], 585 | [166.14316406250012, -22.044433593749957], 586 | [166.12373046875004, -21.988769531249986], 587 | [166.09609375, -21.95664062500002], 588 | [165.9330078125, -21.90800781249996], 589 | [165.82343750000004, -21.85380859375003], 590 | [165.7438476562501, -21.777343749999986], 591 | [165.62021484375006, -21.72421875], 592 | [165.42763671875, -21.61503906249996], 593 | [165.32861328124997, -21.580078125000043], 594 | [165.24199218750002, -21.52548828125002], 595 | [165.01015625000005, -21.32685546874997], 596 | [164.92744140625004, -21.289843749999974], 597 | [164.85527343750002, -21.201562500000023], 598 | [164.6556640625, -20.99208984374998], 599 | [164.55947265625005, -20.905859375], 600 | [164.45468750000012, -20.829101562499986], 601 | [164.37451171875003, -20.739257812499986], 602 | [164.312890625, -20.632714843750037], 603 | [164.16972656250007, -20.48017578125004], 604 | [164.15214843750002, -20.414941406249994], 605 | [164.15810546875, -20.347949218750017], 606 | [164.12363281250006, -20.30488281250004], 607 | [164.06503906250012, -20.278613281250017], 608 | [164.0373046875001, -20.23359375], 609 | [164.04052734375003, -20.172851562499957], 610 | [164.05966796875012, -20.141503906249966], 611 | [164.20234375000004, -20.246093749999957] 612 | ] 613 | ] 614 | ] 615 | }, 616 | "properties": { 617 | "SOVEREIGNT": "France", 618 | "ADMIN": "New Caledonia \n (France)", 619 | "TYPE": "Territory" 620 | } 621 | } 622 | ] 623 | } 624 | --------------------------------------------------------------------------------