Merge pull request #110 from TheRealOwenRees/constants
Various tidy ups :)
@@ -5,6 +5,7 @@
|
|||||||
"colours",
|
"colours",
|
||||||
"contactez",
|
"contactez",
|
||||||
"defaulticon",
|
"defaulticon",
|
||||||
|
"Depfu",
|
||||||
"Échecs",
|
"Échecs",
|
||||||
"Fédération",
|
"Fédération",
|
||||||
"Florifourchette",
|
"Florifourchette",
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ and is deployed on [Vercel](https://vercel.com/)
|
|||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Contributions are encouraged. Please open an issue to discuss your ideas.
|
Contributions are encouraged. Please open an issue to discuss your ideas.
|
||||||
Add your code into a feature branch such as `feature/feature-name`. We utilise a [GitHub Flow](https://www.gitkraken.com/learn/git/best-practices/git-branch-strategy#github-flow-considerations) branching strategy.
|
Add your code into a feature branch such as `feature/feature-name`. We use the [GitHub Flow](https://www.gitkraken.com/learn/git/best-practices/git-branch-strategy#github-flow-considerations) branching strategy.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAtomValue } from "jotai";
|
|||||||
|
|
||||||
import { TimeControl } from "@/types";
|
import { TimeControl } from "@/types";
|
||||||
import { filteredTournamentsByTimeControlAtom } from "@/app/atoms";
|
import { filteredTournamentsByTimeControlAtom } from "@/app/atoms";
|
||||||
|
import { TimeControlColours } from "@/app/constants";
|
||||||
|
|
||||||
const Legend = () => {
|
const Legend = () => {
|
||||||
const t = useTranslations("Tournaments");
|
const t = useTranslations("Tournaments");
|
||||||
@@ -15,11 +16,11 @@ const Legend = () => {
|
|||||||
const timeControls = useMemo(
|
const timeControls = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
{ tc: TimeControl.Classic, colour: "#00ac39" },
|
TimeControl.Classic,
|
||||||
{ tc: TimeControl.Rapid, colour: "#0086c7" },
|
TimeControl.Rapid,
|
||||||
{ tc: TimeControl.Blitz, colour: "#cec348" },
|
TimeControl.Blitz,
|
||||||
{ tc: TimeControl.Other, colour: "#d10c3e" },
|
TimeControl.Other,
|
||||||
].filter(({ tc }) => tournaments.some((t) => t.timeControl === tc)),
|
].filter((tc) => tournaments.some((t) => t.timeControl === tc)),
|
||||||
[tournaments],
|
[tournaments],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -39,12 +40,11 @@ const Legend = () => {
|
|||||||
<ul>
|
<ul>
|
||||||
${timeControls
|
${timeControls
|
||||||
.map(
|
.map(
|
||||||
({ tc, colour }) => `
|
(tc) => `
|
||||||
<li>
|
<li>
|
||||||
<span class="block h-4 w-7 border border-[#999] float-left mr-1" style="background: ${colour}"></span>${t(
|
<span class="block h-4 w-7 border border-[#999] float-left mr-1" style="background: ${
|
||||||
"timeControlEnum",
|
TimeControlColours[tc]
|
||||||
{ tc },
|
}"></span>${t("timeControlEnum", { tc })}
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ import {
|
|||||||
filteredTournamentsByTimeControlAtom,
|
filteredTournamentsByTimeControlAtom,
|
||||||
normsOnlyAtom,
|
normsOnlyAtom,
|
||||||
} from "@/app/atoms";
|
} from "@/app/atoms";
|
||||||
import { pie } from "@/lib/pie";
|
import { generatePieSVG } from "@/lib/pie";
|
||||||
|
import { TimeControlColours } from "@/app/constants";
|
||||||
|
|
||||||
import Legend from "./Legend";
|
import Legend from "./Legend";
|
||||||
import { TournamentMarker } from "./TournamentMarker";
|
import { TournamentMarker, TournamentMarkerRef } from "./TournamentMarker";
|
||||||
import TimeControlFilters from "./TimeControlFilters";
|
import TimeControlFilters from "./TimeControlFilters";
|
||||||
|
|
||||||
// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing
|
// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing
|
||||||
@@ -99,7 +100,7 @@ export default function TournamentMap() {
|
|||||||
debouncedHoveredListTournamentIdAtom,
|
debouncedHoveredListTournamentIdAtom,
|
||||||
);
|
);
|
||||||
|
|
||||||
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
|
const markerRefs = useRef<Record<string, TournamentMarkerRef>>({});
|
||||||
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||||
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
||||||
|
|
||||||
@@ -133,7 +134,9 @@ export default function TournamentMap() {
|
|||||||
const markerRef = markerRefs.current[tournament.groupId];
|
const markerRef = markerRefs.current[tournament.groupId];
|
||||||
if (markerRef) {
|
if (markerRef) {
|
||||||
if (clusterRef.current) {
|
if (clusterRef.current) {
|
||||||
const visibleMarker = clusterRef.current.getVisibleParent(markerRef);
|
const visibleMarker = clusterRef.current.getVisibleParent(
|
||||||
|
markerRef.getMarker(),
|
||||||
|
);
|
||||||
if (!visibleMarker) return;
|
if (!visibleMarker) return;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -162,7 +165,7 @@ export default function TournamentMap() {
|
|||||||
if (!marker.isBouncing()) {
|
if (!marker.isBouncing()) {
|
||||||
stopBouncingMarkers();
|
stopBouncingMarkers();
|
||||||
|
|
||||||
marker.bounce();
|
markerRef.bounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,11 +188,9 @@ export default function TournamentMap() {
|
|||||||
if (!tournament) return;
|
if (!tournament) return;
|
||||||
|
|
||||||
expandedClusterMarkerRef.current = e.cluster;
|
expandedClusterMarkerRef.current = e.cluster;
|
||||||
const markerRef = markerRefs.current[
|
const markerRef = markerRefs.current[tournament.groupId];
|
||||||
tournament.groupId
|
|
||||||
] as BouncingMarker;
|
|
||||||
|
|
||||||
if (markerRef && e.markers.includes(markerRef)) {
|
if (markerRef && e.markers.includes(markerRef.getMarker())) {
|
||||||
stopBouncingMarkers();
|
stopBouncingMarkers();
|
||||||
markerRef.bounce();
|
markerRef.bounce();
|
||||||
}
|
}
|
||||||
@@ -234,37 +235,30 @@ export default function TournamentMap() {
|
|||||||
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
||||||
|
|
||||||
const createClusterCustomIcon = useCallback((cluster: any) => {
|
const createClusterCustomIcon = useCallback((cluster: any) => {
|
||||||
let childCount = cluster.getChildCount();
|
const childCount = cluster.getChildCount();
|
||||||
|
|
||||||
const children = cluster.getAllChildMarkers();
|
const children = cluster.getAllChildMarkers();
|
||||||
|
|
||||||
// We added the time control to the icon options when creating the marker
|
// We added the time control as the class name when creating the marker
|
||||||
const timeControlCounts = countBy(
|
const timeControlCounts = countBy(
|
||||||
children,
|
children,
|
||||||
(child: any) => child.options.icon.options.timeControl,
|
(child: any) => child.options.icon.options.className,
|
||||||
);
|
);
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div>
|
<div>
|
||||||
|
${generatePieSVG(
|
||||||
${pie("absolute w-[30px] -z-10", 15, [
|
"absolute w-[30px]",
|
||||||
{
|
15,
|
||||||
value: timeControlCounts[TimeControl.Classic] ?? 0,
|
[
|
||||||
colour: "#00ac39",
|
TimeControl.Classic,
|
||||||
},
|
TimeControl.Rapid,
|
||||||
{
|
TimeControl.Blitz,
|
||||||
value: timeControlCounts[TimeControl.Rapid] ?? 0,
|
TimeControl.Other,
|
||||||
colour: "#0086c7",
|
].map((tc) => ({
|
||||||
},
|
value: timeControlCounts[tc] ?? 0,
|
||||||
{
|
colour: TimeControlColours[tc],
|
||||||
value: timeControlCounts[TimeControl.Blitz] ?? 0,
|
})),
|
||||||
colour: "#cec348",
|
)}
|
||||||
},
|
|
||||||
{
|
|
||||||
value: timeControlCounts[TimeControl.Other] ?? 0,
|
|
||||||
colour: "#d10c3e",
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
<span class="text-white font-semibold relative z-[300]">${childCount}</span>
|
<span class="text-white font-semibold relative z-[300]">${childCount}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -290,10 +284,11 @@ export default function TournamentMap() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TournamentMarker
|
<TournamentMarker
|
||||||
ref={(ref) => (markerRefs.current[groupId] = ref)}
|
ref={(ref) => {
|
||||||
|
markerRefs.current[groupId] = ref!;
|
||||||
|
}}
|
||||||
key={groupId}
|
key={groupId}
|
||||||
tournamentGroup={tournamentGroup}
|
tournamentGroup={tournamentGroup}
|
||||||
colour={colours[timeControl]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { forwardRef, useMemo, useRef } from "react";
|
import { forwardRef, useMemo, useImperativeHandle, useRef } from "react";
|
||||||
import { TimeControl, Tournament } from "@/types";
|
import { Tournament } from "@/types";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import { Marker, Popup, MarkerProps } from "react-leaflet";
|
import { Marker, Popup, MarkerProps } from "react-leaflet";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -9,18 +9,38 @@ import { useSetAtom } from "jotai";
|
|||||||
import { FaTrophy } from "react-icons/fa";
|
import { FaTrophy } from "react-icons/fa";
|
||||||
import { last } from "lodash";
|
import { last } from "lodash";
|
||||||
|
|
||||||
|
import type { BouncingMarker } from "@/leafletTypes";
|
||||||
import { debouncedHoveredMapTournamentGroupIdAtom } from "@/app/atoms";
|
import { debouncedHoveredMapTournamentGroupIdAtom } from "@/app/atoms";
|
||||||
|
import { TimeControlColours } from "@/app/constants";
|
||||||
|
|
||||||
|
export type TournamentMarkerRef = {
|
||||||
|
getMarker: () => L.Marker<any>;
|
||||||
|
bounce: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type TournamentMarkerProps = {
|
type TournamentMarkerProps = {
|
||||||
tournamentGroup: Tournament[];
|
tournamentGroup: Tournament[];
|
||||||
colour: string;
|
|
||||||
} & Omit<MarkerProps, "position">;
|
} & Omit<MarkerProps, "position">;
|
||||||
|
|
||||||
export const TournamentMarker = forwardRef<
|
export const TournamentMarker = forwardRef<
|
||||||
L.Marker<any> | null,
|
TournamentMarkerRef,
|
||||||
TournamentMarkerProps
|
TournamentMarkerProps
|
||||||
>(({ tournamentGroup, colour, ...markerProps }, ref) => {
|
>(({ tournamentGroup, ...markerProps }, ref) => {
|
||||||
const t = useTranslations("Tournaments");
|
const t = useTranslations("Tournaments");
|
||||||
|
const markerRef = useRef<L.Marker<any> | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getMarker: () => markerRef.current!,
|
||||||
|
bounce: () => {
|
||||||
|
const bouncingMarker = markerRef.current as BouncingMarker;
|
||||||
|
bouncingMarker.setBouncingOptions({ contractHeight: 0 });
|
||||||
|
bouncingMarker.bounce();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const { date, latLng, groupId, timeControl } = tournamentGroup[0];
|
const { date, latLng, groupId, timeControl } = tournamentGroup[0];
|
||||||
|
|
||||||
@@ -30,16 +50,18 @@ export const TournamentMarker = forwardRef<
|
|||||||
|
|
||||||
const iconOptions = useMemo(
|
const iconOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new L.Icon({
|
new L.DivIcon({
|
||||||
iconUrl: `/images/leaflet/marker-icon-2x-${colour}.png`,
|
html: `
|
||||||
shadowUrl: "/images/leaflet/marker-shadow.png",
|
<svg x="0px" y="0px" viewBox="0 0 365 560" enable-background="new 0 0 365 560" xml:space="preserve">
|
||||||
iconSize: [25, 41],
|
<g><path stroke="#666666" stroke-width="10" fill="${TimeControlColours[timeControl]}" d="M182.9,551.7c0,0.1,0.2,0.3,0.2,0.3S358.3,283,358.3,194.6c0-130.1-88.8-186.7-175.4-186.9 C96.3,7.9,7.5,64.5,7.5,194.6c0,88.4,175.3,357.4,175.3,357.4S182.9,551.7,182.9,551.7z M122.2,187.2c0-33.6,27.2-60.8,60.8-60.8 c33.6,0,60.8,27.2,60.8,60.8S216.5,248,182.9,248C149.4,248,122.2,220.8,122.2,187.2z"/></g>
|
||||||
iconAnchor: [12, 41],
|
</svg>
|
||||||
popupAnchor: [1, -34],
|
`,
|
||||||
shadowSize: [41, 41],
|
className: timeControl,
|
||||||
timeControl,
|
iconSize: [24, 40],
|
||||||
|
iconAnchor: [12, 40],
|
||||||
|
popupAnchor: [1, -40],
|
||||||
}),
|
}),
|
||||||
[colour, timeControl],
|
[timeControl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startDate = date;
|
const startDate = date;
|
||||||
@@ -47,7 +69,7 @@ export const TournamentMarker = forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
ref={ref}
|
ref={markerRef}
|
||||||
position={latLng}
|
position={latLng}
|
||||||
icon={iconOptions}
|
icon={iconOptions}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { TimeControl } from "@/types"
|
||||||
|
|
||||||
|
export const TimeControlColours = {
|
||||||
|
[TimeControl.Classic]: "#00ac39",
|
||||||
|
[TimeControl.Rapid]: "#0086c7",
|
||||||
|
[TimeControl.Blitz]: "#ddce20",
|
||||||
|
[TimeControl.Other]: "#ea5f17",
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Marker } from "leaflet";
|
||||||
|
|
||||||
|
declare class BouncingOptions {
|
||||||
|
bounceHeight: number;
|
||||||
|
contractHeight: number;
|
||||||
|
bounceSpeed: number;
|
||||||
|
contractSpeed: number;
|
||||||
|
shadowAngle: number;
|
||||||
|
elastic: number;
|
||||||
|
exclusive: number;
|
||||||
|
|
||||||
|
constructor(options: Partial<BouncingOptions>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing
|
||||||
|
// to keep Typescript happy
|
||||||
|
export declare class BouncingMarker extends Marker {
|
||||||
|
isBouncing(): boolean;
|
||||||
|
bounce(): void;
|
||||||
|
stopBouncing(): void;
|
||||||
|
getBouncingMarkers(): Marker[];
|
||||||
|
setBouncingOptions(options: BouncingOptions | Partial<BouncingOptions>): void;
|
||||||
|
stopAllBouncingMarkers(): void;
|
||||||
|
|
||||||
|
_icon: HTMLElement;
|
||||||
|
_bouncingMotion?: {
|
||||||
|
bouncingAnimationPlaying: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,9 +14,7 @@ function polarToCartesian(radius: number, angleInDegrees: number) {
|
|||||||
function getDAttribute(radius: number, startAngle: number, endAngle: number) {
|
function getDAttribute(radius: number, startAngle: number, endAngle: number) {
|
||||||
const isCircle = endAngle - startAngle === 360;
|
const isCircle = endAngle - startAngle === 360;
|
||||||
|
|
||||||
if (isCircle) {
|
if (isCircle) endAngle--;
|
||||||
endAngle--;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = polarToCartesian(radius, startAngle);
|
const start = polarToCartesian(radius, startAngle);
|
||||||
const end = polarToCartesian(radius, endAngle);
|
const end = polarToCartesian(radius, endAngle);
|
||||||
@@ -24,14 +22,14 @@ function getDAttribute(radius: number, startAngle: number, endAngle: number) {
|
|||||||
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
|
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
|
||||||
const d = [
|
const d = [
|
||||||
"M", start.x, start.y,
|
"M", start.x, start.y,
|
||||||
"A", radius, radius, 0, largeArcFlag, 1, end.x, end.y];
|
"A", radius, radius, 0, largeArcFlag, 1, end.x, end.y
|
||||||
|
];
|
||||||
|
|
||||||
if (isCircle) {
|
if (isCircle) {
|
||||||
d.push("Z");
|
d.push("Z");
|
||||||
} else {
|
} else {
|
||||||
d.push("L", radius, radius, "L", start.x, start.y, "Z");
|
d.push("L", radius, radius, "L", start.x, start.y, "Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.join(" ");
|
return d.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,35 +41,28 @@ function svg(className: string, width: number, content: string ) {
|
|||||||
return `<svg class="${className}" viewBox="0 0 ${width} ${width}"><g class='pie'>${content}</g></svg>`;
|
return `<svg class="${className}" viewBox="0 0 ${width} ${width}"><g class='pie'>${content}</g></svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PieSector = {
|
type PieSlice = {
|
||||||
value: number;
|
value: number;
|
||||||
colour: string;
|
colour: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pie(className: string, radius: number, values: PieSector[]) {
|
export function generatePieSVG(className: string, radius: number, values: PieSlice[]) {
|
||||||
|
|
||||||
type Sector = {
|
type Sector = {
|
||||||
colour: string;
|
colour: string;
|
||||||
degrees: number;
|
degrees: number;
|
||||||
from?: number;
|
from?: number;
|
||||||
to?: number;
|
to?: number;
|
||||||
path?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = values.reduce((a, b) => a + b.value, 0);
|
const total = values.reduce((a, b) => a + b.value, 0);
|
||||||
const data: Sector[] = values.map(({ value, colour }) => ({ colour, degrees: value / total * 360 }));
|
const data: Sector[] = values.map(({ value, colour }) => ({ colour, degrees: value / total * 360 }));
|
||||||
|
|
||||||
data.forEach((value, i, arr) => {
|
const paths = data.reduce<[number, string][]>((prev, value, i) => {
|
||||||
if (i === 0) {
|
const from = i === 0 ? 0 : prev[i - 1][0];
|
||||||
value.from = 0;
|
const to = from + value.degrees;
|
||||||
value.to = value.degrees;
|
|
||||||
} else {
|
|
||||||
value.from = arr[i - 1].to!;
|
|
||||||
value.to = value.from + value.degrees;
|
|
||||||
}
|
|
||||||
|
|
||||||
value.path = path(getDAttribute(radius, value.from, value.to ), value.colour);
|
return [...prev, [to, path(getDAttribute(radius, from, to ), value.colour)]];
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return svg(className, radius * 2, data.map(o => o.path).join(''));
|
return svg(className, radius * 2, paths.map(([to, path]) => path) .join(''));
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 608 B |