Skip to content

Commit 0aa1ba9

Browse files
committed
Add new custom-quiz page
1 parent 49c4ba3 commit 0aa1ba9

File tree

6 files changed

+122
-15
lines changed

6 files changed

+122
-15
lines changed

src/components/GuessLocationQuiz.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
1313
import Map, { Layer, Marker, Source, type MapRef, type StyleSpecification } from "react-map-gl/maplibre";
1414
import { Mode } from "../enums";
1515
import { shuffle } from "../utils/ArrayUtils";
16-
import { clampLat, clampLng, getDistanceToCurrentFeature } from "../utils/MapUtils";
16+
import { clampMapBounds, getDistanceToCurrentFeature } from "../utils/MapUtils";
1717
import DistanceLabel from "./DistanceLabel";
1818
import GameOverModal from "./GameOverModal";
1919
import { hoverLineLayerStyle, hoverPointLayerStyle, hoverPolygonLayerStyle, outlinePolygonLayerStyle } from "./mapstyle";
@@ -96,7 +96,7 @@ export default function GuessLocationQuiz({ data, datasetName, onResetGame }: Pr
9696
padding: { left: 12, top: 12, right: 12, bottom: 12 }
9797
}
9898
}}
99-
maxBounds={[clampLng(minLng - 5), clampLat(minLat - 3), clampLng(maxLng + 5), clampLat(maxLat + 3)]}
99+
maxBounds={clampMapBounds([minLng - 5, minLat - 3, maxLng + 5, maxLat + 3])}
100100
maxZoom={16}
101101
doubleClickZoom={false}
102102
dragRotate={false}

src/components/MapView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import "maplibre-gl/dist/maplibre-gl.css";
1111
import { useEffect, useMemo, useRef } from "react";
1212
import type { MapLayerMouseEvent, MapLayerTouchEvent, MapRef, StyleSpecification } from "react-map-gl/maplibre";
1313
import Map, { AttributionControl, Layer, Source } from "react-map-gl/maplibre";
14-
import { clampLat, clampLng } from "../utils/MapUtils";
14+
import { clampMapBounds } from "../utils/MapUtils";
1515
import {
1616
hoverLineLayerStyle,
1717
hoverPointLayerStyle,
@@ -118,7 +118,7 @@ export default function MapView({ data, pendingGuessFeatures, rightGuessFeatures
118118
padding: { left: 12, top: 12, right: 12, bottom: 12 }
119119
}
120120
}}
121-
maxBounds={[clampLng(minLng - 5), clampLat(minLat - 3), clampLng(maxLng + 5), clampLat(maxLat + 3)]}
121+
maxBounds={clampMapBounds([minLng - 5, minLat - 3, maxLng + 5, maxLat + 3])}
122122
maxZoom={16}
123123
doubleClickZoom={false}
124124
dragRotate={false}

src/main.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import App from "./App.tsx";
1515
import "./i18n";
1616
import "./index.css";
1717
import Play from "./pages/Play.tsx";
18+
import SelectFile from "./pages/SelectFile.tsx";
1819

1920
const router = createBrowserRouter([
2021
{
@@ -25,6 +26,10 @@ const router = createBrowserRouter([
2526
path: "/play",
2627
element: <Play />,
2728
},
29+
{
30+
path: "/custom-quiz",
31+
element: <SelectFile />,
32+
},
2833
]);
2934

3035
createRoot(document.getElementById("root")!).render(

src/pages/Play.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Feature, FeatureCollection } from "geojson";
1010
import { useEffect, useState } from "react";
1111
import { useTranslation } from 'react-i18next';
1212
import { Link } from "react-router";
13-
import { useSearchParams } from "react-router-dom";
13+
import { useLocation, useSearchParams } from 'react-router-dom';
1414
import CityMapQuiz from "../components/CityMapQuiz";
1515
import GuessLocationQuiz from "../components/GuessLocationQuiz";
1616
import StandardQuiz from "../components/StandardQuiz";
@@ -53,19 +53,28 @@ export default function Play() {
5353
const [data, setData] = useState<FeatureCollection | undefined>();
5454
const [error, setError] = useState<Error | null>(null);
5555
const [queryParams] = useSearchParams();
56+
const location = useLocation();
5657
const { t } = useTranslation();
5758

5859
useEffect(() => {
59-
if (queryParams.get("dataset") === "random") {
60+
const dataset = queryParams.get("dataset");
61+
if (dataset === "random") {
6062
selectRandomData()
6163
.then((featureCollection) => {
6264
setData(featureCollection);
6365
})
6466
.catch((error: Error) => {
6567
setError(error);
6668
});
69+
} else if (dataset == null) {
70+
if (location.state == null) {
71+
setError(new Error("No dataset was provided"));
72+
return;
73+
}
74+
const { fileContent } = location.state as { fileContent: FeatureCollection };
75+
setData(fileContent);
6776
} else {
68-
import(`../assets/geojson/${queryParams.get("dataset")}.json`)
77+
import(`../assets/geojson/${dataset}.json`)
6978
.then((geojson: { default: FeatureCollection }) => {
7079
const featureCollection = geojson.default;
7180
setData(featureCollection);
@@ -74,7 +83,7 @@ export default function Play() {
7483
setError(error);
7584
});
7685
}
77-
}, [queryParams]);
86+
}, [location.state, queryParams]);
7887

7988
if (error) {
8089
return <p className="h-screen flex items-center justify-center">{String(error)}</p>;
@@ -83,8 +92,6 @@ export default function Play() {
8392
return <p className="h-screen flex items-center justify-center">Loading...</p>;
8493
}
8594

86-
// Validate datasets are well formed geojsons with id and name fields
87-
8895
const mode = (queryParams.get("mode") || Mode.PointAndClick) as Mode;
8996
const datasetName = settingsJson.datasets.find(dataset => removeFileExtension(dataset.data) === queryParams.get("dataset"))?.name;
9097
let quiz = null;

src/pages/SelectFile.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2025, Carlos Pérez Ramil
3+
*
4+
* This file is part of the GaliGuessr project and is licensed under the GNU GPL v3.0.
5+
* See LICENSE file in the root directory of this project or at <https://www.gnu.org/licenses/gpl-3.0>.
6+
*/
7+
8+
import type { FeatureCollection } from "geojson";
9+
import { useCallback, useState, type ChangeEvent } from "react";
10+
import { useTranslation } from "react-i18next";
11+
import { useNavigate } from "react-router-dom";
12+
import { Mode } from "../enums";
13+
14+
export default function SelectFile() {
15+
const [fileContent, setFileContent] = useState<FeatureCollection | null>(null);
16+
const [error, setError] = useState<string | null>(null);
17+
const navigate = useNavigate();
18+
const { t } = useTranslation();
19+
20+
const handleFileChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
21+
setError(null);
22+
const file = event.target.files?.[0];
23+
if (!file) {
24+
return;
25+
}
26+
27+
const reader = new FileReader();
28+
reader.onload = (e) => {
29+
try {
30+
const content = e.target?.result as string;
31+
const featureCollection = JSON.parse(content) as FeatureCollection;
32+
featureCollection.features.forEach((feature, index) => feature.id = index);
33+
setFileContent(featureCollection);
34+
} catch (err) {
35+
setError("Failed to parse JSON file");
36+
console.error("Error parsing JSON:", err);
37+
}
38+
};
39+
reader.onerror = () => {
40+
setError("Error reading file");
41+
};
42+
reader.readAsText(file);
43+
44+
//TODO: Validate datasets are well formed geojsons with id and name fields
45+
46+
}, []);
47+
48+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
49+
event.preventDefault();
50+
const selectedMode = event.currentTarget.elements.namedItem("mode") as HTMLInputElement;
51+
void navigate(`/play?mode=${selectedMode.value}`, { state: { fileContent } });
52+
};
53+
54+
return (
55+
<div className="flex flex-col items-center p-10">
56+
<form
57+
className="flex flex-col items-center gap-4"
58+
onSubmit={handleSubmit}
59+
>
60+
<label><b>Select a valid GeoJSON File</b></label>
61+
<input
62+
type="file"
63+
accept=".json,.geojson"
64+
onChange={handleFileChange}
65+
/>
66+
<select id="mode" name="mode">
67+
{Object.values(Mode)
68+
.map((mode, index: number) => (
69+
<option
70+
value={mode}
71+
key={index}>
72+
{t("modes." + mode, { lng: 'en' })}
73+
</option>
74+
))
75+
}
76+
</select>
77+
<button
78+
className="border border-solid border-black p-2"
79+
type="submit"
80+
>
81+
Play
82+
</button>
83+
</form>
84+
85+
{error && <div style={{ color: "red" }}>{error}</div>}
86+
87+
</div>
88+
);
89+
};

src/utils/MapUtils.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,17 @@ export function getDistanceToCurrentFeature(currentFeatureGeometry: Geometry, us
6969
return result;
7070
}
7171

72+
export function clampMapBounds(bounds: [number, number, number, number]): [number, number, number, number] | undefined {
73+
let [minLng, minLat, maxLng, maxLat] = bounds;
7274

73-
export function clampLng(lng: number): number {
74-
return Math.min(Math.max(lng, -180.0), 180.0);
75-
}
75+
minLng = ((minLng + 180) % 360 + 360) % 360 - 180;
76+
maxLng = ((maxLng + 180) % 360 + 360) % 360 - 180;
77+
if (minLng > maxLng) {
78+
return undefined;
79+
}
80+
81+
minLat = Math.min(Math.max(minLat, -90.0), 90.0);
82+
maxLat = Math.min(Math.max(maxLat, -90.0), 90.0);
7683

77-
export function clampLat(lat: number): number {
78-
return Math.min(Math.max(lat, -90.0), 90.0);
84+
return [minLng, minLat, maxLng, maxLat];
7985
}

0 commit comments

Comments
 (0)