Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@import "flowbite-react/plugin/tailwindcss";
@source "../.flowbite-react/class-list.json";
@plugin "daisyui" {
themes: light --default
themes: light --default;
}
@plugin "daisyui/theme" {
name: "garden";
Expand All @@ -13,7 +13,7 @@
--color-base-200: oklch(86.445% 0.002 17.197);
--color-base-300: oklch(79.938% 0.001 17.197);
--color-base-content: oklch(16.961% 0.001 17.32);
--color-primary: #EE2536;
--color-primary: #ee2536;
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(48.495% 0.11 355.095);
--color-secondary-content: oklch(89.699% 0.022 355.095);
Expand All @@ -39,43 +39,52 @@
--noise: 0;
}


h1 {
@apply text-3xl font-bold
@apply text-3xl font-bold;
}

h2 {
@apply text-2xl
@apply text-2xl;
}

h3 {
@apply text-xl
@apply text-xl;
}

p a {
@apply text-blue-500 hover:underline
@apply text-blue-500 hover:underline;
}

:where(.shadow) {
box-shadow: 0 1px 2px 0 rgb(239 68 68 / 0.15), 0 1px 3px 0 rgb(239 68 68 / 0.10) !important;
box-shadow:
0 1px 2px 0 rgb(239 68 68 / 0.15),
0 1px 3px 0 rgb(239 68 68 / 0.1) !important;
}

:where(.shadow-sm) {
box-shadow: 0 1px 2px 0 rgb(239 68 68 / 0.12), 0 1px 3px 0 rgb(239 68 68 / 0.08) !important;
box-shadow:
0 1px 2px 0 rgb(239 68 68 / 0.12),
0 1px 3px 0 rgb(239 68 68 / 0.08) !important;
}

:where(.shadow-md) {
box-shadow: 0 4px 6px -1px rgb(239 68 68 / 0.16), 0 2px 4px -2px rgb(239 68 68 / 0.12) !important;
box-shadow:
0 4px 6px -1px rgb(239 68 68 / 0.16),
0 2px 4px -2px rgb(239 68 68 / 0.12) !important;
}

:where(.shadow-lg) {
box-shadow: 0 10px 15px -3px rgb(239 68 68 / 0.18), 0 4px 6px -4px rgb(239 68 68 / 0.14) !important;
box-shadow:
0 10px 15px -3px rgb(239 68 68 / 0.18),
0 4px 6px -4px rgb(239 68 68 / 0.14) !important;
}

:where(.shadow-xl) {
box-shadow: 0 20px 25px -5px rgb(239 68 68 / 0.20), 0 8px 10px -6px rgb(239 68 68 / 0.16) !important;
box-shadow:
0 20px 25px -5px rgb(239 68 68 / 0.2),
0 8px 10px -6px rgb(239 68 68 / 0.16) !important;
}

:where(.shadow-2xl) {
box-shadow: 0 25px 50px -12px rgb(239 68 68 / 0.22) !important;
}
box-shadow: 0 25px 50px -12px rgb(239 68 68 / 0.22) !important;
}
132 changes: 126 additions & 6 deletions frontend/src/components/maps/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ type MapViewport = {
};

const SINGAPORE_CENTER: PositionType = [1.364917, 103.822872];
const SINGAPORE_CBD_CENTER: PositionType = [1.283, 103.851];
const SINGAPORE_BOUNDS: [PositionType, PositionType] = [
[1.144, 103.535],
[1.494, 104.502],
];
const ONE_MAP_DEFAULT_ZOOM = 15;

const ONE_MAP_ATTRIBUTION =
'<img src="https://www.onemap.gov.sg/web-assets/images/logo/om_logo.png" style="height:20px;width:20px;"/>&nbsp;<a href="https://www.onemap.gov.sg/" target="_blank" rel="noopener noreferrer">OneMap</a>&nbsp;&copy;&nbsp;contributors&nbsp;&#124;&nbsp;<a href="https://www.sla.gov.sg/" target="_blank" rel="noopener noreferrer">Singapore Land Authority</a>';
Expand All @@ -45,7 +47,10 @@ interface MapProps {
points?: MapPoint[];
basemap?: BasemapType;
onViewportChange?: (viewport: MapViewport) => void;
onRenderedPointsChange?: (points: MapPoint[]) => void;
onLoadingChange?: (isLoading: boolean) => void;
scrollWheelZoom?: boolean;
className?: string;
}

const ViewportEvents = ({
Expand Down Expand Up @@ -86,13 +91,77 @@ const ViewportEvents = ({
return null;
};

const MapLoadEvents = ({
onLoadingChange,
}: {
onLoadingChange?: (isLoading: boolean) => void;
}) => {
const map = useMap();

useEffect(() => {
map.whenReady(() => {
onLoadingChange?.(false);
});
}, [map, onLoadingChange]);

return null;
};

const OneMapDefaultView = ({
enabled,
center,
zoom,
}: {
enabled: boolean;
center: PositionType;
zoom: number;
}) => {
const map = useMap();

useEffect(() => {
if (!enabled) return;

map.whenReady(() => {
map.setView(center, zoom, { animate: false });
});
}, [map, enabled, center, zoom]);

return null;
};

const InvalidateMapOnResize = () => {
const map = useMap();

useEffect(() => {
const container = map.getContainer();
if (!container) return;

const observer = new ResizeObserver(() => {
map.invalidateSize({ animate: false });
});

observer.observe(container);

return () => {
observer.disconnect();
};
}, [map]);

return null;
};

const getMinDistancePx = (zoom: number) => {
if (zoom >= 18) return 10;
if (zoom >= 16) return 14;
if (zoom >= 14) return 20;
return 28;
};

const getEffectivePriority = (priority?: number) => {
if (!priority || priority <= 0) return Number.MAX_SAFE_INTEGER;
return priority;
};

const AttractionPopupContent = ({ point }: { point: MapPoint }) => {
return (
<div className="space-y-1">
Expand All @@ -115,12 +184,25 @@ const AttractionPopupContent = ({ point }: { point: MapPoint }) => {
);
};

const AttractionMarkersLayer = ({ points }: { points: MapPoint[] }) => {
const AttractionMarkersLayer = ({
points,
onRenderedPointsChange,
}: {
points: MapPoint[];
onRenderedPointsChange?: (points: MapPoint[]) => void;
}) => {
const map = useMap();
const onRenderedPointsChangeRef = useRef(onRenderedPointsChange);
const lastRenderedSignatureRef = useRef<string>("");

useEffect(() => {
onRenderedPointsChangeRef.current = onRenderedPointsChange;
}, [onRenderedPointsChange]);

const zoom = map.getZoom();
const minimumDistancePx = getMinDistancePx(zoom);
const sortedPoints = [...points].sort(
(a, b) => (a.priority ?? 0) - (b.priority ?? 0),
(a, b) => getEffectivePriority(a.priority) - getEffectivePriority(b.priority),
);

const accepted: Array<{ point: MapPoint; x: number; y: number }> = [];
Expand All @@ -143,6 +225,23 @@ const AttractionMarkersLayer = ({ points }: { points: MapPoint[] }) => {
}
}

const renderedPoints = accepted.map(({ point }) => point);
const renderedSignature = renderedPoints
.map(
(point) =>
`${point.id ?? point.title}:${point.position[0].toFixed(6)}:${point.position[1].toFixed(6)}`,
)
.join("|");

useEffect(() => {
const callback = onRenderedPointsChangeRef.current;
if (!callback) return;
if (lastRenderedSignatureRef.current === renderedSignature) return;

lastRenderedSignatureRef.current = renderedSignature;
callback(renderedPoints);
}, [renderedPoints, renderedSignature]);

return (
<>
{accepted.map(({ point }) => (
Expand All @@ -168,23 +267,29 @@ const Map = ({
points,
basemap = "openstreetmap",
onViewportChange,
onRenderedPointsChange,
onLoadingChange,
scrollWheelZoom = false,
className,
}: MapProps) => {
const mapPoints = points ?? [];
const hasPoints = mapPoints.length > 0;
const mapCenter = mapPoints[0]?.position ?? homePosition ?? centerPosition;
const isOneMap = basemap === "onemap";
const mapCenter = isOneMap
? SINGAPORE_CBD_CENTER
: mapPoints[0]?.position ?? homePosition ?? centerPosition;
const mapZoom = isOneMap ? ONE_MAP_DEFAULT_ZOOM : 13;

return (
<MapContainer
center={mapCenter}
zoom={13}
zoom={mapZoom}
minZoom={isOneMap ? 11 : undefined}
maxZoom={isOneMap ? 19 : undefined}
maxBounds={isOneMap ? SINGAPORE_BOUNDS : undefined}
maxBoundsViscosity={isOneMap ? 1 : undefined}
scrollWheelZoom={scrollWheelZoom}
className="relative z-0 w-full h-100 overflow-hidden rounded-2xl border border-base-300"
className={`relative z-0 w-full h-full overflow-hidden rounded-2xl border border-base-300 ${className ?? ""}`}
>
<TileLayer
attribution={isOneMap ? ONE_MAP_ATTRIBUTION : OSM_ATTRIBUTION}
Expand All @@ -196,10 +301,25 @@ const Map = ({
detectRetina={isOneMap}
minZoom={isOneMap ? 11 : undefined}
maxZoom={isOneMap ? 19 : undefined}
eventHandlers={{
loading: () => onLoadingChange?.(true),
load: () => onLoadingChange?.(false),
tileerror: () => onLoadingChange?.(false),
}}
/>
<OneMapDefaultView
enabled={isOneMap}
center={SINGAPORE_CBD_CENTER}
zoom={ONE_MAP_DEFAULT_ZOOM}
/>
<InvalidateMapOnResize />
<MapLoadEvents onLoadingChange={onLoadingChange} />
<ViewportEvents onViewportChange={onViewportChange} />
{hasPoints ? (
<AttractionMarkersLayer points={mapPoints} />
<AttractionMarkersLayer
points={mapPoints}
onRenderedPointsChange={onRenderedPointsChange}
/>
) : homePosition ? (
<>
{isOneMap ? (
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/maps/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.leaflet-control-attribution {
display: flex;
}
2 changes: 1 addition & 1 deletion frontend/src/components/navigation/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const Navbar = () => {
</div>
</div>

<div id="megamenu-portal" />
<div id="megamenu-portal" className="z-50"/>
</>
);
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/public/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Home = () => {
<HeroSection>
<Map key="map-container" homePosition={location || undefined}></Map>
</HeroSection>

<p>{formatCurrency(1000 * rate, currency)}</p>
<p>{t('greeting', { ns: 'system' })}</p>
</Layout>
Expand Down
Loading
Loading