From 092d142a56bea24c1ae28fa1081b970afe8799c3 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 18 Mar 2026 13:11:29 -0700 Subject: [PATCH] refactor: restructure state, routing, and more --- js/src/App.tsx | 21 +- .../layoutwithpath.tsx} | 53 +--- js/src/features/network-root.tsx | 18 ++ js/src/features/network-selector.tsx | 2 +- js/src/features/particles/folder-view.tsx | 12 +- .../features/particles/particle-list-view.tsx | 15 +- .../particles/particle-view-resolver.tsx | 39 ++- js/src/features/particles/stream-view.tsx | 12 +- js/src/features/send/use-recorder.ts | 35 ++- js/src/{pages => features}/settings-page.tsx | 4 +- js/src/hooks/use-particle-children.ts | 46 --- js/src/hooks/use-particle.ts | 62 +++- js/src/lib/firestore-paths.ts | 23 -- js/src/lib/particle-path.ts | 67 ++++ js/src/pages/stream-player-page.tsx | 285 ------------------ js/src/stores/playback-store.ts | 6 +- 16 files changed, 218 insertions(+), 482 deletions(-) rename js/src/{pages/path-resolver.tsx => features/layoutwithpath.tsx} (69%) create mode 100644 js/src/features/network-root.tsx rename js/src/{pages => features}/settings-page.tsx (98%) delete mode 100644 js/src/hooks/use-particle-children.ts delete mode 100644 js/src/lib/firestore-paths.ts create mode 100644 js/src/lib/particle-path.ts delete mode 100644 js/src/pages/stream-player-page.tsx diff --git a/js/src/App.tsx b/js/src/App.tsx index 8638d5f..b55f52a 100644 --- a/js/src/App.tsx +++ b/js/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { HashRouter, Routes, Route } from "react-router-dom"; +import { HashRouter, Routes, Route, Outlet } from "react-router-dom"; import { TooltipProvider } from "@/components/ui/tooltip"; import { useAuthStore } from "@/stores/auth-store"; import { LoginPage } from "@/features/auth/login-page"; @@ -8,8 +8,11 @@ import { QueryClient, QueryClientProvider, } from '@tanstack/react-query' -import PathResolver from "./pages/path-resolver"; -import { SettingsPage } from "./pages/settings-page"; +import SettingsPage from "@/features/settings-page"; +import NetworkSelector from "@/features/network-selector"; +import NetworkRoot from "@/features/network-root"; +import ParticleViewResolver from "@/features/particles/particle-view-resolver"; +import LayoutWithPath from "@/features/layoutwithpath"; const queryClient = new QueryClient(); @@ -37,12 +40,18 @@ const App = () => { }; function AuthenticatedApp() { - // NOTE: Hash router provides history, despite using catch-all return ( - } /> - } /> + } /> + + }> + } /> + + } /> + } /> + + ); diff --git a/js/src/pages/path-resolver.tsx b/js/src/features/layoutwithpath.tsx similarity index 69% rename from js/src/pages/path-resolver.tsx rename to js/src/features/layoutwithpath.tsx index 482e40f..be74cf0 100644 --- a/js/src/pages/path-resolver.tsx +++ b/js/src/features/layoutwithpath.tsx @@ -1,10 +1,7 @@ -import { useLocation, useNavigate } from "react-router-dom"; -import { Home, Settings } from "lucide-react"; -import { NetworkSelector } from "@/features/network-selector"; -import { ParticleListView } from "@/features/particles/particle-list-view"; -import { ParticleViewResolver } from "@/features/particles/particle-view-resolver"; import { WindowControls } from "@/components/window-controls"; import { Button } from "@/components/ui/button"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Home, Settings } from "lucide-react"; import { Breadcrumb, BreadcrumbItem, @@ -14,13 +11,8 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { useAuthStore } from "@/stores/auth-store"; import { useNetworks } from "@/hooks/use-networks"; -function parsePathSegments(path: string): string[] { - return path.split("/").filter(Boolean); -} - function NetworkBreadcrumbContent({ networkId }: { networkId: string }) { const { data: networks } = useNetworks(); const network = networks?.find((n) => n.id === networkId); @@ -39,8 +31,9 @@ function NetworkBreadcrumbContent({ networkId }: { networkId: string }) { ); } -function TopBar({ segments }: { segments: string[] }) { +function TopBar() { const navigate = useNavigate(); + const segments = useLocation().pathname.split("/").filter(Boolean); const networkId = segments[0] ?? null; return ( @@ -53,7 +46,6 @@ function TopBar({ segments }: { segments: string[] }) { {segments.length === 0 ? ( - Networks ) : ( navigate("/")} > - Networks )} @@ -125,41 +116,11 @@ function TopBar({ segments }: { segments: string[] }) { ); } -/** - * URL structure: - * / → network selector - * /:networkId → root particles for that network - * /:networkId/:p1/:p2/... → nested particle view (renders the parent which will use it's children) - */ -export default function PathResolver() { - const segments = parsePathSegments(useLocation().pathname); - - const content = (() => { - // No segments → show network selector - if (segments.length === 0) { - return ; - } - - const [networkId, ...particleSegments] = segments; - - // /:networkId with no particle segments → root particle list - if (particleSegments.length === 0) { - return ; - } - - // /:networkId/:p1/:p2/... → resolve and render the container particle - return ( - - ); - })(); - +export default function LayoutWithPath() { return (
- -
{content}
+ +
); } diff --git a/js/src/features/network-root.tsx b/js/src/features/network-root.tsx new file mode 100644 index 0000000..b4458dc --- /dev/null +++ b/js/src/features/network-root.tsx @@ -0,0 +1,18 @@ +import { useParams } from "react-router-dom"; +import { particlePath } from "@/lib/particle-path"; +import { ParticleListView } from "@/features/particles/particle-list-view"; + +/** + * Route-level component for /:networkId (index). + * Shows root-level particles for the selected network. + */ +export default function NetworkRoot() { + const { networkId } = useParams(); + const path = particlePath(networkId!, []); + + return ( +
+ +
+ ); +} diff --git a/js/src/features/network-selector.tsx b/js/src/features/network-selector.tsx index 4998e5e..22b2bc6 100644 --- a/js/src/features/network-selector.tsx +++ b/js/src/features/network-selector.tsx @@ -42,7 +42,7 @@ function NetworkRow({ ); } -export function NetworkSelector() { +export default function NetworkSelector() { const navigate = useNavigate(); const { data, isPending, error } = useNetworks(); diff --git a/js/src/features/particles/folder-view.tsx b/js/src/features/particles/folder-view.tsx index 41866d4..4100434 100644 --- a/js/src/features/particles/folder-view.tsx +++ b/js/src/features/particles/folder-view.tsx @@ -1,19 +1,19 @@ import { Particle } from "@/api/types"; -import { useParticleChildren } from "@/hooks/use-particle-children"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import type { ParticlePath } from "@/lib/particle-path"; interface FolderViewProps { folderParticle: Particle; - networkId: string; - particleSegments: string[]; + path: ParticlePath; } -export function FolderView({ networkId, particleSegments, folderParticle }: FolderViewProps) { - const { children, error, isLoading } = useParticleChildren(networkId, particleSegments); +export function FolderView({ path, folderParticle }: FolderViewProps) { + const { children, error, isLoading } = useLiveParticleChildren(path); return (

- Folder view — {networkId}/{particleSegments.join("/")} + Folder view — {folderParticle.id}

); diff --git a/js/src/features/particles/particle-list-view.tsx b/js/src/features/particles/particle-list-view.tsx index 54f66cd..0d3205d 100644 --- a/js/src/features/particles/particle-list-view.tsx +++ b/js/src/features/particles/particle-list-view.tsx @@ -1,15 +1,16 @@ -import { useParticleChildren } from "@/hooks/use-particle-children"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import type { ParticlePath } from "@/lib/particle-path"; +import ControlsIndicator from "@/features/send/controls-indicator"; interface ParticleListViewProps { - networkId: string; - particleSegments: string[]; + path: ParticlePath; } /** * Grid/list of child particles for a container (folder, stream root, or network root). */ -export function ParticleListView({ networkId, particleSegments }: ParticleListViewProps) { - const { children, isLoading } = useParticleChildren(networkId, particleSegments); +export function ParticleListView({ path }: ParticleListViewProps) { + const { children, isLoading } = useLiveParticleChildren(path); if (isLoading) { return ( @@ -21,8 +22,8 @@ export function ParticleListView({ networkId, particleSegments }: ParticleListVi if (children.length === 0) { return ( -
-

No particles yet

+
+
); } diff --git a/js/src/features/particles/particle-view-resolver.tsx b/js/src/features/particles/particle-view-resolver.tsx index 7d05ea3..76ef87f 100644 --- a/js/src/features/particles/particle-view-resolver.tsx +++ b/js/src/features/particles/particle-view-resolver.tsx @@ -1,20 +1,22 @@ -import { useParticle } from "@/hooks/use-particle"; -import { StreamView } from "./stream-view"; -import { FolderView } from "./folder-view"; -import { ParticleListView } from "./particle-list-view"; +import { useParams } from "react-router-dom"; +import { useLiveParticle } from "@/hooks/use-particle"; +import { particlePath } from "@/lib/particle-path"; import { isContainerType } from "@/api/types"; - -interface ParticleViewResolverProps { - networkId: string; - particleSegments: string[]; -} +import { StreamView } from "@/features/particles/stream-view"; +import { FolderView } from "@/features/particles/folder-view"; +import { ParticleListView } from "@/features/particles/particle-list-view"; /** - * Resolves a particle by its path segments and renders the appropriate view - * based on particle type (e.g. stream would show clips in story mode, folder would list files, etc.) + * Route-level component for /:networkId/*. + * Reads params from the router, resolves the particle, and renders + * the appropriate view based on particle type. */ -export function ParticleViewResolver({ networkId, particleSegments }: ParticleViewResolverProps) { - const { particle, isLoading, error } = useParticle(networkId, particleSegments); +export default function ParticleViewResolver() { + const { networkId, "*": rest } = useParams(); + const segments = (rest ?? "").split("/").filter(Boolean); + const path = particlePath(networkId!, segments); + + const { particle, isLoading, error } = useLiveParticle(path); if (isLoading) { return ( @@ -32,12 +34,11 @@ export function ParticleViewResolver({ networkId, particleSegments }: ParticleVi ); } - // While the hook is stubbed, particle will be null — show a placeholder if (!particle) { return (

- Particle: {particleSegments.join(" / ")} + Particle: {segments.join(" / ")}

); @@ -45,15 +46,13 @@ export function ParticleViewResolver({ networkId, particleSegments }: ParticleVi switch (particle.type) { case "stream": - return ; + return ; case "folder": - return ; + return ; default: - // For container types we haven't built a view for, fall back to list if (isContainerType(particle.type)) { - return ; + return ; } - // Leaf particle — placeholder return (

diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 836ca16..3bb7514 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -1,19 +1,19 @@ import { Particle } from "@/api/types"; -import { useParticleChildren } from "@/hooks/use-particle-children"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import type { ParticlePath } from "@/lib/particle-path"; interface StreamViewProps { streamParticle: Particle; - networkId: string; - particleSegments: string[]; + path: ParticlePath; } -export function StreamView({ networkId, particleSegments, streamParticle }: StreamViewProps) { - const { children, error, isLoading } = useParticleChildren(networkId, particleSegments); +export function StreamView({ path, streamParticle }: StreamViewProps) { + const { children, error, isLoading } = useLiveParticleChildren(path); return (

- Stream view — {networkId}/{particleSegments.join("/")} + Stream view — {streamParticle.id}

{isLoading &&

Loading stream data...

} diff --git a/js/src/features/send/use-recorder.ts b/js/src/features/send/use-recorder.ts index 60dcebc..f46b364 100644 --- a/js/src/features/send/use-recorder.ts +++ b/js/src/features/send/use-recorder.ts @@ -36,7 +36,6 @@ export function useRecorder( const setMediaStream = useRecordingStore((s) => s.setMediaStream); const setReviewBlob = useRecordingStore((s) => s.setReviewBlob); const resetRecording = useRecordingStore((s) => s.reset); - const addParticleToStream = useAppStore((s) => s.addParticleToStream); const stopTracks = useCallback(() => { streamRef.current?.getTracks().forEach((t) => t.stop()); @@ -73,29 +72,29 @@ export function useRecorder( await apiClient.confirmUpload(object_id); - const particle = await apiClient.createStreamParticle(streamId, { - type: "media", - data: { - object_id, - duration_ms: reviewDurationMs, - mime_type: mimeType, - }, - }); + // const particle = await apiClient.createStreamParticle(streamId, { + // type: "media", + // data: { + // object_id, + // duration_ms: reviewDurationMs, + // mime_type: mimeType, + // }, + // }); + // + // addParticleToStream(streamId, particle); - addParticleToStream(streamId, particle); - - const playbackState = usePlaybackStore.getState(); - if (playbackState.streamId === streamId) { - usePlaybackStore.setState({ - particles: [...playbackState.particles, particle], - }); - } + // const playbackState = usePlaybackStore.getState(); + // if (playbackState.streamId === streamId) { + // usePlaybackStore.setState({ + // particles: [...playbackState.particles, particle], + // }); + // } resetRecording(); } catch (err) { setError(err instanceof Error ? err.message : "Upload failed"); } - }, [streamId, networkId, setStatus, setError, resetRecording, addParticleToStream]); + }, [streamId, networkId, setStatus, setError, resetRecording]); const startRecording = useCallback(async () => { const currentStatus = useRecordingStore.getState().status; diff --git a/js/src/pages/settings-page.tsx b/js/src/features/settings-page.tsx similarity index 98% rename from js/src/pages/settings-page.tsx rename to js/src/features/settings-page.tsx index bd938d7..a6d76bb 100644 --- a/js/src/pages/settings-page.tsx +++ b/js/src/features/settings-page.tsx @@ -59,7 +59,7 @@ function SettingsGroup({ ); } -export function SettingsPage() { +export default function SettingsPage() { const navigate = useNavigate(); const user = useAuthStore((s) => s.user); const signOut = useAuthStore((s) => s.signOut); @@ -68,7 +68,7 @@ export function SettingsPage() { return (
-
+
-
-
-
-

- No particles yet. Hold ` to record the first one. -

-
-
- - {stream.name} - - -
- - {composingText && streamId && ( - setComposingText(false)} - /> - )} - - {showRecordingOverlay && ( - - )} -
- ); - } - - const currentParticle = particles[currentIndex]; - - return ( -
- {/* Particle content — fills entire viewport */} -
- {currentParticle && ( - - )} -
- - {/* Top overlay: progress bars + back button */} -
-
- -
-
- -
-
- - {/* Top center overlay for particle author avatar and name */} -
-
- {currentParticle && ( - - - {currentParticle.created_by_email - .split("@")[0] - .slice(0, 2) - .toUpperCase()} - - - )} - - {currentParticle?.created_by_email.split("@")[0]} - · - {stream.name} - -
-
- - {/* Bottom overlay: stream info + reply */} -
-
-
- _this is a placeholder for captions_ -
- - -
-
- - {/* Text compose overlay */} - {composingText && streamId && ( - setComposingText(false)} - /> - )} - - {/* Recording overlay */} - {showRecordingOverlay && ( - - )} -
- ); -} diff --git a/js/src/stores/playback-store.ts b/js/src/stores/playback-store.ts index d199e4f..70df166 100644 --- a/js/src/stores/playback-store.ts +++ b/js/src/stores/playback-store.ts @@ -1,11 +1,11 @@ import { create } from "zustand"; -import type { StreamParticle } from "@/api/types"; +import type { Particle } from "@/api/types"; type PlaybackStatus = "idle" | "playing" | "ended"; interface PlaybackState { streamId: string | null; - particles: StreamParticle[]; + particles: Particle[]; currentIndex: number; status: PlaybackStatus; paused: boolean; @@ -13,7 +13,7 @@ interface PlaybackState { initStream: ( streamId: string, - particles: StreamParticle[], + particles: Particle[], startIndex: number, ) => void; next: () => void;