From f05f7be8b00ddd727164c807d575c03652928c3a Mon Sep 17 00:00:00 2001 From: assistant Date: Tue, 31 Mar 2026 03:01:41 +0000 Subject: [PATCH] feat(stages): E2E stage creation with 3D builder, map picker, music upload, and visualization selector --- .../musician/stages/create/page.tsx | 7 +- .../(musician)/musician/stages/map/page.tsx | 75 ++++++ .../(musician)/musician/stages/new/page.tsx | 219 +----------------- src/app/(musician)/musician/stages/page.tsx | 19 +- src/app/api/stages/route.ts | 6 +- src/components/stages/MapPicker.tsx | 93 +++++++- 6 files changed, 180 insertions(+), 239 deletions(-) create mode 100644 src/app/(musician)/musician/stages/map/page.tsx diff --git a/src/app/(musician)/musician/stages/create/page.tsx b/src/app/(musician)/musician/stages/create/page.tsx index 48a7e0d..04f348e 100644 --- a/src/app/(musician)/musician/stages/create/page.tsx +++ b/src/app/(musician)/musician/stages/create/page.tsx @@ -211,12 +211,7 @@ export default function CreateStagePage() { } toast.success(publish ? "Stage published!" : "Stage created!"); - - if (publish) { - router.push("/explore"); - } else { - router.push(`/musician/stages/${stage.id}/visualize`); - } + router.push(`/musician/stages/${stage.id}`); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to create stage"); } finally { diff --git a/src/app/(musician)/musician/stages/map/page.tsx b/src/app/(musician)/musician/stages/map/page.tsx new file mode 100644 index 0000000..10956d6 --- /dev/null +++ b/src/app/(musician)/musician/stages/map/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { StageMap } from "@/components/stages/StageMap"; +import { StageMapStage } from "@/components/stages/StageMap"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { ArrowLeft, Plus, Loader2 } from "lucide-react"; + +export default function StageMapPage() { + const [stages, setStages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchStages() { + try { + const res = await fetch("/api/stages"); + const json = await res.json(); + setStages(json.data || []); + } catch { + setError("Failed to load stages"); + } finally { + setLoading(false); + } + } + fetchStages(); + }, []); + + return ( +
+ {/* Header */} +
+
+ + + +
+

Stage Map

+

+ {loading ? "Loading..." : `${stages.length} stage${stages.length !== 1 ? "s" : ""}`} +

+
+
+ + + +
+ + {/* Map */} +
+ {loading && ( +
+ +
+ )} + {error && ( +
+

{error}

+
+ )} + {!loading && !error && ( + + )} +
+
+ ); +} diff --git a/src/app/(musician)/musician/stages/new/page.tsx b/src/app/(musician)/musician/stages/new/page.tsx index 9824f4c..7040032 100644 --- a/src/app/(musician)/musician/stages/new/page.tsx +++ b/src/app/(musician)/musician/stages/new/page.tsx @@ -1,223 +1,14 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Slider } from "@/components/ui/slider"; -import { MapPin, Navigation, Loader2, CheckCircle, Map } from "lucide-react"; -import { toast } from "sonner"; -import { MapPicker } from "@/components/stages/MapPicker"; -export default function NewStagePage() { +export default function NewStageRedirect() { const router = useRouter(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [lat, setLat] = useState(""); - const [lng, setLng] = useState(""); - const [radius, setRadius] = useState([50]); - const [loading, setLoading] = useState(false); - const [locating, setLocating] = useState(false); - const [showMap, setShowMap] = useState(false); - function useMyLocation() { - setLocating(true); - navigator.geolocation.getCurrentPosition( - (pos) => { - setLat(pos.coords.latitude); - setLng(pos.coords.longitude); - setLocating(false); - toast.success("Location captured!"); - }, - () => { - toast.error("Could not get location. Please enter manually."); - setLocating(false); - }, - { enableHighAccuracy: true } - ); - } - - // Initialize with current location or default useEffect(() => { - if (!lat && !lng && navigator.geolocation) { - setLocating(true); - navigator.geolocation.getCurrentPosition( - (pos) => { - setLat(pos.coords.latitude); - setLng(pos.coords.longitude); - setLocating(false); - }, - () => { - // Default to SF if geolocation fails - setLat(37.7749); - setLng(-122.4194); - setLocating(false); - } - ); - } - }, []); - - function handleLocationChange(newLat: number, newLng: number) { - setLat(newLat); - setLng(newLng); - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!lat || !lng) { toast.error("Latitude and longitude are required."); return; } - - setLoading(true); - const res = await fetch("/api/stages", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: name.trim(), - description: description.trim() || undefined, - latitude: lat, - longitude: lng, - radius: radius[0], - }), - }); - - setLoading(false); - - if (!res.ok) { - const err = await res.json(); - toast.error(err.error ?? "Failed to create stage."); - return; - } - - const { data } = await res.json(); - toast.success("Stage created!"); - router.push(`/musician/stages/${data.id}/visualize`); - } - - return ( -
-

Create Stage

-

Place an AR stage at a GPS location

- -
-
- - setName(e.target.value)} - placeholder="e.g. The Underground, Rooftop Rave" - required - className="bg-zinc-900 border-zinc-700 text-white placeholder:text-zinc-600 focus:border-violet-500 h-11" - /> -
- -
- - setDescription(e.target.value)} - placeholder="Optional — describe this venue" - className="bg-zinc-900 border-zinc-700 text-white placeholder:text-zinc-600 focus:border-violet-500 h-11" - /> -
- - {/* Location */} -
-
- -
- - -
-
- - {/* Map Picker */} - {showMap && lat && lng && ( - - )} - - {/* Manual coordinates input */} -
-
- - setLat(e.target.value ? parseFloat(e.target.value) : "")} - placeholder="37.7749" - required - className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-600 h-10 text-sm font-mono" - /> -
-
- - setLng(e.target.value ? parseFloat(e.target.value) : "")} - placeholder="-122.4194" - required - className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-600 h-10 text-sm font-mono" - /> -
-
- -
-
- - {radius[0]}m -
- -

Users within {radius[0]}m will see this stage in AR

-
-
+ router.replace("/musician/stages/create"); + }, [router]); - -
-
- ); + return null; } diff --git a/src/app/(musician)/musician/stages/page.tsx b/src/app/(musician)/musician/stages/page.tsx index 917d9aa..94e0d0c 100644 --- a/src/app/(musician)/musician/stages/page.tsx +++ b/src/app/(musician)/musician/stages/page.tsx @@ -28,18 +28,25 @@ export default async function StagesPage() {

Stages

{stages.length} GPS-anchored stage{stages.length !== 1 ? "s" : ""}

- - - +
+ + + + + + +
{stages.length === 0 ? (

No stages yet.

- + diff --git a/src/app/api/stages/route.ts b/src/app/api/stages/route.ts index 2bfe02e..8267612 100644 --- a/src/app/api/stages/route.ts +++ b/src/app/api/stages/route.ts @@ -59,7 +59,11 @@ export async function GET(request: NextRequest) { let stages = await prisma.stage.findMany({ where, - include: include as never, + include: { + musician: { select: { displayName: true } }, + visualizations: true, + stageTrackLinks: include.stageTrackLinks ? { include: { track: true } } : undefined, + }, orderBy: { createdAt: "desc" }, }); diff --git a/src/components/stages/MapPicker.tsx b/src/components/stages/MapPicker.tsx index 87c7141..9663756 100644 --- a/src/components/stages/MapPicker.tsx +++ b/src/components/stages/MapPicker.tsx @@ -58,6 +58,10 @@ export function MapPicker({ latitude, longitude, onLocationChange, radius = 50 } onLocationChange(lat, lng); }; + const [showManualEntry, setShowManualEntry] = useState(false); + const [manualLat, setManualLat] = useState(""); + const [manualLng, setManualLng] = useState(""); + const handleUseMyLocation = () => { if (!navigator.geolocation) { toast.error("Geolocation is not supported by your browser"); @@ -74,11 +78,24 @@ export function MapPicker({ latitude, longitude, onLocationChange, radius = 50 } (err) => { toast.error(`Could not get location: ${err.message}`); setIsLocating(false); + setShowManualEntry(true); }, { enableHighAccuracy: true } ); }; + const handleManualSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const lat = parseFloat(manualLat); + const lng = parseFloat(manualLng); + if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { + toast.error("Invalid coordinates. Lat: -90 to 90, Lng: -180 to 180"); + return; + } + handleLocationChange(lat, lng); + setShowManualEntry(false); + }; + // Calculate default center (default to SF if no coords provided) const defaultCenter: [number, number] = latitude && longitude ? [latitude, longitude] : [37.7749, -122.4194]; @@ -89,17 +106,29 @@ export function MapPicker({ latitude, longitude, onLocationChange, radius = 50 } Pin Your Stage
- +
+ + +
@@ -124,10 +153,50 @@ export function MapPicker({ latitude, longitude, onLocationChange, radius = 50 } - + {/* Radius indicator */} + + {showManualEntry && ( +
+

Enter coordinates manually

+
+ setManualLat(e.target.value)} + className="w-full px-2 py-1.5 text-xs bg-zinc-800 border border-zinc-700 rounded text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500" + /> + setManualLng(e.target.value)} + className="w-full px-2 py-1.5 text-xs bg-zinc-800 border border-zinc-700 rounded text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500" + /> +
+ + +
+
+
+ )}