Skip to content
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@acme/api": "workspace:*",
"@acme/api": "workspace:^0.2.0",
"@acme/auth": "workspace:^0.1.0",
"@acme/db": "workspace:^0.1.0",
"@acme/env": "workspace:^0.1.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@acme/api": "workspace:*",
"@acme/api": "workspace:^0.2.0",
"@acme/auth": "workspace:^0.1.0",
"@acme/db": "workspace:^0.1.0",
"@acme/env": "workspace:^0.1.0",
Expand Down
26 changes: 20 additions & 6 deletions apps/map/src/app/_components/map/event-chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { useCallback } from "react";
import type { DayOfWeek } from "@acme/shared/app/enums";
import { cn } from "@acme/ui";

import type { MapStatus } from "~/utils/types";
import { getWhenFromWorkout } from "~/utils/get-when-from-workout";
import { getSelectedChipBg } from "~/utils/map-status-colors";
import { setView } from "~/utils/set-view";
import { setSelectedItem } from "~/utils/store/selected-item";
import BootSvgComponent from "../SVGs/boot-camp";
Expand All @@ -20,6 +22,7 @@ export const EventChip = (props: {
variant?: "interactive" | "non-interactive";
size?: "small" | "medium" | "large";
selected?: boolean;
mapStatus?: MapStatus;
onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
event: {
name?: string;
Expand Down Expand Up @@ -104,8 +107,7 @@ export const EventChip = (props: {
"px-2 shadow",
"cursor-pointer",
{ "pointer-events-none bg-muted": !isInteractive },
{ "bg-red-600": isInteractive && selected },
{ "bg-muted": isInteractive && !selected },
isInteractive && selected && getSelectedChipBg(props.mapStatus ?? null),
{ "gap-1 py-[1px]": size === "small" },
{ "gap-1 py-[2px]": size === "medium" },
{ "gap-2 py-[3px]": size === "large" },
Expand All @@ -116,7 +118,7 @@ export const EventChip = (props: {
<div
className={cn("flex flex-1 gap-2 text-foreground", {
"text-base": size === "large",
"text-background": selected && isInteractive,
"text-background": (selected ?? !!props.mapStatus) && isInteractive,
"justify-start": size === "small",
"justify-center": size !== "small",
})}
Expand All @@ -135,19 +137,31 @@ export const EventChip = (props: {
<BootSvgComponent
height={iconSize}
width={iconSize}
fill={selected && isInteractive ? "background" : undefined}
fill={
(selected ?? !!props.mapStatus) && isInteractive
? "background"
: undefined
}
/>
) : event.eventTypes.some((et) => et.name === "Ruck") ? (
<RuckSvgComponent
height={iconSize}
width={iconSize}
fill={selected && isInteractive ? "background" : undefined}
fill={
(selected ?? !!props.mapStatus) && isInteractive
? "background"
: undefined
}
/>
) : event.eventTypes.some((et) => et.name === "Run") ? (
<RunSvgComponent
height={iconSize}
width={iconSize}
fill={selected && isInteractive ? "background" : undefined}
fill={
(selected ?? !!props.mapStatus) && isInteractive
? "background"
: undefined
}
/>
) : null}
</div>
Expand Down
145 changes: 121 additions & 24 deletions apps/map/src/app/_components/map/filtered-map-results-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import type { ReactNode } from "react";
import { createContext, useContext, useMemo } from "react";

import { DEFAULT_CENTER } from "@acme/shared/app/constants";
import type { DayOfWeek } from "@acme/shared/app/enums";
import { RERENDER_LOGS } from "@acme/shared/common/constants";

import type { SparseF3Marker } from "~/utils/types";
import { DAYS_OF_WEEK } from "~/utils/days-of-week";
import type { MapStatus, SparseF3Marker } from "~/utils/types";
import { orpc, useQuery } from "~/orpc/react";
import { filterData } from "~/utils/filtered-data";
import { filterStore } from "~/utils/store/filter";
Expand All @@ -33,6 +35,45 @@ const FilteredMapResultsContext = createContext<{
allLocationMarkersWithLatLngAndFilterData: undefined,
});

function dateToDayOfWeek(dateStr: string): DayOfWeek {
const d = new Date(dateStr + "T00:00:00");
return DAYS_OF_WEEK[d.getUTCDay()] ?? "sunday";
}

function computeMapStatus(
event: { startDate: string | null; endDate: string | null; id: number },
instanceLookup: Map<
number,
{ seriesException: string | null; startDate: string }[]
>,
): MapStatus {
const now = new Date();
now.setHours(0, 0, 0, 0);
const thirtyDaysOut = new Date(now);
thirtyDaysOut.setDate(thirtyDaysOut.getDate() + 30);

if (event.endDate) {
const end = new Date(event.endDate + "T00:00:00");
const started = event.startDate
? new Date(event.startDate + "T00:00:00") <= now
: true;
if (started && end >= now && end <= thirtyDaysOut) return "closing";
}

const instances = instanceLookup.get(event.id) ?? [];
if (instances.some((i) => i.seriesException === "closed")) return "closing";

if (event.startDate) {
const start = new Date(event.startDate + "T00:00:00");
if (start > now && start <= thirtyDaysOut) return "deviation";
}

if (instances.some((i) => i.seriesException === "different-time"))
return "deviation";

return null;
}

export const FilteredMapResultsProvider = (params: { children: ReactNode }) => {
RERENDER_LOGS && console.log("FilteredMapResultsProvider rerender");
const nearbyLocationCenter = mapStore.use.nearbyLocationCenter();
Expand All @@ -42,6 +83,12 @@ export const FilteredMapResultsProvider = (params: { children: ReactNode }) => {
}),
);

const { data: upcomingInstancesData } = useQuery(
orpc.map.location.upcomingInstances.queryOptions({
input: undefined,
}),
);

const filters = filterStore.useBoundStore();

/**
Expand All @@ -50,6 +97,27 @@ export const FilteredMapResultsProvider = (params: { children: ReactNode }) => {
const allLocationMarkersWithLatLngAndFilterData = useMemo(() => {
if (!mapEventAndLocationData) return undefined;

const instancesBySeriesId = new Map<
number,
{ seriesException: string | null; startDate: string }[]
>();
const standaloneInstances: NonNullable<typeof upcomingInstancesData> = [];

if (upcomingInstancesData) {
for (const instance of upcomingInstancesData) {
if (instance.seriesId != null) {
const list = instancesBySeriesId.get(instance.seriesId) ?? [];
list.push({
seriesException: instance.seriesException,
startDate: instance.startDate,
});
instancesBySeriesId.set(instance.seriesId, list);
} else {
standaloneInstances.push(instance);
}
}
}

const allLocationMarkerFilterData = mapEventAndLocationData.map(
(location) => {
return {
Expand All @@ -60,42 +128,71 @@ export const FilteredMapResultsProvider = (params: { children: ReactNode }) => {
lon: location[4],
fullAddress: location[5],
events: location[6].map((event) => {
return {
const eventObj = {
id: event[0],
name: event[1],
dayOfWeek: event[2],
startTime: event[3],
eventTypes: event[4],
startDate: event[5],
endDate: event[6],
};
return {
...eventObj,
mapStatus: computeMapStatus(eventObj, instancesBySeriesId),
};
}),
};
},
);

const locationIdToLatLng = allLocationMarkerFilterData.reduce(
(acc, location) => {
acc[location.id] = location;
return acc;
},
{} as Record<
number,
{
lat: number | null;
lon: number | null;
fullAddress: string | null;
}
>,
);
const locationMap = new Map<
number,
(typeof allLocationMarkerFilterData)[number]
>();
for (const loc of allLocationMarkerFilterData) {
locationMap.set(loc.id, loc);
}

return allLocationMarkerFilterData.map((location) => {
return {
...location,
lat: locationIdToLatLng[location.id]?.lat ?? null,
lon: locationIdToLatLng[location.id]?.lon ?? null,
fullAddress: locationIdToLatLng[location.id]?.fullAddress ?? null,
for (const instance of standaloneInstances) {
if (
instance.locationId == null ||
instance.lat == null ||
instance.lon == null
)
continue;
const dayOfWeek = dateToDayOfWeek(instance.startDate);
const instanceEvent = {
id: -instance.id,
name: instance.name,
dayOfWeek: dayOfWeek as DayOfWeek,
startTime: instance.startTime,
eventTypes: instance.eventTypes,
startDate: instance.startDate,
endDate: null,
mapStatus: "highlight" as MapStatus,
};
});
}, [mapEventAndLocationData]);

const existing = locationMap.get(instance.locationId);
if (existing) {
existing.events.push(instanceEvent);
} else {
const newLocation = {
id: instance.locationId,
aoName: instance.aoName ?? "",
logo: instance.aoLogo,
lat: instance.lat,
lon: instance.lon,
fullAddress: instance.fullAddress,
events: [instanceEvent],
};
locationMap.set(instance.locationId, newLocation);
allLocationMarkerFilterData.push(newLocation);
}
}

return allLocationMarkerFilterData;
}, [mapEventAndLocationData, upcomingInstancesData]);

/**
* Filter the location markers by the filters
Expand Down
37 changes: 24 additions & 13 deletions apps/map/src/app/_components/map/group-marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { dayOfWeekToShortDayOfWeek } from "@acme/shared/app/functions";
import { cn } from "@acme/ui";

import { groupMarkerClick } from "~/utils/actions/group-marker-click";
import {
getDominantStatus,
getSelectedBg,
getSelectedBorder,
getStatusBase,
STATUS_BASE_DEFAULT,
} from "~/utils/map-status-colors";
import { appStore } from "~/utils/store/app";
import { mapStore } from "~/utils/store/map";
import {
Expand Down Expand Up @@ -93,12 +100,15 @@ export const FeatureMarker = ({
[id],
);

const dominantStatus = events?.length ? getDominantStatus(events) : null;

return !events?.length ? null : !isClose ? (
<FeaturesClusterMarker
clusterId={id}
position={position}
size={1}
sizeAsText={"1"}
statusColor={dominantStatus}
onMarkerClick={() => {
const eventId = events.find(
(event) => event.id === selectedEventId,
Expand Down Expand Up @@ -170,21 +180,24 @@ export const FeatureMarker = ({
}}
className={cn(
// min-h-[32.5px] so it doesn't collapse with no text
"min-h-[32.5px] flex-1 cursor-pointer bg-foreground py-2 text-center text-background",
"border-b-2 border-l-2 border-r-2 border-t-2 border-foreground ",
"min-h-[32.5px] flex-1 cursor-pointer py-2 text-center",
"border-b-2 border-l-2 border-r-2 border-t-2",
`google-eventid-${event.id}`,
(event.mapStatus && getStatusBase(event.mapStatus)) ??
STATUS_BASE_DEFAULT,
{
"rounded-r-full": isEnd,
"rounded-l-full": isStart,
"border-red-600 dark:border-red-400":
isCurrentSelectedEvent ||
(isCurrentSelectedLocation && noSelectedEvent),
"border-red-600 bg-red-600 font-bold dark:bg-red-400":
// On mobile we always use the background
(touchDevice && isCurrentSelectedEvent) ||
isCurrentPanelEvent ||
(isCurrentPanelLocation && noSelectedPanelEvent),
},
(isCurrentSelectedEvent ||
(isCurrentSelectedLocation && noSelectedEvent)) &&
getSelectedBorder(event.mapStatus),
((touchDevice && isCurrentSelectedEvent) ||
isCurrentPanelEvent ||
(isCurrentPanelLocation && noSelectedPanelEvent)) && [
getSelectedBg(event.mapStatus),
"font-bold",
],
)}
>
{dayText}
Expand All @@ -198,9 +211,7 @@ export const FeatureMarker = ({
style={{ zIndex: 0 }}
>
<path
className={cn("fill-foreground", {
"fill-[#dc2626] dark:fill-[#f87171]": false,
})}
className="fill-foreground"
d={
events.length === 1
? "M34 10 L26 24.249 Q20 34.641 14 24.249 L6 10"
Expand Down
Loading
Loading