Skip to content

Commit c4076bf

Browse files
Drake BotDrake Bot
authored andcommitted
update
1 parent 1d54b3e commit c4076bf

3 files changed

Lines changed: 242 additions & 51 deletions

File tree

frontend/src/app/Router.tsx

Lines changed: 228 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { createMatchEngine } from "../features/dating/MatchEngine";
2424
import { DatingFeed } from "../features/dating/DatingFeed";
2525
import { ChatWindow, type ChatApiClient } from "../features/chat/ChatWindow";
2626
import type { ChatMessage } from "../features/chat/chat.types";
27+
import { MapView } from "../features/map/MapView";
2728
import placeholderA from "../assets/reddoor-placeholder-1.svg";
2829
import placeholderB from "../assets/reddoor-placeholder-2.svg";
2930
import placeholderC from "../assets/reddoor-placeholder-3.svg";
@@ -6508,9 +6509,15 @@ function SettingsPanel({
65086509
const [adminCreateProfileDisplayName, setAdminCreateProfileDisplayName] = useState<string>("");
65096510
const [adminCreateProfileAge, setAdminCreateProfileAge] = useState<string>("");
65106511
const [adminCreateProfileBio, setAdminCreateProfileBio] = useState<string>("");
6511-
const [adminCreateProfileLat, setAdminCreateProfileLat] = useState<string>("");
6512-
const [adminCreateProfileLng, setAdminCreateProfileLng] = useState<string>("");
6512+
const [adminCreateMainPhotoFile, setAdminCreateMainPhotoFile] = useState<File | null>(null);
6513+
const [adminCreateGalleryPhotoFiles, setAdminCreateGalleryPhotoFiles] = useState<ReadonlyArray<File>>([]);
6514+
const [adminCreateVideoFile, setAdminCreateVideoFile] = useState<File | null>(null);
65136515
const [adminCreatedUserId, setAdminCreatedUserId] = useState<string | null>(null);
6516+
const [adminPendingUserPlacement, setAdminPendingUserPlacement] = useState<{
6517+
userId: string;
6518+
displayName: string;
6519+
position: { lat: number; lng: number };
6520+
} | null>(null);
65146521

65156522
async function refreshAdmin(): Promise<void> {
65166523
if (session.role !== "admin") {
@@ -6678,8 +6685,51 @@ function SettingsPanel({
66786685
setAdminStatus("Email, password, and display name are required.");
66796686
return;
66806687
}
6688+
6689+
// Get admin's current location
6690+
if (!("geolocation" in navigator)) {
6691+
setAdminStatus("Geolocation not available.");
6692+
return;
6693+
}
6694+
66816695
setAdminBusy(true);
6682-
setAdminStatus("");
6696+
setAdminStatus("Getting your location...");
6697+
6698+
try {
6699+
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
6700+
navigator.geolocation.getCurrentPosition(resolve, reject, {
6701+
enableHighAccuracy: true,
6702+
timeout: 10000,
6703+
maximumAge: 30000
6704+
});
6705+
});
6706+
6707+
const adminLocation = {
6708+
lat: position.coords.latitude,
6709+
lng: position.coords.longitude
6710+
};
6711+
6712+
// Create pending user state for placement
6713+
setAdminPendingUserPlacement({
6714+
userId: `pending-${Date.now()}`,
6715+
displayName: adminCreateProfileDisplayName.trim(),
6716+
position: adminLocation
6717+
});
6718+
6719+
setAdminStatus("Position the user on the map and confirm placement.");
6720+
} catch (e) {
6721+
setAdminStatus("Failed to get your location. Please check location permissions.");
6722+
} finally {
6723+
setAdminBusy(false);
6724+
}
6725+
}
6726+
6727+
async function adminConfirmUserPlacement(): Promise<void> {
6728+
if (!adminPendingUserPlacement) return;
6729+
6730+
setAdminBusy(true);
6731+
setAdminStatus("Creating user and profile...");
6732+
66836733
try {
66846734
// Create the user
66856735
const userResult = await api.adminCreateUser(session.sessionToken, {
@@ -6694,25 +6744,88 @@ function SettingsPanel({
66946744
const userId = userResult.user.id;
66956745
setAdminCreatedUserId(userId);
66966746

6697-
// Create the profile
6747+
// Upload photos if provided
6748+
let mainPhotoMediaId: string | undefined;
6749+
let galleryMediaIds: string[] = [];
6750+
6751+
if (adminCreateMainPhotoFile) {
6752+
try {
6753+
const initiated = await api.initiateMediaUpload(session.sessionToken, {
6754+
kind: "photo_main",
6755+
mimeType: adminCreateMainPhotoFile.type || "image/jpeg",
6756+
sizeBytes: adminCreateMainPhotoFile.size
6757+
});
6758+
6759+
const handledLocally = await uploadToLocalSignedUrl(initiated.uploadUrl, adminCreateMainPhotoFile, adminCreateMainPhotoFile.type || "image/jpeg");
6760+
if (!handledLocally) {
6761+
const uploadRes = await fetch(initiated.uploadUrl, {
6762+
method: "PUT",
6763+
headers: { "content-type": adminCreateMainPhotoFile.type || "image/jpeg" },
6764+
body: adminCreateMainPhotoFile
6765+
});
6766+
if (!uploadRes.ok) throw new Error(`Upload failed (${uploadRes.status})`);
6767+
}
6768+
6769+
await api.completeMediaUpload(session.sessionToken, initiated.mediaId);
6770+
mainPhotoMediaId = initiated.mediaId;
6771+
} catch (e) {
6772+
console.warn("Failed to upload main photo:", e);
6773+
}
6774+
}
6775+
6776+
// Upload gallery photos
6777+
for (const file of adminCreateGalleryPhotoFiles) {
6778+
try {
6779+
const initiated = await api.initiateMediaUpload(session.sessionToken, {
6780+
kind: "photo_gallery",
6781+
mimeType: file.type || "image/jpeg",
6782+
sizeBytes: file.size
6783+
});
6784+
6785+
const handledLocally = await uploadToLocalSignedUrl(initiated.uploadUrl, file, file.type || "image/jpeg");
6786+
if (!handledLocally) {
6787+
const uploadRes = await fetch(initiated.uploadUrl, {
6788+
method: "PUT",
6789+
headers: { "content-type": file.type || "image/jpeg" },
6790+
body: file
6791+
});
6792+
if (!uploadRes.ok) throw new Error(`Upload failed (${uploadRes.status})`);
6793+
}
6794+
6795+
await api.completeMediaUpload(session.sessionToken, initiated.mediaId);
6796+
galleryMediaIds.push(initiated.mediaId);
6797+
} catch (e) {
6798+
console.warn("Failed to upload gallery photo:", e);
6799+
}
6800+
}
6801+
6802+
// Create the profile with location
66986803
const age = parseInt(adminCreateProfileAge.trim()) || 25;
6699-
const lat = parseFloat(adminCreateProfileLat.trim()) || undefined;
6700-
const lng = parseFloat(adminCreateProfileLng.trim()) || undefined;
67016804

67026805
const profileData: any = {
67036806
displayName: adminCreateProfileDisplayName.trim(),
67046807
age,
67056808
bio: adminCreateProfileBio.trim() || ""
67066809
};
67076810

6708-
if (lat !== undefined && lng !== undefined) {
6709-
profileData.travelMode = {
6710-
enabled: true,
6711-
lat,
6712-
lng
6713-
};
6811+
// Add stats if provided (we'll need to add these to the form)
6812+
// For now, just basic profile
6813+
6814+
if (mainPhotoMediaId) {
6815+
profileData.mainPhotoMediaId = mainPhotoMediaId;
6816+
}
6817+
6818+
if (galleryMediaIds.length > 0) {
6819+
profileData.galleryMediaIds = galleryMediaIds;
67146820
}
67156821

6822+
// Set travel mode to the confirmed position
6823+
profileData.travelMode = {
6824+
enabled: true,
6825+
lat: adminPendingUserPlacement.position.lat,
6826+
lng: adminPendingUserPlacement.position.lng
6827+
};
6828+
67166829
await api.adminUpsertProfile(session.sessionToken, userId, profileData);
67176830

67186831
// Reset form
@@ -6725,8 +6838,10 @@ function SettingsPanel({
67256838
setAdminCreateProfileDisplayName("");
67266839
setAdminCreateProfileAge("");
67276840
setAdminCreateProfileBio("");
6728-
setAdminCreateProfileLat("");
6729-
setAdminCreateProfileLng("");
6841+
setAdminCreateMainPhotoFile(null);
6842+
setAdminCreateGalleryPhotoFiles([]);
6843+
setAdminCreateVideoFile(null);
6844+
setAdminPendingUserPlacement(null);
67306845

67316846
await refreshAdmin();
67326847
setAdminStatus(`User and profile created successfully. User ID: ${userId}`);
@@ -6739,6 +6854,20 @@ function SettingsPanel({
67396854
}
67406855
}
67416856

6857+
function adminCancelUserPlacement(): void {
6858+
setAdminPendingUserPlacement(null);
6859+
setAdminStatus("User creation cancelled.");
6860+
}
6861+
6862+
function adminUpdatePendingUserPosition(position: { lat: number; lng: number }): void {
6863+
if (adminPendingUserPlacement) {
6864+
setAdminPendingUserPlacement({
6865+
...adminPendingUserPlacement,
6866+
position
6867+
});
6868+
}
6869+
}
6870+
67426871
return (
67436872
<div style={{ display: "grid", gap: 12 }}>
67446873
<div style={cardStyle()}>
@@ -6912,41 +7041,18 @@ function SettingsPanel({
69127041
placeholder="John Doe"
69137042
/>
69147043
</label>
6915-
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
6916-
<label style={{ display: "grid", gap: 4 }}>
6917-
<span style={{ fontSize: 13, color: "#b9bec9" }}>Age</span>
6918-
<input
6919-
type="number"
6920-
value={adminCreateProfileAge}
6921-
onChange={(e) => setAdminCreateProfileAge(e.target.value)}
6922-
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
6923-
placeholder="25"
6924-
min="18"
6925-
max="120"
6926-
/>
6927-
</label>
6928-
<div style={{ display: "grid", gap: 4 }}>
6929-
<span style={{ fontSize: 13, color: "#b9bec9" }}>Location (optional)</span>
6930-
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
6931-
<input
6932-
type="number"
6933-
value={adminCreateProfileLat}
6934-
onChange={(e) => setAdminCreateProfileLat(e.target.value)}
6935-
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
6936-
placeholder="Lat"
6937-
step="0.000001"
6938-
/>
6939-
<input
6940-
type="number"
6941-
value={adminCreateProfileLng}
6942-
onChange={(e) => setAdminCreateProfileLng(e.target.value)}
6943-
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
6944-
placeholder="Lng"
6945-
step="0.000001"
6946-
/>
6947-
</div>
6948-
</div>
6949-
</div>
7044+
<label style={{ display: "grid", gap: 4 }}>
7045+
<span style={{ fontSize: 13, color: "#b9bec9" }}>Age</span>
7046+
<input
7047+
type="number"
7048+
value={adminCreateProfileAge}
7049+
onChange={(e) => setAdminCreateProfileAge(e.target.value)}
7050+
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
7051+
placeholder="25"
7052+
min="18"
7053+
max="120"
7054+
/>
7055+
</label>
69507056
<label style={{ display: "grid", gap: 4 }}>
69517057
<span style={{ fontSize: 13, color: "#b9bec9" }}>Bio (optional)</span>
69527058
<textarea
@@ -6957,16 +7063,88 @@ function SettingsPanel({
69577063
maxLength={280}
69587064
/>
69597065
</label>
7066+
<label style={{ display: "grid", gap: 4 }}>
7067+
<span style={{ fontSize: 13, color: "#b9bec9" }}>Main Photo</span>
7068+
<input
7069+
type="file"
7070+
accept="image/*"
7071+
onChange={(e) => setAdminCreateMainPhotoFile(e.target.files?.[0] || null)}
7072+
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
7073+
/>
7074+
</label>
7075+
<label style={{ display: "grid", gap: 4 }}>
7076+
<span style={{ fontSize: 13, color: "#b9bec9" }}>Gallery Photos</span>
7077+
<input
7078+
type="file"
7079+
accept="image/*"
7080+
multiple
7081+
onChange={(e) => setAdminCreateGalleryPhotoFiles(Array.from(e.target.files || []))}
7082+
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
7083+
/>
7084+
</label>
7085+
<label style={{ display: "grid", gap: 4 }}>
7086+
<span style={{ fontSize: 13, color: "#b9bec9" }}>Video (optional)</span>
7087+
<input
7088+
type="file"
7089+
accept="video/*"
7090+
onChange={(e) => setAdminCreateVideoFile(e.target.files?.[0] || null)}
7091+
style={{ padding: 8, border: "1px solid #444", borderRadius: 4, background: "#2a2d32", color: "#fff" }}
7092+
/>
7093+
</label>
69607094
<button
69617095
type="button"
69627096
style={buttonSecondary(adminBusy)}
69637097
disabled={adminBusy}
69647098
onClick={() => void adminCreateUserAndProfile()}
69657099
>
6966-
CREATE USER & PROFILE
7100+
CREATE USER & POSITION ON MAP
69677101
</button>
69687102
</div>
69697103
</div>
7104+
7105+
{adminPendingUserPlacement && (
7106+
<div style={{ display: "grid", gap: 6 }}>
7107+
<div style={{ fontSize: 15, fontWeight: 700 }}>POSITION USER ON MAP</div>
7108+
<div style={{ color: "#b9bec9", fontSize: 13 }}>
7109+
Drag the marker to position the user, then confirm placement.
7110+
</div>
7111+
<div style={{ height: 400, border: "1px solid #444", borderRadius: 4, overflow: "hidden" }}>
7112+
<MapView
7113+
initialView={{
7114+
center: adminPendingUserPlacement.position,
7115+
zoom: 15
7116+
}}
7117+
markers={[{
7118+
id: adminPendingUserPlacement.userId,
7119+
position: adminPendingUserPlacement.position,
7120+
color: "#C00000",
7121+
draggable: true,
7122+
onDragEnd: adminUpdatePendingUserPosition
7123+
}]}
7124+
visible={true}
7125+
onMapClick={() => {}}
7126+
/>
7127+
</div>
7128+
<div style={{ display: "grid", gridTemplateColumns: "auto auto", gap: 8 }}>
7129+
<button
7130+
type="button"
7131+
style={buttonPrimary(adminBusy)}
7132+
disabled={adminBusy}
7133+
onClick={() => void adminConfirmUserPlacement()}
7134+
>
7135+
{adminBusy ? "CREATING..." : "CONFIRM PLACEMENT & CREATE USER"}
7136+
</button>
7137+
<button
7138+
type="button"
7139+
style={buttonSecondary(adminBusy)}
7140+
disabled={adminBusy}
7141+
onClick={() => void adminCancelUserPlacement()}
7142+
>
7143+
CANCEL
7144+
</button>
7145+
</div>
7146+
</div>
7147+
)}
69707148
</div>
69717149
</div>
69727150
) : null}

frontend/src/features/map/MapView.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,20 @@ export function MapView({
287287
markerButton.addEventListener("touchend", trigger, { passive: false });
288288
markerButton.addEventListener("pointerup", trigger);
289289
}
290-
const marker = new ml.Marker({ element: markerButton } as unknown as Record<string, unknown>)
290+
const marker = new ml.Marker({
291+
element: markerButton,
292+
draggable: m.draggable === true
293+
} as unknown as Record<string, unknown>)
291294
.setLngLat([m.position.lng, m.position.lat]);
292295
if (popup) marker.setPopup(popup);
296+
297+
if (m.draggable && typeof m.onDragEnd === "function") {
298+
marker.on("dragend", () => {
299+
const lngLat = marker.getLngLat();
300+
m.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });
301+
});
302+
}
303+
293304
marker.addTo(map);
294305
markersRef.current.push(marker);
295306
}

frontend/src/features/map/map.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type MapMarker = Readonly<{
1212
label?: string;
1313
imageUrl?: string;
1414
onClick?: () => void;
15+
draggable?: boolean;
16+
onDragEnd?: (position: LngLat) => void;
1517
}>;
1618

1719
export type MapViewOptions = Readonly<{

0 commit comments

Comments
 (0)