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) {