Skip to content
Draft
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
4 changes: 2 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
- [x] **FE1** — Map: show POI markers with distinct glowing style
- [X] **FE1** — Map: show billboard markers with note icon style
- [x] **FE1** — Map: show user's current location as their 64×64 avatar (instead of a standard dot)
- [ ] **FE1** — POI discovery UX: toast when entering geofence + quest progress trigger (quest-progress toast plumbing exists for API mutations; geofence trigger still pending)
- [ ] **FE1** — POI discovery UX: toast when entering geofence + quest progress trigger (manual POI check-in now calls the live visit API; automatic geofence trigger still pending)
- [X] **FE2** — Billboard expanded view (~60vh overlay): text + username pill + all placements (z-ordered)
- [X] **FE2** — Pixel art sticker editor: 64×64 grid, 8-colour palette, tap-to-fill, save to collection
- [X] **FE2** — Sticky note composer: text input, preview as sticky note, post to billboard
Expand All @@ -76,7 +76,7 @@
- [x] **BE2** — Durable Object: WebSocket handler, Postgres connection, broadcast on mutations
- [ ] **BE2** — Expo Push Notification integration: register token, send on reply + daily reminder
- [x] **FE1** — Quest screen: main quest tiers + daily quest + streak counter + progress bars (currently backed by mock quest data)
- [x] **FE1** — Profile screen: level, perks unlocked, stats (notes placed, stickers saved, POIs visited)
- [x] **FE1** — Profile screen: level, perks unlocked, stats (notes placed, stickers saved, POIs visited), backed by the live user/progress API
- [x] **FE1** — Level-up celebration animation/overlay
- [x] **FE2** — WebSocket connection in app: connect to DO, listen for updates, refresh displayed data
- [x] **FE2** — Saved stickers collection picker: browse, select, reuse stickers inside the billboard placement flow
Expand Down
77 changes: 75 additions & 2 deletions apps/app/app/(app)/map.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useCallback, useState } from "react";
import {
ActivityIndicator,
Modal,
Expand All @@ -15,17 +15,19 @@ import { Map } from "@/components/map/Map";
import { MapHUD } from "@/components/map/MapHUD";
import { UNSW_CAMPUS_ID, UNSW_CENTER } from "@/constants/coordinates";
import { ApiError } from "@/lib/api/client";
import { useBillboards, useCreateBillboard, usePois } from "@/lib/api/hooks";
import { useBillboards, useCreateBillboard, usePois, useVisitPoi } from "@/lib/api/hooks";
import { colors } from "@/lib/theme";

export default function MapScreen() {
const [activeBillboardId, setActiveBillboardId] = useState<string | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [body, setBody] = useState("");
const [createError, setCreateError] = useState<string | null>(null);
const [poiError, setPoiError] = useState<string | null>(null);
const billboards = useBillboards({ campusId: UNSW_CAMPUS_ID });
const pois = usePois({ campusId: UNSW_CAMPUS_ID });
const createBillboard = useCreateBillboard();
const visitPoi = useVisitPoi({ campusId: UNSW_CAMPUS_ID });

const closeCreate = () => {
setCreateOpen(false);
Expand Down Expand Up @@ -65,6 +67,35 @@ export default function MapScreen() {
);
};

const checkInToPoi = useCallback(
(id: string) => {
const poi = pois.data?.find((candidate) => candidate.id === id);
if (!poi || visitPoi.isPending) return;

setPoiError(null);
void resolveCheckInPosition({ lat: poi.lat, lng: poi.lng })
.then((input) => {
visitPoi.mutate(
{ id, input },
{
onSuccess: (data) => {
if (!data.withinRadius) {
setPoiError("Move closer to this POI to check in.");
}
},
onError: (err) => {
setPoiError(err instanceof ApiError ? err.message : "Could not check in here.");
},
},
);
})
.catch(() => {
setPoiError("Allow location access to check in.");
});
},
[pois.data, visitPoi],
);

return (
<View style={styles.root}>
<Map
Expand All @@ -75,6 +106,7 @@ export default function MapScreen() {
lng: billboard.lng,
}))}
onBillboardPress={setActiveBillboardId}
onPoiCheckIn={checkInToPoi}
pois={(pois.data ?? []).map((poi) => ({
id: poi.id,
title: poi.title,
Expand All @@ -89,6 +121,11 @@ export default function MapScreen() {
<ActivityIndicator color={colors.sageDark} />
</View>
) : null}
{poiError ? (
<View style={styles.poiError}>
<Text style={styles.poiErrorText}>{poiError}</Text>
</View>
) : null}
<MapHUD onCreateBillboard={() => setCreateOpen(true)} />
<Modal
visible={activeBillboardId !== null}
Expand Down Expand Up @@ -157,6 +194,25 @@ export default function MapScreen() {
);
}

function resolveCheckInPosition(fallback: { lat: number; lng: number }) {
if (typeof navigator === "undefined" || !navigator.geolocation) {
return Promise.resolve(fallback);
}

return new Promise<{ lat: number; lng: number }>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
},
reject,
{ enableHighAccuracy: true, maximumAge: 30_000, timeout: 5_000 },
);
});
}

const styles = StyleSheet.create({
root: {
flex: 1,
Expand All @@ -174,6 +230,23 @@ const styles = StyleSheet.create({
top: 18,
width: 42,
},
poiError: {
alignSelf: "center",
backgroundColor: "#F6D7CE",
borderColor: colors.pinRedDark,
borderRadius: 12,
borderWidth: 2,
maxWidth: 360,
paddingHorizontal: 14,
paddingVertical: 10,
position: "absolute",
top: 18,
},
poiErrorText: {
color: colors.pinRedDark,
fontSize: 16,
textAlign: "center",
},
modalRoot: {
flex: 1,
justifyContent: "center",
Expand Down
Loading
Loading