Skip to content
Open
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
10 changes: 9 additions & 1 deletion apps/app/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@
}
}
],
"expo-secure-store"
"expo-secure-store",
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow UNSW Connect to use your location.",
"locationWhenInUsePermission": "Allow UNSW Connect to use your location."
}
],
"@maplibre/maplibre-react-native"
],
"experiments": {
"typedRoutes": true,
Expand Down
32 changes: 27 additions & 5 deletions apps/app/app/(app)/map.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useCallback, useRef, useState } from "react";
import {
ActivityIndicator,
Linking,
Modal,
Pressable,
ScrollView,
Expand All @@ -11,21 +12,26 @@ import {
} from "react-native";

import { BillboardPanel } from "@/components/billboard/BillboardPanel";
import { Map } from "@/components/map/Map";
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<string | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [body, setBody] = useState("");
const [createError, setCreateError] = useState<string | null>(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 closeCreate = () => {
setCreateOpen(false);
Expand Down Expand Up @@ -59,15 +65,26 @@ export default function MapScreen() {
setCreateError(err.message);
return;
}
setCreateError("Could not pin this whiteboard.");
console.log(err.message);
setCreateError("Could not pin this billboard.");
},
},
);
};

const handleEnableLocation = useCallback(async () => {
if (canAskAgain) {
await requestPermission();
} else {
Linking.openURL("app-settings:");
}
}, [canAskAgain, requestPermission]);

return (
<View style={styles.root}>
<Map
ref={mapRef}
location={location}
billboards={(billboards.data ?? []).map((billboard) => ({
id: billboard.id,
title: billboard.body,
Expand All @@ -89,7 +106,11 @@ export default function MapScreen() {
<ActivityIndicator color={colors.sageDark} />
</View>
) : null}
<MapHUD onCreateBillboard={() => setCreateOpen(true)} />
<MapHUD
onCreateBillboard={() => setCreateOpen(true)}
isPermissionDenied={isDenied}
onEnableLocation={handleEnableLocation}
/>
<Modal
visible={activeBillboardId !== null}
transparent
Expand Down Expand Up @@ -117,7 +138,7 @@ export default function MapScreen() {
<Pressable style={styles.backdrop} onPress={closeCreate} />
<View style={styles.createPanel}>
<View style={styles.createHeader}>
<Text style={styles.createTitle}>New whiteboard</Text>
<Text style={styles.createTitle}>New billboard</Text>
<Pressable
onPress={closeCreate}
style={styles.closeButton}
Expand Down Expand Up @@ -153,6 +174,7 @@ export default function MapScreen() {
</View>
</View>
</Modal>
<CanvasModal visible={isCanvasOpen} onClose={() => setIsCanvasOpen(false)} />
</View>
);
}
Expand Down
1 change: 0 additions & 1 deletion apps/app/app/(auth)/sign-in.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default function SignInScreen() {
const router = useRouter();

useEffect(() => {
alert(isSignedIn);
if (isSignedIn) {
router.replace("/(app)/map");
}
Expand Down
2 changes: 1 addition & 1 deletion apps/app/app/billboard/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function BillboardDetailScreen() {

return (
<Screen>
<Stack.Screen options={{ title: "Whiteboard", headerLeft: renderBackButton }} />
<Stack.Screen options={{ title: "Billboard", headerLeft: renderBackButton }} />
<BillboardPanel id={id} />
</Screen>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/app/components/billboard/BillboardPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function BillboardPanel({ id, onClose }: BillboardPanelProps) {
if (billboard.isError || !billboard.data) {
return (
<View style={styles.empty}>
<Text style={styles.emptyTitle}>Whiteboard not found</Text>
<Text style={styles.emptyTitle}>Billboard not found</Text>
<Text style={styles.emptyBody}>
{(billboard.error as Error | undefined)?.message ?? "It may have expired."}
</Text>
Expand Down
49 changes: 21 additions & 28 deletions apps/app/components/map/Map.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Camera, Map as MapLibre, Marker } from "@maplibre/maplibre-react-native
import { fonts } from "@/app/theme";
import { UNSW_CENTER } from "@/constants/coordinates";

import type { MapPoi, MapProps } from "./Map";
import type { MapPoi, MapProps } from "./Map.types";

const THUNDERFOREST_API_KEY = process.env.EXPO_PUBLIC_THUNDERFOREST_KEY ?? "YOUR_API_KEY_HERE";

Expand Down Expand Up @@ -45,21 +45,19 @@ interface POIMarkerProps {

function POIMarker({ poi, isSelected, onPress }: POIMarkerProps) {
return (
<Marker lngLat={[poi.lng, poi.lat]} anchor="bottom">
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<View style={poiStyles.markerContainer}>
<View style={poiStyles.topCircle} />
<Marker lngLat={[poi.lng, poi.lat]} anchor="bottom" onPress={onPress}>
<View style={poiStyles.markerContainer}>
<View style={poiStyles.topCircle} />

<View style={[poiStyles.mainBox, isSelected && poiStyles.mainBoxSelected]}>
{/* Embedded Cross Lines */}
<View style={poiStyles.verticalLine} />
<View style={poiStyles.horizontalLine} />
</View>

{/* Base Platform Bar (x=0, y=28, w=32, h=4) */}
<View style={poiStyles.baseBar} />
<View style={[poiStyles.mainBox, isSelected && poiStyles.mainBoxSelected]}>
{/* Embedded Cross Lines */}
<View style={poiStyles.verticalLine} />
<View style={poiStyles.horizontalLine} />
</View>
</TouchableOpacity>

{/* Base Platform Bar (x=0, y=28, w=32, h=4) */}
<View style={poiStyles.baseBar} />
</View>
</Marker>
);
}
Expand All @@ -76,15 +74,15 @@ function BillboardMarker({
title: string;
}) {
return (
<Marker lngLat={[lng, lat]} anchor="bottom">
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Marker lngLat={[lng, lat]} anchor="bottom" onPress={onPress}>
<View>
<View style={billboardStyles.marker}>
<Text style={billboardStyles.text} numberOfLines={1}>
{title.slice(0, 2)}
</Text>
</View>
<View style={billboardStyles.pin} />
</TouchableOpacity>
</View>
</Marker>
);
}
Expand All @@ -110,22 +108,20 @@ function UserAvatarMarker({ coordinate, imageUrl }: UserAvatarMarkerProps) {
}

export default forwardRef<{ invalidateSize: () => void }, MapProps>(function Map(
{ billboards, onBillboardPress, pois },
{ location, billboards, onBillboardPress, pois },
_ref,
) {
const [selectedPOI, setSelectedPOI] = useState<MapPoi | null>(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 (
<View style={styles.container}>
<MapLibre style={styles.map} mapStyle={MAP_STYLE} onPress={() => setSelectedPOI(null)}>
<Camera
initialViewState={{
center: [UNSW_CENTER.lng, UNSW_CENTER.lat],
zoom: 18,
}}
/>
<Camera center={userCoord} zoom={18} duration={1000} easing="fly" />
{pois.map((poi) => (
<POIMarker
key={poi.id}
Expand All @@ -145,10 +141,7 @@ export default forwardRef<{ invalidateSize: () => void }, MapProps>(function Map
/>
))}

<UserAvatarMarker
coordinate={[UNSW_CENTER.lng, UNSW_CENTER.lat]}
imageUrl={userAvatarUrl}
/>
<UserAvatarMarker coordinate={userCoord} imageUrl={userAvatarUrl} />
</MapLibre>

{selectedPOI && (
Expand Down
35 changes: 1 addition & 34 deletions apps/app/components/map/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1 @@
import { forwardRef } from "react";
import { Platform } from "react-native";
import MapNative from "./Map.native";
import MapWeb from "./Map.web";

export type MapHandle = { invalidateSize: () => void };

export type MapPoint = {
id: string;
title: string;
lat: number;
lng: number;
};

export type MapPoi = MapPoint & {
description?: string | null;
visited?: boolean;
};

export type MapProps = {
billboards: MapPoint[];
onBillboardPress?: (id: string) => void;
pois: MapPoi[];
};

export const Map = forwardRef<MapHandle, MapProps>(function Map(props, ref) {
if (Platform.OS !== "web") {
return <MapNative {...props} ref={ref} />;
}

return <MapWeb {...props} ref={ref} />;
});

export default Map;
export { default } from "./Map.web";
25 changes: 25 additions & 0 deletions apps/app/components/map/Map.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type MapHandle = { invalidateSize: () => void };

export type MapPoint = {
id: string;
title: string;
lat: number;
lng: number;
};

export type MapPoi = MapPoint & {
description?: string | null;
visited?: boolean;
};

export type MapLocation = {
latitude: number;
longitude: number;
};

export type MapProps = {
location?: MapLocation | null;
billboards: MapPoint[];
onBillboardPress?: (id: string) => void;
pois: MapPoi[];
};
24 changes: 13 additions & 11 deletions apps/app/components/map/Map.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Platform, StyleSheet, View } from "react-native";
import { UNSW_CENTER } from "@/constants/coordinates";
import { useUserProfile } from "@/lib/userProfile";

import type { MapPoint, MapPoi } from "./Map";
import type { MapProps } from "./Map.types";
import { createBillboardIcon, createPOIIcon, createUserAvatarIcon } from "./markers";

const DRAWN_AVATAR_BG = "#faf7ef";
Expand All @@ -18,14 +18,8 @@ const TILE_ATTR =

type MapHandle = { invalidateSize: () => void };

type MapProps = {
billboards: MapPoint[];
onBillboardPress?: (id: string) => void;
pois: MapPoi[];
};

export const Map = forwardRef<MapHandle, MapProps>(function MapWeb(
{ billboards, onBillboardPress, pois },
{ location, billboards, onBillboardPress, pois },
ref,
) {
const containerRef = useRef<View | null>(null);
Expand All @@ -34,9 +28,7 @@ export const Map = forwardRef<MapHandle, MapProps>(function MapWeb(
const { avatarUri } = useUserProfile();

useImperativeHandle(ref, () => ({
invalidateSize: () => {
mapRef.current?.invalidateSize();
},
invalidateSize: () => mapRef.current?.invalidateSize(),
}));

useEffect(() => {
Expand Down Expand Up @@ -104,6 +96,16 @@ export const Map = forwardRef<MapHandle, MapProps>(function MapWeb(
);
}, [avatarUri]);

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 <View ref={containerRef} style={styles.container} />;
});

Expand Down
Loading
Loading