From 78c0f32b60d37662ec6df99bd339f0f7685b4831 Mon Sep 17 00:00:00 2001 From: lachlanshoesmith <12870244+lachlanshoesmith@users.noreply.github.com> Date: Wed, 20 May 2026 10:07:44 +1000 Subject: [PATCH 1/2] feat: add geolocation support and debug alert cleanup --- apps/app/app.json | 9 ++- apps/app/app/(app)/map.tsx | 20 +++++- apps/app/app/(auth)/sign-in.native.tsx | 1 - apps/app/components/map/Map.native.tsx | 24 ++++--- apps/app/components/map/Map.tsx | 18 ++++- apps/app/components/map/Map.web.tsx | 42 +++++++++--- apps/app/components/map/MapHUD.tsx | 64 ++++++++++++++--- apps/app/hooks/useUserLocation.ts | 95 ++++++++++++++++++++++++++ apps/app/package.json | 1 + bun.lock | 3 + 10 files changed, 237 insertions(+), 40 deletions(-) create mode 100644 apps/app/hooks/useUserLocation.ts diff --git a/apps/app/app.json b/apps/app/app.json index 59a88c8..fedd25f 100644 --- a/apps/app/app.json +++ b/apps/app/app.json @@ -41,7 +41,14 @@ } } ], - "expo-secure-store" + "expo-secure-store", + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "Allow UNSW Connect to use your location.", + "locationWhenInUsePermission": "Allow UNSW Connect to use your location." + } + ] ], "experiments": { "typedRoutes": true, diff --git a/apps/app/app/(app)/map.tsx b/apps/app/app/(app)/map.tsx index 02f3273..d2cca61 100644 --- a/apps/app/app/(app)/map.tsx +++ b/apps/app/app/(app)/map.tsx @@ -1,22 +1,36 @@ import { useCallback, useRef, useState } from "react"; -import { LayoutChangeEvent, StyleSheet, View } from "react-native"; +import { LayoutChangeEvent, Linking, StyleSheet, View } from "react-native"; import Map from "@/components/map/Map"; import { CanvasModal } from "@/components/CanvasModal"; import { MapHUD } from "@/components/map/MapHUD"; +import { useUserLocation } from "@/hooks/useUserLocation"; export default function MapScreen() { const mapRef = useRef<{ invalidateSize: () => void }>(null); const [isCanvasOpen, setIsCanvasOpen] = useState(false); + const { location, isDenied, canAskAgain, requestPermission } = useUserLocation(); const onLayout = useCallback((_event: LayoutChangeEvent) => { mapRef.current?.invalidateSize(); }, []); + const handleEnableLocation = useCallback(async () => { + if (canAskAgain) { + await requestPermission(); + } else { + Linking.openURL("app-settings:"); + } + }, [canAskAgain, requestPermission]); + return ( - - setIsCanvasOpen(true)} /> + + setIsCanvasOpen(true)} + isPermissionDenied={isDenied} + onEnableLocation={handleEnableLocation} + /> setIsCanvasOpen(false)} /> ); diff --git a/apps/app/app/(auth)/sign-in.native.tsx b/apps/app/app/(auth)/sign-in.native.tsx index 50e9a16..a32fbba 100644 --- a/apps/app/app/(auth)/sign-in.native.tsx +++ b/apps/app/app/(auth)/sign-in.native.tsx @@ -8,7 +8,6 @@ export default function SignInScreen() { const router = useRouter(); useEffect(() => { - alert(isSignedIn); if (isSignedIn) { router.replace("/(app)/map"); } diff --git a/apps/app/components/map/Map.native.tsx b/apps/app/components/map/Map.native.tsx index 50c7870..660da53 100644 --- a/apps/app/components/map/Map.native.tsx +++ b/apps/app/components/map/Map.native.tsx @@ -86,20 +86,25 @@ function UserAvatarMarker({ coordinate, imageUrl }: UserAvatarMarkerProps) { ); } -export default forwardRef<{ invalidateSize: () => void }>(function Map(_props, _ref) { +interface MapNativeProps { + location: { latitude: number; longitude: number } | null; +} + +export default forwardRef<{ invalidateSize: () => void }, MapNativeProps>(function Map( + { location }, + _ref, +) { const [selectedPOI, setSelectedPOI] = useState(null); const userAvatarUrl = Asset.fromModule(require("@/assets/images/avatar.png")).uri; + const userCoord: [number, number] = location + ? [location.longitude, location.latitude] + : [UNSW_CENTER.lng, UNSW_CENTER.lat]; return ( setSelectedPOI(null)}> - + {DEMO_POIS.map((poi) => ( void }>(function Map(_props, _ /> ))} - + {selectedPOI && ( diff --git a/apps/app/components/map/Map.tsx b/apps/app/components/map/Map.tsx index a02412f..2b84a99 100644 --- a/apps/app/components/map/Map.tsx +++ b/apps/app/components/map/Map.tsx @@ -3,10 +3,22 @@ import { Platform } from "react-native"; import MapNative from "./Map.native"; import MapWeb from "./Map.web"; -export default forwardRef<{ invalidateSize: () => void }>(function Map(_props, ref) { +export interface MapLocation { + latitude: number; + longitude: number; +} + +interface MapProps { + location: MapLocation | null; +} + +export default forwardRef<{ invalidateSize: () => void }, MapProps>(function Map( + { location }, + ref, +) { if (Platform.OS !== "web") { - return ; + return ; } else { - return ; + return ; } }); diff --git a/apps/app/components/map/Map.web.tsx b/apps/app/components/map/Map.web.tsx index 8e1c11d..d44219a 100644 --- a/apps/app/components/map/Map.web.tsx +++ b/apps/app/components/map/Map.web.tsx @@ -1,4 +1,4 @@ -import type { Map as LeafletMap } from "leaflet"; +import type { Map as LeafletMap, Marker as LeafletMarker } from "leaflet"; import { Asset } from "expo-asset"; import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; import { Platform, StyleSheet, View } from "react-native"; @@ -12,17 +12,19 @@ const TILE_ATTR = type MapHandle = { invalidateSize: () => void }; -export default forwardRef(function MapWeb(_props, ref) { +interface MapWebProps { + location: { latitude: number; longitude: number } | null; +} + +export default forwardRef(function MapWeb({ location }, ref) { const containerRef = useRef(null); - const mapRef = useRef<{ - invalidateSize: () => void; - remove: () => void; - } | null>(null); + const mapRef = useRef(null); + const userMarkerRef = useRef(null); + const locationRef = useRef(location); + locationRef.current = location; useImperativeHandle(ref, () => ({ - invalidateSize: () => { - mapRef.current?.invalidateSize(); - }, + invalidateSize: () => mapRef.current?.invalidateSize(), })); useEffect(() => { @@ -35,8 +37,13 @@ export default forwardRef(function MapWeb(_props, ref) { let map: LeafletMap | null = null; import("leaflet").then((L) => { + const currentLoc = locationRef.current; + const initialCenter: [number, number] = currentLoc + ? [currentLoc.latitude, currentLoc.longitude] + : [UNSW_CENTER.lat, UNSW_CENTER.lng]; + map = L.map(container, { - center: [UNSW_CENTER.lat, UNSW_CENTER.lng], + center: initialCenter, zoom: 19, minZoom: 18, zoomControl: false, @@ -58,19 +65,32 @@ export default forwardRef(function MapWeb(_props, ref) { } const avatarUrl = Asset.fromModule(require("@/assets/images/avatar.png")).uri; - L.marker([UNSW_CENTER.lat, UNSW_CENTER.lng], { + const userMarker = L.marker(initialCenter, { icon: toLeafletIcon(createUserAvatarMarker(avatarUrl)), }).addTo(map); + userMarkerRef.current = userMarker; mapRef.current = map; }); return () => { map?.remove(); mapRef.current = null; + userMarkerRef.current = null; }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!mapRef.current || !userMarkerRef.current || !location) return; + + const { latitude, longitude } = location; + mapRef.current.setView([latitude, longitude], mapRef.current.getZoom(), { + animate: true, + }); + userMarkerRef.current.setLatLng([latitude, longitude]); + }, [location]); + return ; }); diff --git a/apps/app/components/map/MapHUD.tsx b/apps/app/components/map/MapHUD.tsx index ef50537..f22a864 100644 --- a/apps/app/components/map/MapHUD.tsx +++ b/apps/app/components/map/MapHUD.tsx @@ -53,16 +53,31 @@ function TextButton({ label, onPress }: { label: string; onPress: () => void }) type MapHUDProps = { onStudioPress: () => void; + isPermissionDenied: boolean; + onEnableLocation: () => void; }; -export function MapHUD({ onStudioPress }: MapHUDProps) { +export function MapHUD({ onStudioPress, isPermissionDenied, onEnableLocation }: MapHUDProps) { return ( - - - router.push("/quests" as any)} /> - - + {isPermissionDenied && ( + + Enable location to see nearby quests & POIs + [styles.permissionButton, { opacity: pressed ? 0.7 : 1 }]} + > + Enable + + + )} + + + + router.push("/quests" as any)} /> + + + ); @@ -70,13 +85,42 @@ export function MapHUD({ onStudioPress }: MapHUDProps) { const styles = StyleSheet.create({ container: { - alignItems: "flex-start", + position: "absolute", + left: 15, + right: 15, bottom: 15, + }, + bottomRow: { flexDirection: "row", justifyContent: "space-between", - left: 15, - position: "absolute", - right: 15, + alignItems: "flex-start", + }, + permissionBanner: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#5b7559", + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 10, + marginBottom: 10, + }, + permissionText: { + color: "#ffedd6", + fontFamily: fonts.family, + fontSize: 16, + flex: 1, + }, + permissionButton: { + backgroundColor: "#ffedd6", + borderRadius: 8, + paddingHorizontal: 16, + paddingVertical: 6, + marginLeft: 12, + }, + permissionButtonText: { + color: "#5b7559", + fontFamily: fonts.family, + fontSize: 18, }, leftSection: { flexDirection: "column", diff --git a/apps/app/hooks/useUserLocation.ts b/apps/app/hooks/useUserLocation.ts new file mode 100644 index 0000000..058cf71 --- /dev/null +++ b/apps/app/hooks/useUserLocation.ts @@ -0,0 +1,95 @@ +import { useEffect, useRef, useState } from "react"; +import * as Location from "expo-location"; + +export interface UserLocation { + latitude: number; + longitude: number; + heading: number | null; + accuracy: number | null; +} + +export function useUserLocation() { + const [location, setLocation] = useState(null); + const [permission, setPermission] = useState(null); + const [isTracking, setIsTracking] = useState(false); + const subscriptionRef = useRef(null); + + const stopWatching = () => { + subscriptionRef.current?.remove(); + subscriptionRef.current = null; + setIsTracking(false); + }; + + const startWatching = async () => { + stopWatching(); + + const lastPos = await Location.getLastKnownPositionAsync({ maxAge: 60000 }); + if (lastPos) { + setLocation({ + latitude: lastPos.coords.latitude, + longitude: lastPos.coords.longitude, + heading: lastPos.coords.heading, + accuracy: lastPos.coords.accuracy, + }); + } + + const sub = await Location.watchPositionAsync( + { + accuracy: Location.Accuracy.Balanced, + timeInterval: 5000, + distanceInterval: 5, + }, + (loc) => { + setLocation({ + latitude: loc.coords.latitude, + longitude: loc.coords.longitude, + heading: loc.coords.heading, + accuracy: loc.coords.accuracy, + }); + setIsTracking(true); + }, + ); + + subscriptionRef.current = sub; + }; + + const requestPermission = async () => { + const perm = await Location.requestForegroundPermissionsAsync(); + setPermission(perm); + if (perm.granted) { + await startWatching(); + } + return perm; + }; + + useEffect(() => { + let cancelled = false; + + (async () => { + const perm = await Location.requestForegroundPermissionsAsync(); + if (cancelled) return; + setPermission(perm); + + if (perm.granted) { + await startWatching(); + } + })(); + + return () => { + cancelled = true; + stopWatching(); + }; + }, []); + + const isDenied = permission !== null && !permission.granted && !permission.canAskAgain; + const canAskAgain = permission?.canAskAgain ?? true; + + return { + location, + isDenied, + canAskAgain, + permission, + isTracking, + requestPermission, + }; +} diff --git a/apps/app/package.json b/apps/app/package.json index 218cce3..b903fae 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -33,6 +33,7 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.11", + "expo-location": "~19.0.8", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", "expo-splash-screen": "~31.0.13", diff --git a/bun.lock b/bun.lock index e454f49..2e43036 100644 --- a/bun.lock +++ b/bun.lock @@ -50,6 +50,7 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.11", + "expo-location": "~19.0.8", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", "expo-splash-screen": "~31.0.13", @@ -1365,6 +1366,8 @@ "expo-linking": ["expo-linking@8.0.12", "", { "dependencies": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ=="], + "expo-location": ["expo-location@19.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA=="], + "expo-manifests": ["expo-manifests@1.0.11", "", { "dependencies": { "@expo/config": "~12.0.13", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-6zItytTewN37Cjhp3glUg0ozrgW2GwB8x9wtfzUNoJIMmxO38nnGdTLMaotYhRqdf5PP2Dzdmej1HDHXVNUpRw=="], "expo-modules-autolinking": ["expo-modules-autolinking@3.0.25", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-YmHWctJlwvOuLZccg3cOXvSiXVJrPMKl7g2YR0YHWoGL9v2RvcmgaPJWPSLVW+voNEgEPsbo5UmUrAqbnYcBeg=="], From 69528c2c9cea7c4fe2188f9b9a0380f90e72b85c Mon Sep 17 00:00:00 2001 From: lachlanshoesmith <12870244+lachlanshoesmith@users.noreply.github.com> Date: Wed, 20 May 2026 14:02:58 +1000 Subject: [PATCH 2/2] =?UTF-8?q?fix(app):=20rename=20'whiteboard'=20?= =?UTF-8?q?=E2=86=92=20'billboard',=20fix=20map=20marker=20press,=20improv?= =?UTF-8?q?e=20MapHUD=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace remaining 'whiteboard' references with 'billboard' for consistency - Remove extraneous TouchableOpacity wrappers on MapLibre markers (onPress now on Marker directly) - Wrap ProfileButton + right stack in bottomRow with alignItems: flex-end - Add debug logging in apiFetch --- apps/app/app/(app)/map.tsx | 120 ++++---- apps/app/app/billboard/[id].tsx | 2 +- .../components/billboard/BillboardPanel.tsx | 2 +- apps/app/components/map/Map.native.tsx | 270 ++++++++---------- apps/app/components/map/MapHUD.tsx | 18 +- apps/app/constants/coordinates.ts | 2 +- apps/app/lib/api/client.ts | 4 +- 7 files changed, 193 insertions(+), 225 deletions(-) diff --git a/apps/app/app/(app)/map.tsx b/apps/app/app/(app)/map.tsx index 9a3277a..1b35cd8 100644 --- a/apps/app/app/(app)/map.tsx +++ b/apps/app/app/(app)/map.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from "react"; import { ActivityIndicator, Linking, @@ -9,32 +9,29 @@ import { Text, TextInput, View, -} from 'react-native'; +} from "react-native"; -import { BillboardPanel } from '@/components/billboard/BillboardPanel'; -import { CanvasModal } from '@/components/CanvasModal'; -import Map from '@/components/map/Map'; -import { MapHUD } from '@/components/map/MapHUD'; -import { UNSW_CAMPUS_ID, UNSW_CENTER } from '@/constants/coordinates'; -import { useUserLocation } from '@/hooks/useUserLocation'; -import { ApiError } from '@/lib/api/client'; -import { useBillboards, useCreateBillboard, usePois } from '@/lib/api/hooks'; -import { colors } from '@/lib/theme'; +import { BillboardPanel } from "@/components/billboard/BillboardPanel"; +import { CanvasModal } from "@/components/CanvasModal"; +import Map from "@/components/map/Map"; +import { MapHUD } from "@/components/map/MapHUD"; +import { UNSW_CAMPUS_ID, UNSW_CENTER } from "@/constants/coordinates"; +import { useUserLocation } from "@/hooks/useUserLocation"; +import { ApiError } from "@/lib/api/client"; +import { useBillboards, useCreateBillboard, usePois } from "@/lib/api/hooks"; +import { colors } from "@/lib/theme"; export default function MapScreen() { const mapRef = useRef<{ invalidateSize: () => void }>(null); const [isCanvasOpen, setIsCanvasOpen] = useState(false); - const [activeBillboardId, setActiveBillboardId] = useState( - null, - ); + const [activeBillboardId, setActiveBillboardId] = useState(null); const [createOpen, setCreateOpen] = useState(false); - const [body, setBody] = useState(''); + const [body, setBody] = useState(""); const [createError, setCreateError] = useState(null); const billboards = useBillboards({ campusId: UNSW_CAMPUS_ID }); const pois = usePois({ campusId: UNSW_CAMPUS_ID }); const createBillboard = useCreateBillboard(); - const { location, isDenied, canAskAgain, requestPermission } = - useUserLocation(); + const { location, isDenied, canAskAgain, requestPermission } = useUserLocation(); const closeCreate = () => { setCreateOpen(false); @@ -44,7 +41,7 @@ export default function MapScreen() { const submitBillboard = () => { const trimmed = body.trim(); if (!trimmed) { - setCreateError('Write something for your whiteboard first.'); + setCreateError("Write something for your whiteboard first."); return; } @@ -59,7 +56,7 @@ export default function MapScreen() { }, { onSuccess: (data) => { - setBody(''); + setBody(""); setCreateOpen(false); setActiveBillboardId(data.billboard.id); }, @@ -68,7 +65,8 @@ export default function MapScreen() { setCreateError(err.message); return; } - setCreateError('Could not pin this whiteboard.'); + console.log(err.message); + setCreateError("Could not pin this billboard."); }, }, ); @@ -78,7 +76,7 @@ export default function MapScreen() { if (canAskAgain) { await requestPermission(); } else { - Linking.openURL('app-settings:'); + Linking.openURL("app-settings:"); } }, [canAskAgain, requestPermission]); @@ -120,10 +118,7 @@ export default function MapScreen() { onRequestClose={() => setActiveBillboardId(null)} > - setActiveBillboardId(null)} - /> + setActiveBillboardId(null)} /> - + - New whiteboard + New billboard {body.length}/500 - {createError ? ( - {createError} - ) : null} + {createError ? {createError} : null} - {createBillboard.isPending ? 'Pinning...' : 'Pin here'} + {createBillboard.isPending ? "Pinning..." : "Pin here"} - setIsCanvasOpen(false)} - /> + setIsCanvasOpen(false)} /> ); } @@ -202,47 +184,47 @@ const styles = StyleSheet.create({ flex: 1, }, mapStatus: { - alignItems: 'center', + alignItems: "center", backgroundColor: colors.pageBgSoft, borderColor: colors.sageDark, borderRadius: 999, borderWidth: 2, height: 42, - justifyContent: 'center', - position: 'absolute', + justifyContent: "center", + position: "absolute", right: 18, top: 18, width: 42, }, modalRoot: { flex: 1, - justifyContent: 'center', + justifyContent: "center", }, backdrop: { ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(36, 30, 22, 0.55)', + backgroundColor: "rgba(36, 30, 22, 0.55)", }, modalScroll: { - maxHeight: '92%', - width: '100%', + maxHeight: "92%", + width: "100%", }, modalScrollContent: { - alignItems: 'center', - justifyContent: 'center', + alignItems: "center", + justifyContent: "center", padding: 18, }, modalPanel: { - backgroundColor: '#F2EAD3', - borderColor: '#384730', + backgroundColor: "#F2EAD3", + borderColor: "#384730", borderRadius: 16, borderWidth: 3, gap: 18, maxWidth: 820, padding: 18, - width: '100%', + width: "100%", }, createPanel: { - alignSelf: 'center', + alignSelf: "center", backgroundColor: colors.pageBg, borderColor: colors.sageDarker, borderRadius: 16, @@ -250,25 +232,25 @@ const styles = StyleSheet.create({ gap: 14, maxWidth: 460, padding: 18, - width: '90%', + width: "90%", }, createHeader: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-between', + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", }, createTitle: { color: colors.ink, fontSize: 26, }, closeButton: { - alignItems: 'center', + alignItems: "center", backgroundColor: colors.pageBgSoft, borderColor: colors.sageDark, borderRadius: 999, borderWidth: 2, height: 34, - justifyContent: 'center', + justifyContent: "center", width: 34, }, closeText: { @@ -282,17 +264,17 @@ const styles = StyleSheet.create({ borderRadius: 12, borderWidth: 2, color: colors.ink, - fontFamily: 'Jersey10_400Regular', + fontFamily: "Jersey10_400Regular", fontSize: 20, minHeight: 140, padding: 14, - textAlignVertical: 'top', + textAlignVertical: "top", }, createFooter: { gap: 6, }, charCount: { - alignSelf: 'flex-end', + alignSelf: "flex-end", color: colors.inkSoft, fontSize: 14, }, @@ -301,14 +283,14 @@ const styles = StyleSheet.create({ fontSize: 16, }, submitButton: { - alignItems: 'center', + alignItems: "center", backgroundColor: colors.sageDark, borderColor: colors.sageDarker, borderRadius: 12, borderWidth: 2, - flexDirection: 'row', + flexDirection: "row", gap: 8, - justifyContent: 'center', + justifyContent: "center", minHeight: 48, }, disabled: { diff --git a/apps/app/app/billboard/[id].tsx b/apps/app/app/billboard/[id].tsx index ca75e2b..67cd81b 100644 --- a/apps/app/app/billboard/[id].tsx +++ b/apps/app/app/billboard/[id].tsx @@ -23,7 +23,7 @@ export default function BillboardDetailScreen() { return ( - + ); diff --git a/apps/app/components/billboard/BillboardPanel.tsx b/apps/app/components/billboard/BillboardPanel.tsx index 82f5bd5..af9477e 100644 --- a/apps/app/components/billboard/BillboardPanel.tsx +++ b/apps/app/components/billboard/BillboardPanel.tsx @@ -161,7 +161,7 @@ export function BillboardPanel({ id, onClose }: BillboardPanelProps) { if (billboard.isError || !billboard.data) { return ( - Whiteboard not found + Billboard not found {(billboard.error as Error | undefined)?.message ?? "It may have expired."} diff --git a/apps/app/components/map/Map.native.tsx b/apps/app/components/map/Map.native.tsx index 154a9b9..f783e04 100644 --- a/apps/app/components/map/Map.native.tsx +++ b/apps/app/components/map/Map.native.tsx @@ -1,26 +1,21 @@ -import { Asset } from 'expo-asset'; -import { forwardRef, useState } from 'react'; -import { StyleSheet, Text, TouchableOpacity, View, Image } from 'react-native'; +import { Asset } from "expo-asset"; +import { forwardRef, useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View, Image } from "react-native"; // Import Marker alongside Map from the library -import { - Camera, - Map as MapLibre, - Marker, -} from '@maplibre/maplibre-react-native'; +import { Camera, Map as MapLibre, Marker } from "@maplibre/maplibre-react-native"; -import { fonts } from '@/app/theme'; -import { UNSW_CENTER } from '@/constants/coordinates'; +import { fonts } from "@/app/theme"; +import { UNSW_CENTER } from "@/constants/coordinates"; -import type { MapPoi, MapProps } from './Map.types'; +import type { MapPoi, MapProps } from "./Map.types"; -const THUNDERFOREST_API_KEY = - process.env.EXPO_PUBLIC_THUNDERFOREST_KEY ?? 'YOUR_API_KEY_HERE'; +const THUNDERFOREST_API_KEY = process.env.EXPO_PUBLIC_THUNDERFOREST_KEY ?? "YOUR_API_KEY_HERE"; const MAP_STYLE: any = JSON.stringify({ version: 8, sources: { - 'thunderforest-neighbourhood': { - type: 'raster', + "thunderforest-neighbourhood": { + type: "raster", tiles: [ `https://api.thunderforest.com/neighbourhood/{z}/{x}/{y}.png?apikey=${THUNDERFOREST_API_KEY}`, ], @@ -33,9 +28,9 @@ const MAP_STYLE: any = JSON.stringify({ }, layers: [ { - id: 'thunderforest-tiles', - type: 'raster', - source: 'thunderforest-neighbourhood', + id: "thunderforest-tiles", + type: "raster", + source: "thunderforest-neighbourhood", minzoom: 0, maxzoom: 22, }, @@ -50,23 +45,19 @@ interface POIMarkerProps { function POIMarker({ poi, isSelected, onPress }: POIMarkerProps) { return ( - - - - + + + - - {/* Embedded Cross Lines */} - - - - - {/* Base Platform Bar (x=0, y=28, w=32, h=4) */} - + + {/* Embedded Cross Lines */} + + - + + {/* Base Platform Bar (x=0, y=28, w=32, h=4) */} + + ); } @@ -83,15 +74,15 @@ function BillboardMarker({ title: string; }) { return ( - - + + {title.slice(0, 2)} - + ); } @@ -116,127 +107,118 @@ function UserAvatarMarker({ coordinate, imageUrl }: UserAvatarMarkerProps) { ); } -export default forwardRef<{ invalidateSize: () => void }, MapProps>( - function Map({ location, billboards, onBillboardPress, pois }, _ref) { - const [selectedPOI, setSelectedPOI] = useState(null); +export default forwardRef<{ invalidateSize: () => void }, MapProps>(function Map( + { location, billboards, onBillboardPress, pois }, + _ref, +) { + const [selectedPOI, setSelectedPOI] = useState(null); - const userAvatarUrl = Asset.fromModule( - require('@/assets/images/avatar.png'), - ).uri; - const userCoord: [number, number] = location - ? [location.longitude, location.latitude] - : [UNSW_CENTER.lng, UNSW_CENTER.lat]; + const userAvatarUrl = Asset.fromModule(require("@/assets/images/avatar.png")).uri; + const userCoord: [number, number] = location + ? [location.longitude, location.latitude] + : [UNSW_CENTER.lng, UNSW_CENTER.lat]; - return ( - - setSelectedPOI(null)} - > - - {pois.map((poi) => ( - - setSelectedPOI((prev) => (prev?.id === poi.id ? null : poi)) - } - /> - ))} + return ( + + setSelectedPOI(null)}> + + {pois.map((poi) => ( + setSelectedPOI((prev) => (prev?.id === poi.id ? null : poi))} + /> + ))} - {billboards.map((billboard) => ( - onBillboardPress?.(billboard.id)} - /> - ))} + {billboards.map((billboard) => ( + onBillboardPress?.(billboard.id)} + /> + ))} - - + + - {selectedPOI && ( - - - - - - {selectedPOI.title} - - setSelectedPOI(null)} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} - > - - + {selectedPOI && ( + + + + + + {selectedPOI.title} - - {selectedPOI.description ?? ''} - + setSelectedPOI(null)} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > + + + {selectedPOI.description ?? ""} - )} - - ); - }, -); + + )} + + ); +}); const poiStyles = StyleSheet.create({ markerContainer: { width: 32, height: 40, - alignItems: 'center', + alignItems: "center", }, topCircle: { - position: 'absolute', + position: "absolute", top: 1, width: 8, height: 8, borderRadius: 4, - backgroundColor: '#FFD700', + backgroundColor: "#FFD700", borderWidth: 1.5, - borderColor: '#B8860B', + borderColor: "#B8860B", zIndex: 2, }, mainBox: { - position: 'absolute', + position: "absolute", top: 8, width: 28, height: 20, borderRadius: 3, - backgroundColor: '#8B6914', + backgroundColor: "#8B6914", borderWidth: 2, - borderColor: '#5C4A10', - justifyContent: 'center', - alignItems: 'center', + borderColor: "#5C4A10", + justifyContent: "center", + alignItems: "center", }, mainBoxSelected: { - borderColor: '#4A90E2', + borderColor: "#4A90E2", borderWidth: 2.5, }, verticalLine: { - position: 'absolute', + position: "absolute", width: 2, height: 12, - backgroundColor: '#5C4A10', + backgroundColor: "#5C4A10", }, horizontalLine: { - position: 'absolute', + position: "absolute", width: 12, height: 2, - backgroundColor: '#5C4A10', + backgroundColor: "#5C4A10", }, baseBar: { - position: 'absolute', + position: "absolute", top: 28, width: 32, height: 4, borderRadius: 1, - backgroundColor: '#5C4A10', + backgroundColor: "#5C4A10", }, }); @@ -244,33 +226,33 @@ const avatarStyles = StyleSheet.create({ markerContainer: { width: 54, height: 61, - alignItems: 'center', + alignItems: "center", }, avatarFrame: { width: 48, height: 48, borderRadius: 24, - overflow: 'hidden', + overflow: "hidden", borderWidth: 3, - borderColor: '#5b7559', - backgroundColor: '#ffffff', + borderColor: "#5b7559", + backgroundColor: "#ffffff", }, avatarImage: { - width: '100%', - height: '100%', - resizeMode: 'cover', + width: "100%", + height: "100%", + resizeMode: "cover", }, trianglePointer: { width: 0, height: 0, - backgroundColor: 'transparent', - borderStyle: 'solid', + backgroundColor: "transparent", + borderStyle: "solid", borderLeftWidth: 8, borderRightWidth: 8, borderTopWidth: 10, - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - borderTopColor: '#5b7559', + borderLeftColor: "transparent", + borderRightColor: "transparent", + borderTopColor: "#5b7559", marginTop: -3, }, }); @@ -279,22 +261,22 @@ const billboardStyles = StyleSheet.create({ marker: { width: 34, height: 28, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#F7D978', - borderColor: '#6A401A', + alignItems: "center", + justifyContent: "center", + backgroundColor: "#F7D978", + borderColor: "#6A401A", borderRadius: 3, borderWidth: 2, }, pin: { - alignSelf: 'center', - backgroundColor: '#6A401A', + alignSelf: "center", + backgroundColor: "#6A401A", height: 9, marginTop: -1, width: 4, }, text: { - color: '#6A401A', + color: "#6A401A", fontFamily: fonts.family, fontSize: 14, }, @@ -303,36 +285,36 @@ const billboardStyles = StyleSheet.create({ const styles = StyleSheet.create({ container: { flex: 1, - width: '100%', + width: "100%", }, map: { flex: 1, }, callout: { - position: 'absolute', + position: "absolute", bottom: 0, left: 0, right: 0, padding: 16, }, calloutContent: { - backgroundColor: '#ffffff', + backgroundColor: "#ffffff", borderRadius: 16, padding: 16, - shadowColor: '#000', + shadowColor: "#000", shadowOffset: { width: 0, height: -2 }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 6, }, calloutHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", }, calloutTitleRow: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", gap: 8, }, calloutDot: { @@ -342,16 +324,16 @@ const styles = StyleSheet.create({ }, calloutTitle: { fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', + fontWeight: "600", + color: "#1a1a1a", }, calloutDismiss: { fontSize: 16, - color: '#999', + color: "#999", }, calloutDescription: { fontSize: 14, - color: '#555', + color: "#555", marginTop: 8, lineHeight: 20, }, diff --git a/apps/app/components/map/MapHUD.tsx b/apps/app/components/map/MapHUD.tsx index a94036c..4830846 100644 --- a/apps/app/components/map/MapHUD.tsx +++ b/apps/app/components/map/MapHUD.tsx @@ -90,13 +90,15 @@ export function MapHUD({ onCreateBillboard, isPermissionDenied, onEnableLocation )} - - - - - router.push("/quests" as any)} /> - - router.push("/studio" as any)} /> + + + + + + router.push("/quests" as any)} /> + + router.push("/studio" as any)} /> + @@ -113,7 +115,7 @@ const styles = StyleSheet.create({ bottomRow: { flexDirection: "row", justifyContent: "space-between", - alignItems: "flex-start", + alignItems: "flex-end", }, permissionBanner: { flexDirection: "row", diff --git a/apps/app/constants/coordinates.ts b/apps/app/constants/coordinates.ts index 973fb58..afbbca9 100644 --- a/apps/app/constants/coordinates.ts +++ b/apps/app/constants/coordinates.ts @@ -4,7 +4,7 @@ export const UNSW_CENTER = { lat: -33.917, lng: 151.231 } as const; export const DEMO_BILLBOARD = { id: "00000000-0000-4000-8000-000000000b01", - title: "Campus Whiteboard", + title: "Campus Billboard", lat: -33.9173, lng: 151.2313, } as const; diff --git a/apps/app/lib/api/client.ts b/apps/app/lib/api/client.ts index 1e4831d..5f3f660 100644 --- a/apps/app/lib/api/client.ts +++ b/apps/app/lib/api/client.ts @@ -44,7 +44,9 @@ export async function apiFetch({ init.body = JSON.stringify(body); } - const res = await fetch(`${API_BASE_URL}${path}`, init); + const url = `${API_BASE_URL}${path}`; + console.log("[apiFetch]", method, url, body); + const res = await fetch(url, init); const json: unknown = await res.json().catch(() => ({})); if (!res.ok) {