a
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useReducer, useTransition } from "react";
|
||||
import { useEffect, useReducer, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
makeInitialState,
|
||||
reducer,
|
||||
type InitialNav,
|
||||
type Screen,
|
||||
type Tab,
|
||||
} from "@/lib/app-state";
|
||||
@@ -21,17 +22,35 @@ import { CollectionDetailScreen } from "@/screens/collection-detail-screen";
|
||||
import { ProfileScreen } from "@/screens/profile-screen";
|
||||
import { AddPlaceSheet } from "@/sheets/add-place-sheet";
|
||||
import { EditPlaceSheet } from "@/sheets/edit-place-sheet";
|
||||
import { EditProfileSheet } from "@/sheets/edit-profile-sheet";
|
||||
import { SaveToCollectionSheet } from "@/sheets/save-to-collection-sheet";
|
||||
import { CollectionFormSheet } from "@/sheets/collection-form-sheet";
|
||||
import { InviteDialog } from "@/sheets/invite-dialog";
|
||||
import { MembersSheet, ConfirmDialog } from "@/sheets/members-sheet";
|
||||
import { DesktopShell } from "@/desktop/desktop-shell";
|
||||
|
||||
// Tracks a CSS media query reactively. SSR-safe: returns `false` until the
|
||||
// first effect tick on the client, then matches the real viewport.
|
||||
function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia(query);
|
||||
setMatches(mq.matches);
|
||||
const onChange = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
mq.addEventListener("change", onChange);
|
||||
return () => mq.removeEventListener("change", onChange);
|
||||
}, [query]);
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function PlacesApp({
|
||||
initialPlaces,
|
||||
data,
|
||||
initialNav,
|
||||
}: {
|
||||
initialPlaces: Place[];
|
||||
data: AppData;
|
||||
initialNav?: InitialNav;
|
||||
}) {
|
||||
// If we boot up offline, prefer the cached snapshot so the UI has *some*
|
||||
// data to show. When network returns, the visibility/focus effect refreshes.
|
||||
@@ -42,14 +61,21 @@ export function PlacesApp({
|
||||
if (snap) {
|
||||
return {
|
||||
places: snap.places,
|
||||
data: { me: snap.me, users: snap.users, collections: snap.collections },
|
||||
data: {
|
||||
me: snap.me,
|
||||
users: snap.users,
|
||||
collections: snap.collections,
|
||||
publicPlaces: snap.publicPlaces ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return { places: initialPlaces, data };
|
||||
})();
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, bootData.places, makeInitialState);
|
||||
const [state, dispatch] = useReducer(reducer, bootData.places, (p) =>
|
||||
makeInitialState(p, initialNav),
|
||||
);
|
||||
const [, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -66,6 +92,7 @@ export function PlacesApp({
|
||||
users: data.users,
|
||||
collections: data.collections,
|
||||
places: state.places,
|
||||
publicPlaces: data.publicPlaces,
|
||||
});
|
||||
}, [data, state.places]);
|
||||
|
||||
@@ -155,17 +182,39 @@ export function PlacesApp({
|
||||
? bootData.data.collections.find((c) => c.id === m.collectionId)
|
||||
: null;
|
||||
|
||||
// ≥1024px: switch to the 3-pane desktop shell. Modals and toast are still
|
||||
// rendered below so both layouts share the same sheets/dialogs.
|
||||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
|
||||
// On first desktop mount, seed selectedPlaceId so the right pane has
|
||||
// something to show. Collection seeding is deferred to DesktopShell (it
|
||||
// happens lazily when the user clicks the Collections tab) because the
|
||||
// SELECT_COLLECTION action also flips the active tab — running it here
|
||||
// would yank a freshly-loaded user into Collections.
|
||||
useEffect(() => {
|
||||
if (!isDesktop) return;
|
||||
if (state.selectedPlaceId == null && state.places.length > 0) {
|
||||
dispatch({ type: "SELECT_PLACE", placeId: state.places[0].id });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDesktop]);
|
||||
|
||||
return (
|
||||
<AppDataProvider value={bootData.data}>
|
||||
<div className="app-frame">
|
||||
{renderScreen(top.screen)}
|
||||
<TabBar
|
||||
active={activeTab}
|
||||
onTab={onTab}
|
||||
onFab={() => dispatch({ type: "OPEN_ADD" })}
|
||||
showFab={top.screen !== "profile"}
|
||||
/>
|
||||
|
||||
{isDesktop ? (
|
||||
<DesktopShell state={state} dispatch={dispatch} />
|
||||
) : (
|
||||
<div className="app-frame">
|
||||
{renderScreen(top.screen)}
|
||||
<TabBar
|
||||
active={activeTab}
|
||||
onTab={onTab}
|
||||
onFab={() => dispatch({ type: "OPEN_ADD" })}
|
||||
showFab={top.screen !== "profile"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
{m?.type === "add" && (
|
||||
<AddPlaceSheet
|
||||
onClose={() => dispatch({ type: "CLOSE_MODAL" })}
|
||||
@@ -207,6 +256,12 @@ export function PlacesApp({
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
{m?.type === "editProfile" && (
|
||||
<EditProfileSheet
|
||||
onClose={() => dispatch({ type: "CLOSE_MODAL" })}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
)}
|
||||
{m?.type === "invite" && (
|
||||
<InviteDialog
|
||||
collectionId={m.collectionId}
|
||||
@@ -263,7 +318,7 @@ export function PlacesApp({
|
||||
{state.toast}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</AppDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user