Add search, query url based state
Still need to wire up the mod and cell data to the search.
This commit is contained in:
parent
dbec8def39
commit
b263c6b0cb
@ -47,12 +47,12 @@ const jsonFetcher = async (url: string): Promise<Cell | null> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedCell: [number, number];
|
selectedCell: { x: number; y: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
const CellData: React.FC<Props> = ({ selectedCell }) => {
|
const CellData: React.FC<Props> = ({ selectedCell }) => {
|
||||||
const { data, error } = useSWRImmutable(
|
const { data, error } = useSWRImmutable(
|
||||||
`https://cells.modmapper.com/${selectedCell[0]}/${selectedCell[1]}.json`,
|
`https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`,
|
||||||
jsonFetcher
|
jsonFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import Gradient from "javascript-color-gradient";
|
import Gradient from "javascript-color-gradient";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from "swr/immutable";
|
||||||
@ -6,6 +7,7 @@ import useSWRImmutable from "swr/immutable";
|
|||||||
import styles from "../styles/Map.module.css";
|
import styles from "../styles/Map.module.css";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import ToggleLayersControl from "./ToggleLayersControl";
|
import ToggleLayersControl from "./ToggleLayersControl";
|
||||||
|
import SearchBar from "./SearchBar";
|
||||||
|
|
||||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
|
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ colorGradient.setMidpoint(360);
|
|||||||
const jsonFetcher = (url: string) => fetch(url).then((res) => res.json());
|
const jsonFetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
const Map: React.FC = () => {
|
const Map: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
const mapContainer = useRef<HTMLDivElement | null>(
|
const mapContainer = useRef<HTMLDivElement | null>(
|
||||||
null
|
null
|
||||||
) as React.MutableRefObject<HTMLDivElement>;
|
) as React.MutableRefObject<HTMLDivElement>;
|
||||||
@ -31,15 +34,160 @@ const Map: React.FC = () => {
|
|||||||
const mapWrapper = useRef<HTMLDivElement | null>(
|
const mapWrapper = useRef<HTMLDivElement | null>(
|
||||||
null
|
null
|
||||||
) as React.MutableRefObject<HTMLDivElement>;
|
) as React.MutableRefObject<HTMLDivElement>;
|
||||||
const [selectedCell, setSelectedCell] = useState<[number, number] | null>(
|
|
||||||
null
|
const [mapLoaded, setMapLoaded] = useState<boolean>(false);
|
||||||
);
|
const [heatmapLoaded, setHeatmapLoaded] = useState<boolean>(false);
|
||||||
|
const [selectedCell, setSelectedCell] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
const sidebarOpen = selectedCell !== null || router.query.mod !== undefined;
|
||||||
|
|
||||||
const { data, error } = useSWRImmutable(
|
const { data, error } = useSWRImmutable(
|
||||||
"https://cells.modmapper.com/edits.json",
|
"https://cells.modmapper.com/edits.json",
|
||||||
jsonFetcher
|
jsonFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectMapCell = useCallback(
|
||||||
|
(cell: { x: number; y: number }) => {
|
||||||
|
if (!map.current) return;
|
||||||
|
if (map.current && !map.current.getSource("grid-source")) return;
|
||||||
|
|
||||||
|
map.current.setFeatureState(
|
||||||
|
{
|
||||||
|
source: "grid-source",
|
||||||
|
id: (cell.x + 57) * 100 + 50 - cell.y,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selected: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
requestAnimationFrame(() => map.current && map.current.resize());
|
||||||
|
|
||||||
|
var zoom = map.current.getZoom();
|
||||||
|
var viewportNW = map.current.project([-180, 85.051129]);
|
||||||
|
var cellSize = Math.pow(2, zoom + 2);
|
||||||
|
const x = cell.x + 57;
|
||||||
|
const y = 50 - cell.y;
|
||||||
|
let nw = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x,
|
||||||
|
y * cellSize + viewportNW.y,
|
||||||
|
]);
|
||||||
|
let ne = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x + cellSize,
|
||||||
|
y * cellSize + viewportNW.y,
|
||||||
|
]);
|
||||||
|
let se = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x + cellSize,
|
||||||
|
y * cellSize + viewportNW.y + cellSize,
|
||||||
|
]);
|
||||||
|
let sw = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x,
|
||||||
|
y * cellSize + viewportNW.y + cellSize,
|
||||||
|
]);
|
||||||
|
const selectedCellLines: GeoJSON.FeatureCollection<
|
||||||
|
GeoJSON.Geometry,
|
||||||
|
GeoJSON.GeoJsonProperties
|
||||||
|
> = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: "Feature",
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: [
|
||||||
|
[nw.lng, nw.lat],
|
||||||
|
[ne.lng, ne.lat],
|
||||||
|
[se.lng, se.lat],
|
||||||
|
[sw.lng, sw.lat],
|
||||||
|
[nw.lng, nw.lat],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
properties: { x: x, y: y },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (map.current.getLayer("selected-cell-layer")) {
|
||||||
|
map.current.removeLayer("selected-cell-layer");
|
||||||
|
}
|
||||||
|
if (map.current.getSource("selected-cell-source")) {
|
||||||
|
map.current.removeSource("selected-cell-source");
|
||||||
|
}
|
||||||
|
map.current.addSource("selected-cell-source", {
|
||||||
|
type: "geojson",
|
||||||
|
data: selectedCellLines,
|
||||||
|
});
|
||||||
|
map.current.addLayer({
|
||||||
|
id: "selected-cell-layer",
|
||||||
|
type: "line",
|
||||||
|
source: "selected-cell-source",
|
||||||
|
paint: {
|
||||||
|
"line-color": "blue",
|
||||||
|
"line-width": 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bounds = map.current.getBounds();
|
||||||
|
if (!bounds.contains(nw) || !bounds.contains(se)) {
|
||||||
|
map.current.panTo(nw);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[map]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectCell = useCallback(
|
||||||
|
(cell) => {
|
||||||
|
router.push({ query: { cell: cell.x + "," + cell.y } });
|
||||||
|
setSelectedCell(cell);
|
||||||
|
selectMapCell(cell);
|
||||||
|
},
|
||||||
|
[setSelectedCell, selectMapCell, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearSelectedCell = useCallback(() => {
|
||||||
|
setSelectedCell(null);
|
||||||
|
router.push({ query: {} });
|
||||||
|
if (map.current) map.current.removeFeatureState({ source: "grid-source" });
|
||||||
|
if (map.current && map.current.getLayer("selected-cell-layer")) {
|
||||||
|
map.current.removeLayer("selected-cell-layer");
|
||||||
|
}
|
||||||
|
if (map.current && map.current.getSource("selected-cell-source")) {
|
||||||
|
map.current.removeSource("selected-cell-source");
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (map.current) map.current.resize();
|
||||||
|
});
|
||||||
|
}, [map, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!heatmapLoaded) return; // wait for all map layers to load
|
||||||
|
if (router.query.cell && typeof router.query.cell === "string") {
|
||||||
|
const cellUrlParts = decodeURIComponent(router.query.cell).split(",");
|
||||||
|
const cell = {
|
||||||
|
x: parseInt(cellUrlParts[0]),
|
||||||
|
y: parseInt(cellUrlParts[1]),
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
!selectedCell ||
|
||||||
|
selectedCell.x !== cell.x ||
|
||||||
|
selectedCell.y !== cell.y
|
||||||
|
) {
|
||||||
|
selectCell(cell);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedCell) {
|
||||||
|
clearSelectedCell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
selectedCell,
|
||||||
|
router.query.cell,
|
||||||
|
selectCell,
|
||||||
|
clearSelectedCell,
|
||||||
|
heatmapLoaded,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map.current) return; // initialize map only once
|
if (map.current) return; // initialize map only once
|
||||||
map.current = new mapboxgl.Map({
|
map.current = new mapboxgl.Map({
|
||||||
@ -73,341 +221,255 @@ const Map: React.FC = () => {
|
|||||||
[180, 85.051129],
|
[180, 85.051129],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
map.current.on("load", () => {
|
||||||
|
setMapLoaded(true);
|
||||||
|
});
|
||||||
|
}, [setMapLoaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return; // wait for map to initialize and data to load
|
if (!data || !router.isReady || !mapLoaded) return; // wait for map to initialize and data to load
|
||||||
map.current.on("load", () => {
|
if (map.current.getSource("graticule")) return; // don't initialize twice
|
||||||
const zoom = map.current.getZoom();
|
|
||||||
const viewportNW = map.current.project([-180, 85.051129]);
|
|
||||||
const cellSize = Math.pow(2, zoom + 2);
|
|
||||||
|
|
||||||
const graticule: GeoJSON.FeatureCollection<
|
const zoom = map.current.getZoom();
|
||||||
GeoJSON.Geometry,
|
const viewportNW = map.current.project([-180, 85.051129]);
|
||||||
GeoJSON.GeoJsonProperties
|
const cellSize = Math.pow(2, zoom + 2);
|
||||||
> = {
|
|
||||||
type: "FeatureCollection",
|
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
for (let x = 0; x < 128; x += 1) {
|
|
||||||
let lng = map.current.unproject([x * cellSize + viewportNW.x, -90]).lng;
|
|
||||||
graticule.features.push({
|
|
||||||
type: "Feature",
|
|
||||||
geometry: {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: [
|
|
||||||
[lng, -90],
|
|
||||||
[lng, 90],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: { value: x },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (let y = 0; y < 128; y += 1) {
|
|
||||||
let lat = map.current.unproject([
|
|
||||||
-180,
|
|
||||||
y * cellSize + viewportNW.y,
|
|
||||||
]).lat;
|
|
||||||
graticule.features.push({
|
|
||||||
type: "Feature",
|
|
||||||
geometry: {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: [
|
|
||||||
[-180, lat],
|
|
||||||
[180, lat],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: { value: y },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
map.current.addSource("graticule", {
|
const graticule: GeoJSON.FeatureCollection<
|
||||||
type: "geojson",
|
GeoJSON.Geometry,
|
||||||
data: graticule,
|
GeoJSON.GeoJsonProperties
|
||||||
});
|
> = {
|
||||||
|
type: "FeatureCollection",
|
||||||
map.current.addLayer({
|
features: [],
|
||||||
id: "graticule",
|
};
|
||||||
type: "line",
|
for (let x = 0; x < 128; x += 1) {
|
||||||
source: "graticule",
|
let lng = map.current.unproject([x * cellSize + viewportNW.x, -90]).lng;
|
||||||
});
|
graticule.features.push({
|
||||||
|
type: "Feature",
|
||||||
const gridLabelPoints: GeoJSON.FeatureCollection<
|
geometry: {
|
||||||
GeoJSON.Geometry,
|
type: "LineString",
|
||||||
GeoJSON.GeoJsonProperties
|
coordinates: [
|
||||||
> = {
|
[lng, -90],
|
||||||
type: "FeatureCollection",
|
[lng, 90],
|
||||||
features: [],
|
],
|
||||||
};
|
|
||||||
for (let x = 0; x < 128; x += 1) {
|
|
||||||
for (let y = 0; y < 128; y += 1) {
|
|
||||||
let nw = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x + cellSize / 32,
|
|
||||||
y * cellSize + viewportNW.y + cellSize / 32,
|
|
||||||
]);
|
|
||||||
gridLabelPoints.features.push({
|
|
||||||
type: "Feature",
|
|
||||||
geometry: {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: [nw.lng, nw.lat],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
label: `${x - 57}, ${50 - y}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.current.addSource("grid-labels-source", {
|
|
||||||
type: "geojson",
|
|
||||||
data: gridLabelPoints,
|
|
||||||
});
|
|
||||||
|
|
||||||
map.current.addLayer({
|
|
||||||
id: "grid-labels-layer",
|
|
||||||
type: "symbol",
|
|
||||||
source: "grid-labels-source",
|
|
||||||
layout: {
|
|
||||||
"text-field": ["get", "label"],
|
|
||||||
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
|
|
||||||
"text-offset": [0, 0],
|
|
||||||
"text-anchor": "top-left",
|
|
||||||
"text-rotation-alignment": "map",
|
|
||||||
},
|
},
|
||||||
paint: {
|
properties: { value: x },
|
||||||
"text-halo-width": 1,
|
});
|
||||||
"text-halo-blur": 3,
|
}
|
||||||
"text-halo-color": "rgba(255,255,255,0.8)",
|
for (let y = 0; y < 128; y += 1) {
|
||||||
|
let lat = map.current.unproject([-180, y * cellSize + viewportNW.y]).lat;
|
||||||
|
graticule.features.push({
|
||||||
|
type: "Feature",
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: [
|
||||||
|
[-180, lat],
|
||||||
|
[180, lat],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
minzoom: 4,
|
properties: { value: y },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const grid: GeoJSON.FeatureCollection<
|
map.current.addSource("graticule", {
|
||||||
GeoJSON.Geometry,
|
type: "geojson",
|
||||||
GeoJSON.GeoJsonProperties
|
data: graticule,
|
||||||
> = {
|
|
||||||
type: "FeatureCollection",
|
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
for (let x = 0; x < 128; x += 1) {
|
|
||||||
for (let y = 0; y < 128; y += 1) {
|
|
||||||
let nw = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x,
|
|
||||||
y * cellSize + viewportNW.y,
|
|
||||||
]);
|
|
||||||
let ne = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x + cellSize,
|
|
||||||
y * cellSize + viewportNW.y,
|
|
||||||
]);
|
|
||||||
let se = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x + cellSize,
|
|
||||||
y * cellSize + viewportNW.y + cellSize,
|
|
||||||
]);
|
|
||||||
let sw = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x,
|
|
||||||
y * cellSize + viewportNW.y + cellSize,
|
|
||||||
]);
|
|
||||||
const editCount = (data as Record<string, number>)[
|
|
||||||
`${x - 57},${50 - y}`
|
|
||||||
];
|
|
||||||
grid.features.push({
|
|
||||||
type: "Feature",
|
|
||||||
id: x * 100 + y,
|
|
||||||
geometry: {
|
|
||||||
type: "Polygon",
|
|
||||||
coordinates: [
|
|
||||||
[
|
|
||||||
[nw.lng, nw.lat],
|
|
||||||
[ne.lng, ne.lat],
|
|
||||||
[se.lng, se.lat],
|
|
||||||
[sw.lng, sw.lat],
|
|
||||||
[nw.lng, nw.lat],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
cellX: x - 57,
|
|
||||||
cellY: 50 - y,
|
|
||||||
label: `${x - 57}, ${50 - y}`,
|
|
||||||
color: editCount ? colorGradient.getColor(editCount) : "#888888",
|
|
||||||
opacity: editCount ? Math.min((editCount / 150) * 0.25, 0.5) : 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.current.addSource("grid-source", {
|
|
||||||
type: "geojson",
|
|
||||||
data: grid,
|
|
||||||
});
|
|
||||||
|
|
||||||
map.current.addLayer(
|
|
||||||
{
|
|
||||||
id: "grid-layer",
|
|
||||||
type: "fill",
|
|
||||||
source: "grid-source",
|
|
||||||
paint: {
|
|
||||||
"fill-opacity": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"grid-labels-layer"
|
|
||||||
);
|
|
||||||
|
|
||||||
map.current.addLayer(
|
|
||||||
{
|
|
||||||
id: "heatmap-layer",
|
|
||||||
type: "fill",
|
|
||||||
source: "grid-source",
|
|
||||||
paint: {
|
|
||||||
"fill-color": ["get", "color"],
|
|
||||||
"fill-opacity": ["get", "opacity"],
|
|
||||||
"fill-outline-color": [
|
|
||||||
"case",
|
|
||||||
["boolean", ["feature-state", "selected"], false],
|
|
||||||
"white",
|
|
||||||
"transparent",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"grid-labels-layer"
|
|
||||||
);
|
|
||||||
const fullscreenControl = new mapboxgl.FullscreenControl();
|
|
||||||
console.log(
|
|
||||||
(fullscreenControl as unknown as { _container: HTMLElement })._container
|
|
||||||
);
|
|
||||||
(fullscreenControl as unknown as { _container: HTMLElement })._container =
|
|
||||||
mapWrapper.current;
|
|
||||||
console.log(
|
|
||||||
(fullscreenControl as unknown as { _container: HTMLElement })._container
|
|
||||||
);
|
|
||||||
map.current.addControl(fullscreenControl);
|
|
||||||
map.current.addControl(new mapboxgl.NavigationControl());
|
|
||||||
|
|
||||||
let singleClickTimeout: NodeJS.Timeout | null = null;
|
|
||||||
map.current.on("click", "grid-layer", (e) => {
|
|
||||||
console.log("click");
|
|
||||||
const features = e.features;
|
|
||||||
if (singleClickTimeout) return;
|
|
||||||
singleClickTimeout = setTimeout(() => {
|
|
||||||
singleClickTimeout = null;
|
|
||||||
if (features && features[0]) {
|
|
||||||
console.log("timeout");
|
|
||||||
const cell: [number, number] = [
|
|
||||||
features[0].properties!.cellX,
|
|
||||||
features[0].properties!.cellY,
|
|
||||||
];
|
|
||||||
map.current.removeFeatureState({ source: "grid-source" });
|
|
||||||
map.current.setFeatureState(
|
|
||||||
{
|
|
||||||
source: "grid-source",
|
|
||||||
id: features[0].id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selected: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setSelectedCell(cell);
|
|
||||||
map.current.resize();
|
|
||||||
|
|
||||||
var zoom = map.current.getZoom();
|
|
||||||
var viewportNW = map.current.project([-180, 85.051129]);
|
|
||||||
var cellSize = Math.pow(2, zoom + 2);
|
|
||||||
const x = features[0].properties!.x;
|
|
||||||
const y = features[0].properties!.y;
|
|
||||||
let nw = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x,
|
|
||||||
y * cellSize + viewportNW.y,
|
|
||||||
]);
|
|
||||||
let ne = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x + cellSize,
|
|
||||||
y * cellSize + viewportNW.y,
|
|
||||||
]);
|
|
||||||
let se = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x + cellSize,
|
|
||||||
y * cellSize + viewportNW.y + cellSize,
|
|
||||||
]);
|
|
||||||
let sw = map.current.unproject([
|
|
||||||
x * cellSize + viewportNW.x,
|
|
||||||
y * cellSize + viewportNW.y + cellSize,
|
|
||||||
]);
|
|
||||||
const selectedCellLines: GeoJSON.FeatureCollection<
|
|
||||||
GeoJSON.Geometry,
|
|
||||||
GeoJSON.GeoJsonProperties
|
|
||||||
> = {
|
|
||||||
type: "FeatureCollection",
|
|
||||||
features: [
|
|
||||||
{
|
|
||||||
type: "Feature",
|
|
||||||
geometry: {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: [
|
|
||||||
[nw.lng, nw.lat],
|
|
||||||
[ne.lng, ne.lat],
|
|
||||||
[se.lng, se.lat],
|
|
||||||
[sw.lng, sw.lat],
|
|
||||||
[nw.lng, nw.lat],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: { x: x, y: y },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (map.current.getLayer("selected-cell-layer")) {
|
|
||||||
map.current.removeLayer("selected-cell-layer");
|
|
||||||
}
|
|
||||||
if (map.current.getSource("selected-cell-source")) {
|
|
||||||
map.current.removeSource("selected-cell-source");
|
|
||||||
}
|
|
||||||
map.current.addSource("selected-cell-source", {
|
|
||||||
type: "geojson",
|
|
||||||
data: selectedCellLines,
|
|
||||||
});
|
|
||||||
map.current.addLayer({
|
|
||||||
id: "selected-cell-layer",
|
|
||||||
type: "line",
|
|
||||||
source: "selected-cell-source",
|
|
||||||
paint: {
|
|
||||||
"line-color": "blue",
|
|
||||||
"line-width": 3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bounds = map.current.getBounds();
|
|
||||||
if (!bounds.contains(nw) || !bounds.contains(se)) {
|
|
||||||
map.current.panTo(nw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.current.on("dblclick", "grid-layer", (e) => {
|
|
||||||
if (singleClickTimeout) clearTimeout(singleClickTimeout);
|
|
||||||
singleClickTimeout = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
map.current.on("idle", () => {
|
|
||||||
map.current.resize();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}, [setSelectedCell, data]);
|
|
||||||
|
map.current.addLayer({
|
||||||
|
id: "graticule",
|
||||||
|
type: "line",
|
||||||
|
source: "graticule",
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridLabelPoints: GeoJSON.FeatureCollection<
|
||||||
|
GeoJSON.Geometry,
|
||||||
|
GeoJSON.GeoJsonProperties
|
||||||
|
> = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [],
|
||||||
|
};
|
||||||
|
for (let x = 0; x < 128; x += 1) {
|
||||||
|
for (let y = 0; y < 128; y += 1) {
|
||||||
|
let nw = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x + cellSize / 32,
|
||||||
|
y * cellSize + viewportNW.y + cellSize / 32,
|
||||||
|
]);
|
||||||
|
gridLabelPoints.features.push({
|
||||||
|
type: "Feature",
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [nw.lng, nw.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
label: `${x - 57}, ${50 - y}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.current.addSource("grid-labels-source", {
|
||||||
|
type: "geojson",
|
||||||
|
data: gridLabelPoints,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.current.addLayer({
|
||||||
|
id: "grid-labels-layer",
|
||||||
|
type: "symbol",
|
||||||
|
source: "grid-labels-source",
|
||||||
|
layout: {
|
||||||
|
"text-field": ["get", "label"],
|
||||||
|
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
|
||||||
|
"text-offset": [0, 0],
|
||||||
|
"text-anchor": "top-left",
|
||||||
|
"text-rotation-alignment": "map",
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
"text-halo-width": 1,
|
||||||
|
"text-halo-blur": 3,
|
||||||
|
"text-halo-color": "rgba(255,255,255,0.8)",
|
||||||
|
},
|
||||||
|
minzoom: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
const grid: GeoJSON.FeatureCollection<
|
||||||
|
GeoJSON.Geometry,
|
||||||
|
GeoJSON.GeoJsonProperties
|
||||||
|
> = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [],
|
||||||
|
};
|
||||||
|
for (let x = 0; x < 128; x += 1) {
|
||||||
|
for (let y = 0; y < 128; y += 1) {
|
||||||
|
let nw = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x,
|
||||||
|
y * cellSize + viewportNW.y,
|
||||||
|
]);
|
||||||
|
let ne = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x + cellSize,
|
||||||
|
y * cellSize + viewportNW.y,
|
||||||
|
]);
|
||||||
|
let se = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x + cellSize,
|
||||||
|
y * cellSize + viewportNW.y + cellSize,
|
||||||
|
]);
|
||||||
|
let sw = map.current.unproject([
|
||||||
|
x * cellSize + viewportNW.x,
|
||||||
|
y * cellSize + viewportNW.y + cellSize,
|
||||||
|
]);
|
||||||
|
const editCount = (data as Record<string, number>)[
|
||||||
|
`${x - 57},${50 - y}`
|
||||||
|
];
|
||||||
|
grid.features.push({
|
||||||
|
type: "Feature",
|
||||||
|
id: x * 100 + y,
|
||||||
|
geometry: {
|
||||||
|
type: "Polygon",
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[nw.lng, nw.lat],
|
||||||
|
[ne.lng, ne.lat],
|
||||||
|
[se.lng, se.lat],
|
||||||
|
[sw.lng, sw.lat],
|
||||||
|
[nw.lng, nw.lat],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
cellX: x - 57,
|
||||||
|
cellY: 50 - y,
|
||||||
|
label: `${x - 57}, ${50 - y}`,
|
||||||
|
color: editCount ? colorGradient.getColor(editCount) : "#888888",
|
||||||
|
opacity: editCount ? Math.min((editCount / 150) * 0.25, 0.5) : 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.current.addSource("grid-source", {
|
||||||
|
type: "geojson",
|
||||||
|
data: grid,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.current.addLayer(
|
||||||
|
{
|
||||||
|
id: "grid-layer",
|
||||||
|
type: "fill",
|
||||||
|
source: "grid-source",
|
||||||
|
paint: {
|
||||||
|
"fill-opacity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"grid-labels-layer"
|
||||||
|
);
|
||||||
|
|
||||||
|
map.current.addLayer(
|
||||||
|
{
|
||||||
|
id: "heatmap-layer",
|
||||||
|
type: "fill",
|
||||||
|
source: "grid-source",
|
||||||
|
paint: {
|
||||||
|
"fill-color": ["get", "color"],
|
||||||
|
"fill-opacity": ["get", "opacity"],
|
||||||
|
"fill-outline-color": [
|
||||||
|
"case",
|
||||||
|
["boolean", ["feature-state", "selected"], false],
|
||||||
|
"white",
|
||||||
|
"transparent",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"grid-labels-layer"
|
||||||
|
);
|
||||||
|
const fullscreenControl = new mapboxgl.FullscreenControl();
|
||||||
|
(fullscreenControl as unknown as { _container: HTMLElement })._container =
|
||||||
|
mapWrapper.current;
|
||||||
|
map.current.addControl(fullscreenControl);
|
||||||
|
map.current.addControl(new mapboxgl.NavigationControl());
|
||||||
|
|
||||||
|
let singleClickTimeout: NodeJS.Timeout | null = null;
|
||||||
|
map.current.on("click", "grid-layer", (e) => {
|
||||||
|
const features = e.features;
|
||||||
|
if (singleClickTimeout) return;
|
||||||
|
singleClickTimeout = setTimeout(() => {
|
||||||
|
singleClickTimeout = null;
|
||||||
|
if (features && features[0]) {
|
||||||
|
const cell = {
|
||||||
|
x: features[0].properties!.cellX,
|
||||||
|
y: features[0].properties!.cellY,
|
||||||
|
};
|
||||||
|
router.push({ query: { cell: cell.x + "," + cell.y } });
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.current.on("dblclick", "grid-layer", (e) => {
|
||||||
|
if (singleClickTimeout) clearTimeout(singleClickTimeout);
|
||||||
|
singleClickTimeout = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
setHeatmapLoaded(true);
|
||||||
|
}, [data, mapLoaded, router, setHeatmapLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${styles["map-wrapper"]} ${
|
className={`${styles["map-wrapper"]} ${
|
||||||
selectedCell ? styles["map-wrapper-sidebar-open"] : ""
|
sidebarOpen ? styles["map-wrapper-sidebar-open"] : ""
|
||||||
}`}
|
}`}
|
||||||
ref={mapWrapper}
|
ref={mapWrapper}
|
||||||
>
|
>
|
||||||
<div ref={mapContainer} className={styles["map-container"]}>
|
<div ref={mapContainer} className={styles["map-container"]}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
selectedCell={selectedCell}
|
selectedCell={selectedCell}
|
||||||
setSelectedCell={setSelectedCell}
|
clearSelectedCell={() => router.push({ query: {} })}
|
||||||
map={map}
|
map={map}
|
||||||
/>
|
/>
|
||||||
<ToggleLayersControl map={map} />
|
<ToggleLayersControl map={map} />
|
||||||
|
<SearchBar
|
||||||
|
map={map}
|
||||||
|
clearSelectedCell={() => router.push({ query: {} })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
113
components/SearchBar.tsx
Normal file
113
components/SearchBar.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import type mapboxgl from "mapbox-gl";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
|
import styles from "../styles/SearchBar.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
clearSelectedCell: () => void;
|
||||||
|
map: React.MutableRefObject<mapboxgl.Map>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Mod {
|
||||||
|
title: string;
|
||||||
|
nexus_mod_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
item: Mod;
|
||||||
|
refIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list: Mod[] = [
|
||||||
|
{ title: "Unofficial Skyrim Special Edition Patch", nexus_mod_id: 1 },
|
||||||
|
{ title: "Enhanced Lights and FX", nexus_mod_id: 2 },
|
||||||
|
{ title: "Majestic Mountains", nexus_mod_id: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const fuse = new Fuse(list, { keys: ["title"] });
|
||||||
|
|
||||||
|
const searchInput = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
const [searchFocused, setSearchFocused] = useState<boolean>(false);
|
||||||
|
const [clickingResult, setClickingResult] = useState<boolean>(false);
|
||||||
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchInput.current) {
|
||||||
|
if (
|
||||||
|
searchFocused &&
|
||||||
|
global.document.activeElement !== searchInput.current
|
||||||
|
) {
|
||||||
|
searchInput.current.focus();
|
||||||
|
} else if (
|
||||||
|
!searchFocused &&
|
||||||
|
global.document.activeElement === searchInput.current
|
||||||
|
) {
|
||||||
|
searchInput.current.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchFocused]);
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
const results: { item: Mod; refIndex: number }[] = fuse.search(
|
||||||
|
e.target.value
|
||||||
|
);
|
||||||
|
setResults(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChooseResult =
|
||||||
|
(item: Mod) =>
|
||||||
|
(e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => {
|
||||||
|
clearSelectedCell();
|
||||||
|
router.push({ query: { mod: item.nexus_mod_id } });
|
||||||
|
setSearch("");
|
||||||
|
setResults([]);
|
||||||
|
setClickingResult(false);
|
||||||
|
setSearchFocused(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles["search-bar"]} ${
|
||||||
|
searchFocused ? styles["search-bar-focused"] : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search mods or cells…"
|
||||||
|
onChange={onChange}
|
||||||
|
onFocus={() => setSearchFocused(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!clickingResult) {
|
||||||
|
setSearch("");
|
||||||
|
setResults([]);
|
||||||
|
setSearchFocused(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={search}
|
||||||
|
ref={searchInput}
|
||||||
|
/>
|
||||||
|
{results.length > 0 && (
|
||||||
|
<ul className={styles["search-results"]}>
|
||||||
|
{results.map((result) => (
|
||||||
|
<li
|
||||||
|
key={result.item.nexus_mod_id}
|
||||||
|
onClick={onChooseResult(result.item)}
|
||||||
|
onTouchStart={() => setClickingResult(true)}
|
||||||
|
onMouseDown={() => setClickingResult(true)}
|
||||||
|
>
|
||||||
|
{result.item.title}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBar;
|
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import CellData from "./CellData";
|
import CellData from "./CellData";
|
||||||
import styles from "../styles/Sidebar.module.css";
|
import styles from "../styles/Sidebar.module.css";
|
||||||
@ -10,39 +11,42 @@ interface Cell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedCell: [number, number] | null;
|
selectedCell: { x: number; y: number } | null;
|
||||||
setSelectedCell: (cell: [number, number] | null) => void;
|
clearSelectedCell: () => void;
|
||||||
map: React.MutableRefObject<mapboxgl.Map | null>;
|
map: React.MutableRefObject<mapboxgl.Map | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<Props> = ({ selectedCell, setSelectedCell, map }) => {
|
const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, map }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setSelectedCell(null);
|
clearSelectedCell();
|
||||||
if (map.current) map.current.removeFeatureState({ source: "grid-source" });
|
|
||||||
if (map.current && map.current.getLayer("selected-cell-layer")) {
|
|
||||||
map.current.removeLayer("selected-cell-layer");
|
|
||||||
}
|
|
||||||
if (map.current && map.current.getSource("selected-cell-source")) {
|
|
||||||
map.current.removeSource("selected-cell-source");
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (map.current) map.current.resize();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (selectedCell) {
|
||||||
selectedCell && (
|
return (
|
||||||
<div className={styles.sidebar}>
|
<div className={styles.sidebar}>
|
||||||
<button className={styles.close} onClick={onClose}>
|
<button className={styles.close} onClick={onClose}>
|
||||||
✖
|
✖
|
||||||
</button>
|
</button>
|
||||||
<h1>
|
<h1>
|
||||||
Cell {selectedCell[0]}, {selectedCell[1]}
|
Cell {selectedCell.x}, {selectedCell.y}
|
||||||
</h1>
|
</h1>
|
||||||
{selectedCell && <CellData selectedCell={selectedCell} />}
|
{selectedCell && <CellData selectedCell={selectedCell} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
);
|
} else if (router.query.mod) {
|
||||||
|
return (
|
||||||
|
<div className={styles.sidebar}>
|
||||||
|
<button className={styles.close} onClick={onClose}>
|
||||||
|
✖
|
||||||
|
</button>
|
||||||
|
<h1>Mod {router.query.mod}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
import type mapboxgl from "mapbox-gl";
|
import type mapboxgl from "mapbox-gl";
|
||||||
|
|
||||||
import styles from "../styles/ToggleLayersControl.module.css";
|
import styles from "../styles/ToggleLayersControl.module.css";
|
||||||
@ -40,8 +41,15 @@ const ToggleLayersControl: React.FC<Props> = ({ map }) => {
|
|||||||
}
|
}
|
||||||
}, [map, labelsVisible]);
|
}, [map, labelsVisible]);
|
||||||
|
|
||||||
return (
|
let controlContainer;
|
||||||
<div className="mapboxgl-ctrl-top-left">
|
if (global.document) {
|
||||||
|
controlContainer = global.document.querySelector(
|
||||||
|
".mapboxgl-control-container .mapboxgl-ctrl-top-left"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlContainer) {
|
||||||
|
return ReactDOM.createPortal(
|
||||||
<div className="mapboxgl-ctrl mapboxgl-ctrl-group">
|
<div className="mapboxgl-ctrl mapboxgl-ctrl-group">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -73,9 +81,11 @@ const ToggleLayersControl: React.FC<Props> = ({ map }) => {
|
|||||||
>
|
>
|
||||||
<span className="mapboxgl-ctrl-icon" />
|
<span className="mapboxgl-ctrl-icon" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>,
|
||||||
</div>
|
controlContainer
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToggleLayersControl;
|
export default ToggleLayersControl;
|
||||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -309,6 +309,15 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"version": "17.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
|
||||||
|
"integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
@ -1201,6 +1210,11 @@
|
|||||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fuse.js": {
|
||||||
|
"version": "6.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
|
||||||
|
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg=="
|
||||||
|
},
|
||||||
"geojson-vt": {
|
"geojson-vt": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"@types/javascript-color-gradient": "^1.3.0",
|
"@types/javascript-color-gradient": "^1.3.0",
|
||||||
"@types/mapbox-gl": "^2.6.0",
|
"@types/mapbox-gl": "^2.6.0",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
|
"fuse.js": "^6.5.3",
|
||||||
"javascript-color-gradient": "^1.3.2",
|
"javascript-color-gradient": "^1.3.2",
|
||||||
"mapbox-gl": "^2.6.1",
|
"mapbox-gl": "^2.6.1",
|
||||||
"next": "12.0.8",
|
"next": "12.0.8",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "17.0.8",
|
"@types/node": "17.0.8",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
"eslint": "8.6.0",
|
"eslint": "8.6.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
"typescript": "4.5.4"
|
"typescript": "4.5.4"
|
||||||
|
46
styles/SearchBar.module.css
Normal file
46
styles/SearchBar.module.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.search-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 8px;
|
||||||
|
width: 150px;
|
||||||
|
left: calc(50% - 75px);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar.search-bar-focused {
|
||||||
|
width: max(40vw, 250px);
|
||||||
|
left: calc(50% - max(20vw, 125px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
width: 150px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar.search-bar.search-bar-focused input {
|
||||||
|
width: max(40vw, 250px);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
background-color: white;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
list-style-type: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results li {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-top: 1px solid #222222;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results li:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
@ -20,6 +20,8 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
height: 45%;
|
height: 45%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
border-top: 1px solid #222222;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user